const TRACEID_GPU_TIMINGS = 'GpuTimings'; const version$1 = '2.15.1'; const revision = '70fef1e'; function extend(target, ex) { for(const prop in ex){ const copy = ex[prop]; if (Array.isArray(copy)) { target[prop] = extend([], copy); } else if (copy && typeof copy === 'object') { target[prop] = extend({}, copy); } else { target[prop] = copy; } } return target; } const guid = { create () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=>{ const r = Math.random() * 16 | 0; const v = c === 'x' ? r : r & 0x3 | 0x8; return v.toString(16); }); } }; const path = { delimiter: '/', join (...sections) { let result = sections[0]; for(let i = 0; i < sections.length - 1; i++){ const one = sections[i]; const two = sections[i + 1]; if (two[0] === path.delimiter) { result = two; continue; } if (one && two && one[one.length - 1] !== path.delimiter && two[0] !== path.delimiter) { result += path.delimiter + two; } else { result += two; } } return result; }, normalize (pathname) { const lead = pathname.startsWith(path.delimiter); const trail = pathname.endsWith(path.delimiter); const parts = pathname.split('/'); let result = ''; let cleaned = []; for(let i = 0; i < parts.length; i++){ if (parts[i] === '') continue; if (parts[i] === '.') continue; if (parts[i] === '..' && cleaned.length > 0) { cleaned = cleaned.slice(0, cleaned.length - 2); continue; } if (i > 0) cleaned.push(path.delimiter); cleaned.push(parts[i]); } result = cleaned.join(''); if (!lead && result[0] === path.delimiter) { result = result.slice(1); } if (trail && result[result.length - 1] !== path.delimiter) { result += path.delimiter; } return result; }, split (pathname) { const lastDelimiterIndex = pathname.lastIndexOf(path.delimiter); if (lastDelimiterIndex !== -1) { return [ pathname.substring(0, lastDelimiterIndex), pathname.substring(lastDelimiterIndex + 1) ]; } return [ '', pathname ]; }, getBasename (pathname) { return path.split(pathname)[1]; }, getDirectory (pathname) { return path.split(pathname)[0]; }, getExtension (pathname) { const ext = pathname.split('?')[0].split('.').pop(); if (ext !== pathname) { return `.${ext}`; } return ''; }, isRelativePath (pathname) { return pathname.charAt(0) !== '/' && pathname.match(/:\/\//) === null; }, extractPath (pathname) { let result = ''; const parts = pathname.split('/'); let i = 0; if (parts.length > 1) { if (path.isRelativePath(pathname)) { if (parts[0] === '.') { for(i = 0; i < parts.length - 1; ++i){ result += i === 0 ? parts[i] : `/${parts[i]}`; } } else if (parts[0] === '..') { for(i = 0; i < parts.length - 1; ++i){ result += i === 0 ? parts[i] : `/${parts[i]}`; } } else { result = '.'; for(i = 0; i < parts.length - 1; ++i){ result += `/${parts[i]}`; } } } else { for(i = 0; i < parts.length - 1; ++i){ result += i === 0 ? parts[i] : `/${parts[i]}`; } } } return result; } }; const detectPassiveEvents = ()=>{ let result = false; try { const opts = Object.defineProperty({}, 'passive', { get: function() { result = true; return false; } }); window.addEventListener('testpassive', null, opts); window.removeEventListener('testpassive', null, opts); } catch (e) {} return result; }; const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''; const environment = typeof window !== 'undefined' ? 'browser' : typeof global !== 'undefined' ? 'node' : 'worker'; const platformName = /android/i.test(ua) ? 'android' : /ip(?:[ao]d|hone)/i.test(ua) ? 'ios' : /windows/i.test(ua) ? 'windows' : /mac os/i.test(ua) ? 'osx' : /linux/i.test(ua) ? 'linux' : /cros/i.test(ua) ? 'cros' : null; const browserName = environment !== 'browser' ? null : /Chrome\/|Chromium\/|Edg.*\//.test(ua) ? 'chrome' : /Safari\//.test(ua) ? 'safari' : /Firefox\//.test(ua) ? 'firefox' : 'other'; const touch = environment === 'browser' && ('ontouchstart' in window || 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0); const passiveEvents = detectPassiveEvents(); const platform = { name: platformName, environment: environment, browser: environment === 'browser', worker: environment === 'worker', desktop: [ 'windows', 'osx', 'linux', 'cros' ].includes(platformName), mobile: [ 'android', 'ios' ].includes(platformName), ios: platformName === 'ios', android: platformName === 'android', touch: touch, passiveEvents: passiveEvents, browserName: browserName }; const ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'; const ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const ASCII_LETTERS = ASCII_LOWERCASE + ASCII_UPPERCASE; const HIGH_SURROGATE_BEGIN = 0xD800; const HIGH_SURROGATE_END = 0xDBFF; const LOW_SURROGATE_BEGIN = 0xDC00; const LOW_SURROGATE_END = 0xDFFF; const ZERO_WIDTH_JOINER = 0x200D; const REGIONAL_INDICATOR_BEGIN = 0x1F1E6; const REGIONAL_INDICATOR_END = 0x1F1FF; const FITZPATRICK_MODIFIER_BEGIN = 0x1F3FB; const FITZPATRICK_MODIFIER_END = 0x1F3FF; const DIACRITICAL_MARKS_BEGIN = 0x20D0; const DIACRITICAL_MARKS_END = 0x20FF; const VARIATION_MODIFIER_BEGIN = 0xFE00; const VARIATION_MODIFIER_END = 0xFE0F; function getCodePointData(string, i = 0) { const size = string.length; if (i < 0 || i >= size) { return null; } const first = string.charCodeAt(i); if (size > 1 && first >= HIGH_SURROGATE_BEGIN && first <= HIGH_SURROGATE_END) { const second = string.charCodeAt(i + 1); if (second >= LOW_SURROGATE_BEGIN && second <= LOW_SURROGATE_END) { return { code: (first - HIGH_SURROGATE_BEGIN) * 0x400 + second - LOW_SURROGATE_BEGIN + 0x10000, long: true }; } } return { code: first, long: false }; } function isCodeBetween(string, begin, end) { if (!string) { return false; } const codeData = getCodePointData(string); if (codeData) { const code = codeData.code; return code >= begin && code <= end; } return false; } function numCharsToTakeForNextSymbol(string, index) { if (index === string.length - 1) { return 1; } if (isCodeBetween(string[index], HIGH_SURROGATE_BEGIN, HIGH_SURROGATE_END)) { const first = string.substring(index, index + 2); const second = string.substring(index + 2, index + 4); if (isCodeBetween(second, FITZPATRICK_MODIFIER_BEGIN, FITZPATRICK_MODIFIER_END) || isCodeBetween(first, REGIONAL_INDICATOR_BEGIN, REGIONAL_INDICATOR_END) && isCodeBetween(second, REGIONAL_INDICATOR_BEGIN, REGIONAL_INDICATOR_END)) { return 4; } if (isCodeBetween(second, VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) { return 3; } return 2; } if (isCodeBetween(string[index + 1], VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) { return 2; } return 1; } const string = { ASCII_LOWERCASE: ASCII_LOWERCASE, ASCII_UPPERCASE: ASCII_UPPERCASE, ASCII_LETTERS: ASCII_LETTERS, format (s, ...args) { for(let i = 0; i < args.length; i++){ s = s.replace(`{${i}}`, args[i]); } return s; }, getCodePoint (string, i) { const codePointData = getCodePointData(string, i); return codePointData && codePointData.code; }, getCodePoints (string) { if (typeof string !== 'string') { throw new TypeError('Not a string'); } let i = 0; const arr = []; let codePoint; while(!!(codePoint = getCodePointData(string, i))){ arr.push(codePoint.code); i += codePoint.long ? 2 : 1; } return arr; }, getSymbols (string) { if (typeof string !== 'string') { throw new TypeError('Not a string'); } let index = 0; const length = string.length; const output = []; let take = 0; let ch; while(index < length){ take += numCharsToTakeForNextSymbol(string, index + take); ch = string[index + take]; if (isCodeBetween(ch, DIACRITICAL_MARKS_BEGIN, DIACRITICAL_MARKS_END)) { ch = string[index + take++]; } if (isCodeBetween(ch, VARIATION_MODIFIER_BEGIN, VARIATION_MODIFIER_END)) { ch = string[index + take++]; } if (ch && ch.charCodeAt(0) === ZERO_WIDTH_JOINER) { ch = string[index + take++]; continue; } const char = string.substring(index, index + take); output.push(char); index += take; take = 0; } return output; }, fromCodePoint (...args) { return args.map((codePoint)=>{ if (codePoint > 0xFFFF) { codePoint -= 0x10000; return String.fromCharCode((codePoint >> 10) + 0xD800, codePoint % 0x400 + 0xDC00); } return String.fromCharCode(codePoint); }).join(''); } }; class EventHandle { off() { if (this._removed) return; this.handler.offByHandle(this); } on(name, callback, scope = this) { return this.handler._addCallback(name, callback, scope, false); } once(name, callback, scope = this) { return this.handler._addCallback(name, callback, scope, true); } set removed(value) { if (!value) return; this._removed = true; } get removed() { return this._removed; } toJSON(key) { return undefined; } constructor(handler, name, callback, scope, once = false){ this._removed = false; this.handler = handler; this.name = name; this.callback = callback; this.scope = scope; this._once = once; } } class EventHandler { initEventHandler() { this._callbacks = new Map(); this._callbackActive = new Map(); } _addCallback(name, callback, scope, once) { if (!this._callbacks.has(name)) { this._callbacks.set(name, []); } if (this._callbackActive.has(name)) { const callbackActive = this._callbackActive.get(name); if (callbackActive && callbackActive === this._callbacks.get(name)) { this._callbackActive.set(name, callbackActive.slice()); } } const evt = new EventHandle(this, name, callback, scope, once); this._callbacks.get(name).push(evt); return evt; } on(name, callback, scope = this) { return this._addCallback(name, callback, scope, false); } once(name, callback, scope = this) { return this._addCallback(name, callback, scope, true); } off(name, callback, scope) { if (name) { if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) { this._callbackActive.set(name, this._callbackActive.get(name).slice()); } } else { for (const [key, callbacks] of this._callbackActive){ if (!this._callbacks.has(key)) { continue; } if (this._callbacks.get(key) !== callbacks) { continue; } this._callbackActive.set(key, callbacks.slice()); } } if (!name) { for (const callbacks of this._callbacks.values()){ for(let i = 0; i < callbacks.length; i++){ callbacks[i].removed = true; } } this._callbacks.clear(); } else if (!callback) { const callbacks = this._callbacks.get(name); if (callbacks) { for(let i = 0; i < callbacks.length; i++){ callbacks[i].removed = true; } this._callbacks.delete(name); } } else { const callbacks = this._callbacks.get(name); if (!callbacks) { return this; } for(let i = 0; i < callbacks.length; i++){ if (callbacks[i].callback !== callback) { continue; } if (scope && callbacks[i].scope !== scope) { continue; } callbacks[i].removed = true; callbacks.splice(i, 1); i--; } if (callbacks.length === 0) { this._callbacks.delete(name); } } return this; } offByHandle(handle) { const name = handle.name; handle.removed = true; if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) { this._callbackActive.set(name, this._callbackActive.get(name).slice()); } const callbacks = this._callbacks.get(name); if (!callbacks) { return this; } const ind = callbacks.indexOf(handle); if (ind !== -1) { callbacks.splice(ind, 1); if (callbacks.length === 0) { this._callbacks.delete(name); } } return this; } fire(name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) { if (!name) { return this; } const callbacksInitial = this._callbacks.get(name); if (!callbacksInitial) { return this; } let callbacks; if (!this._callbackActive.has(name)) { this._callbackActive.set(name, callbacksInitial); } else if (this._callbackActive.get(name) !== callbacksInitial) { callbacks = callbacksInitial.slice(); } for(let i = 0; (callbacks || this._callbackActive.get(name)) && i < (callbacks || this._callbackActive.get(name)).length; i++){ const evt = (callbacks || this._callbackActive.get(name))[i]; if (!evt.callback) continue; evt.callback.call(evt.scope, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); if (evt._once) { const existingCallback = this._callbacks.get(name); const ind = existingCallback ? existingCallback.indexOf(evt) : -1; if (ind !== -1) { if (this._callbackActive.get(name) === existingCallback) { this._callbackActive.set(name, this._callbackActive.get(name).slice()); } const callbacks = this._callbacks.get(name); if (!callbacks) continue; callbacks[ind].removed = true; callbacks.splice(ind, 1); if (callbacks.length === 0) { this._callbacks.delete(name); } } } } if (!callbacks) { this._callbackActive.delete(name); } return this; } hasEvent(name) { return !!this._callbacks.get(name)?.length; } constructor(){ this._callbacks = new Map(); this._callbackActive = new Map(); } } class IndexedList { push(key, item) { if (this._index[key]) { throw Error(`Key already in index ${key}`); } const location = this._list.push(item) - 1; this._index[key] = location; } has(key) { return this._index[key] !== undefined; } get(key) { const location = this._index[key]; if (location !== undefined) { return this._list[location]; } return null; } remove(key) { const location = this._index[key]; if (location !== undefined) { this._list.splice(location, 1); delete this._index[key]; for(key in this._index){ const idx = this._index[key]; if (idx > location) { this._index[key] = idx - 1; } } return true; } return false; } list() { return this._list; } clear() { this._list.length = 0; for(const prop in this._index){ delete this._index[prop]; } } constructor(){ this._list = []; this._index = {}; } } const cachedResult = (func)=>{ const uninitToken = {}; let result = uninitToken; return ()=>{ if (result === uninitToken) { result = func(); } return result; }; }; class Impl { static loadScript(url, callback) { const s = document.createElement("script"); s.setAttribute('src', url); s.onload = ()=>{ callback(null); }; s.onerror = ()=>{ callback(`Failed to load script='${url}'`); }; document.body.appendChild(s); } static loadWasm(moduleName, config, callback) { const loadUrl = Impl.wasmSupported() && config.glueUrl && config.wasmUrl ? config.glueUrl : config.fallbackUrl; if (loadUrl) { Impl.loadScript(loadUrl, (err)=>{ if (err) { callback(err, null); } else { const module = window[moduleName]; window[moduleName] = undefined; module({ locateFile: ()=>config.wasmUrl, onAbort: ()=>{ callback('wasm module aborted.'); } }).then((instance)=>{ callback(null, instance); }); } }); } else { callback('No supported wasm modules found.', null); } } static getModule(name) { if (!Impl.modules.hasOwnProperty(name)) { Impl.modules[name] = { config: null, initializing: false, instance: null, callbacks: [] }; } return Impl.modules[name]; } static initialize(moduleName, module) { if (module.initializing) { return; } const config = module.config; if (config.glueUrl || config.wasmUrl || config.fallbackUrl) { module.initializing = true; Impl.loadWasm(moduleName, config, (err, instance)=>{ if (err) { if (config.errorHandler) { config.errorHandler(err); } else { console.error(`failed to initialize module=${moduleName} error=${err}`); } } else { module.instance = instance; module.callbacks.forEach((callback)=>{ callback(instance); }); } }); } } } Impl.modules = {}; Impl.wasmSupported = cachedResult(()=>{ try { if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') { const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); if (module instanceof WebAssembly.Module) { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } } catch (e) {} return false; }); class WasmModule { static setConfig(moduleName, config) { const module = Impl.getModule(moduleName); module.config = config; if (module.callbacks.length > 0) { Impl.initialize(moduleName, module); } } static getConfig(moduleName) { return Impl.modules?.[moduleName]?.config; } static getInstance(moduleName, callback) { const module = Impl.getModule(moduleName); if (module.instance) { callback(module.instance); } else { module.callbacks.push(callback); if (module.config) { Impl.initialize(moduleName, module); } } } } class ReadStream { get remainingBytes() { return this.dataView.byteLength - this.offset; } reset(offset = 0) { this.offset = offset; } skip(bytes) { this.offset += bytes; } align(bytes) { this.offset = this.offset + bytes - 1 & ~(bytes - 1); } _inc(amount) { this.offset += amount; return this.offset - amount; } readChar() { return String.fromCharCode(this.dataView.getUint8(this.offset++)); } readChars(numChars) { let result = ''; for(let i = 0; i < numChars; ++i){ result += this.readChar(); } return result; } readU8() { return this.dataView.getUint8(this.offset++); } readU16() { return this.dataView.getUint16(this._inc(2), true); } readU32() { return this.dataView.getUint32(this._inc(4), true); } readU64() { return this.readU32() + 2 ** 32 * this.readU32(); } readU32be() { return this.dataView.getUint32(this._inc(4), false); } readArray(result) { for(let i = 0; i < result.length; ++i){ result[i] = this.readU8(); } } readLine() { const view = this.dataView; let result = ''; while(true){ if (this.offset >= view.byteLength) { break; } const c = String.fromCharCode(this.readU8()); if (c === '\n') { break; } result += c; } return result; } constructor(arraybuffer){ this.offset = 0; this.arraybuffer = arraybuffer; this.dataView = new DataView(arraybuffer); } } class SortedLoopArray { _binarySearch(item) { let left = 0; let right = this.items.length - 1; const search = item[this._sortBy]; let middle; let current; while(left <= right){ middle = Math.floor((left + right) / 2); current = this.items[middle][this._sortBy]; if (current <= search) { left = middle + 1; } else if (current > search) { right = middle - 1; } } return left; } _doSort(a, b) { const sortBy = this._sortBy; return a[sortBy] - b[sortBy]; } insert(item) { const index = this._binarySearch(item); this.items.splice(index, 0, item); this.length++; if (this.loopIndex >= index) { this.loopIndex++; } } append(item) { this.items.push(item); this.length++; } remove(item) { const idx = this.items.indexOf(item); if (idx < 0) return; this.items.splice(idx, 1); this.length--; if (this.loopIndex >= idx) { this.loopIndex--; } } sort() { const current = this.loopIndex >= 0 ? this.items[this.loopIndex] : null; this.items.sort(this._sortHandler); if (current !== null) { this.loopIndex = this.items.indexOf(current); } } constructor(args){ this.items = []; this.length = 0; this.loopIndex = -1; this._sortBy = args.sortBy; this._sortHandler = this._doSort.bind(this); } } class Tags extends EventHandler { add(...args) { let changed = false; const tags = this._processArguments(args, true); if (!tags.length) { return changed; } for(let i = 0; i < tags.length; i++){ if (this._index[tags[i]]) { continue; } changed = true; this._index[tags[i]] = true; this._list.push(tags[i]); this.fire('add', tags[i], this._parent); } if (changed) { this.fire('change', this._parent); } return changed; } remove(...args) { let changed = false; if (!this._list.length) { return changed; } const tags = this._processArguments(args, true); if (!tags.length) { return changed; } for(let i = 0; i < tags.length; i++){ if (!this._index[tags[i]]) { continue; } changed = true; delete this._index[tags[i]]; this._list.splice(this._list.indexOf(tags[i]), 1); this.fire('remove', tags[i], this._parent); } if (changed) { this.fire('change', this._parent); } return changed; } clear() { if (!this._list.length) { return; } const tags = this._list.slice(0); this._list = []; this._index = {}; for(let i = 0; i < tags.length; i++){ this.fire('remove', tags[i], this._parent); } this.fire('change', this._parent); } has(...query) { if (!this._list.length) { return false; } return this._has(this._processArguments(query)); } _has(tags) { if (!this._list.length || !tags.length) { return false; } for(let i = 0; i < tags.length; i++){ if (tags[i].length === 1) { if (this._index[tags[i][0]]) { return true; } } else { let multiple = true; for(let t = 0; t < tags[i].length; t++){ if (this._index[tags[i][t]]) { continue; } multiple = false; break; } if (multiple) { return true; } } } return false; } list() { return this._list.slice(0); } _processArguments(args, flat) { const tags = []; let tmp = []; if (!args || !args.length) { return tags; } for(let i = 0; i < args.length; i++){ if (args[i] instanceof Array) { if (!flat) { tmp = []; } for(let t = 0; t < args[i].length; t++){ if (typeof args[i][t] !== 'string') { continue; } if (flat) { tags.push(args[i][t]); } else { tmp.push(args[i][t]); } } if (!flat && tmp.length) { tags.push(tmp); } } else if (typeof args[i] === 'string') { if (flat) { tags.push(args[i]); } else { tags.push([ args[i] ]); } } } return tags; } get size() { return this._list.length; } constructor(parent){ super(), this._index = {}, this._list = []; this._parent = parent; } } Tags.EVENT_ADD = 'add'; Tags.EVENT_REMOVE = 'remove'; Tags.EVENT_CHANGE = 'change'; const now = typeof window !== 'undefined' && window.performance && window.performance.now ? performance.now.bind(performance) : Date.now; const re = /^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; class URI { toString() { let s = ''; if (this.scheme) { s += `${this.scheme}:`; } if (this.authority) { s += `//${this.authority}`; } s += this.path; if (this.query) { s += `?${this.query}`; } if (this.fragment) { s += `#${this.fragment}`; } return s; } getQuery() { const result = {}; if (this.query) { const queryParams = decodeURIComponent(this.query).split('&'); for (const queryParam of queryParams){ const pair = queryParam.split('='); result[pair[0]] = pair[1]; } } return result; } setQuery(params) { let q = ''; for(const key in params){ if (params.hasOwnProperty(key)) { if (q !== '') { q += '&'; } q += `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`; } } this.query = q; } constructor(uri){ const result = uri.match(re); this.scheme = result[2]; this.authority = result[4]; this.path = result[5]; this.query = result[7]; this.fragment = result[9]; } } class Tracing { static set(channel, enabled = true) {} static get(channel) { return Tracing._traceChannels.has(channel); } } Tracing._traceChannels = new Set(); Tracing.stack = false; const CURVE_LINEAR = 0; const CURVE_SMOOTHSTEP = 1; const CURVE_SPLINE = 4; const CURVE_STEP = 5; const math = { DEG_TO_RAD: Math.PI / 180, RAD_TO_DEG: 180 / Math.PI, clamp (value, min, max) { if (value >= max) return max; if (value <= min) return min; return value; }, intToBytes24 (i) { const r = i >> 16 & 0xff; const g = i >> 8 & 0xff; const b = i & 0xff; return [ r, g, b ]; }, intToBytes32 (i) { const r = i >> 24 & 0xff; const g = i >> 16 & 0xff; const b = i >> 8 & 0xff; const a = i & 0xff; return [ r, g, b, a ]; }, bytesToInt24 (r, g, b) { if (r.length) { b = r[2]; g = r[1]; r = r[0]; } return r << 16 | g << 8 | b; }, bytesToInt32 (r, g, b, a) { if (r.length) { a = r[3]; b = r[2]; g = r[1]; r = r[0]; } return (r << 24 | g << 16 | b << 8 | a) >>> 0; }, lerp (a, b, alpha) { return a + (b - a) * math.clamp(alpha, 0, 1); }, lerpAngle (a, b, alpha) { if (b - a > 180) { b -= 360; } if (b - a < -180) { b += 360; } return math.lerp(a, b, math.clamp(alpha, 0, 1)); }, powerOfTwo (x) { return x !== 0 && !(x & x - 1); }, nextPowerOfTwo (val) { val--; val |= val >> 1; val |= val >> 2; val |= val >> 4; val |= val >> 8; val |= val >> 16; val++; return val; }, nearestPowerOfTwo (val) { return Math.pow(2, Math.round(Math.log2(val))); }, random (min, max) { const diff = max - min; return Math.random() * diff + min; }, smoothstep (min, max, x) { if (x <= min) return 0; if (x >= max) return 1; x = (x - min) / (max - min); return x * x * (3 - 2 * x); }, smootherstep (min, max, x) { if (x <= min) return 0; if (x >= max) return 1; x = (x - min) / (max - min); return x * x * x * (x * (x * 6 - 15) + 10); }, roundUp (numToRound, multiple) { if (multiple === 0) { return numToRound; } return Math.ceil(numToRound / multiple) * multiple; }, between (num, a, b, inclusive) { const min = Math.min(a, b); const max = Math.max(a, b); return inclusive ? num >= min && num <= max : num > min && num < max; } }; class Color { clone() { const cstr = this.constructor; return new cstr(this.r, this.g, this.b, this.a); } copy(rhs) { this.r = rhs.r; this.g = rhs.g; this.b = rhs.b; this.a = rhs.a; return this; } equals(rhs) { return this.r === rhs.r && this.g === rhs.g && this.b === rhs.b && this.a === rhs.a; } set(r, g, b, a = 1) { this.r = r; this.g = g; this.b = b; this.a = a; return this; } lerp(lhs, rhs, alpha) { this.r = lhs.r + alpha * (rhs.r - lhs.r); this.g = lhs.g + alpha * (rhs.g - lhs.g); this.b = lhs.b + alpha * (rhs.b - lhs.b); this.a = lhs.a + alpha * (rhs.a - lhs.a); return this; } linear(src = this) { this.r = Math.pow(src.r, 2.2); this.g = Math.pow(src.g, 2.2); this.b = Math.pow(src.b, 2.2); this.a = src.a; return this; } gamma(src = this) { this.r = Math.pow(src.r, 1 / 2.2); this.g = Math.pow(src.g, 1 / 2.2); this.b = Math.pow(src.b, 1 / 2.2); this.a = src.a; return this; } mulScalar(scalar) { this.r *= scalar; this.g *= scalar; this.b *= scalar; return this; } fromString(hex) { const i = parseInt(hex.replace('#', '0x'), 16); let bytes; if (hex.length > 7) { bytes = math.intToBytes32(i); } else { bytes = math.intToBytes24(i); bytes[3] = 255; } this.set(bytes[0] / 255, bytes[1] / 255, bytes[2] / 255, bytes[3] / 255); return this; } fromArray(arr, offset = 0) { this.r = arr[offset] ?? this.r; this.g = arr[offset + 1] ?? this.g; this.b = arr[offset + 2] ?? this.b; this.a = arr[offset + 3] ?? this.a; return this; } toString(alpha, asArray) { const { r, g, b, a } = this; if (asArray || r > 1 || g > 1 || b > 1) { return `${r.toFixed(3)}, ${g.toFixed(3)}, ${b.toFixed(3)}, ${a.toFixed(3)}`; } let s = `#${((1 << 24) + (Math.round(r * 255) << 16) + (Math.round(g * 255) << 8) + Math.round(b * 255)).toString(16).slice(1)}`; if (alpha === true) { const aa = Math.round(a * 255).toString(16); if (this.a < 16 / 255) { s += `0${aa}`; } else { s += aa; } } return s; } toArray(arr = [], offset = 0, alpha = true) { arr[offset] = this.r; arr[offset + 1] = this.g; arr[offset + 2] = this.b; if (alpha) { arr[offset + 3] = this.a; } return arr; } constructor(r = 0, g = 0, b = 0, a = 1){ const length = r.length; if (length === 3 || length === 4) { this.r = r[0]; this.g = r[1]; this.b = r[2]; this.a = r[3] ?? 1; } else { this.r = r; this.g = g; this.b = b; this.a = a; } } } Color.BLACK = Object.freeze(new Color(0, 0, 0, 1)); Color.BLUE = Object.freeze(new Color(0, 0, 1, 1)); Color.CYAN = Object.freeze(new Color(0, 1, 1, 1)); Color.GRAY = Object.freeze(new Color(0.5, 0.5, 0.5, 1)); Color.GREEN = Object.freeze(new Color(0, 1, 0, 1)); Color.MAGENTA = Object.freeze(new Color(1, 0, 1, 1)); Color.RED = Object.freeze(new Color(1, 0, 0, 1)); Color.WHITE = Object.freeze(new Color(1, 1, 1, 1)); Color.YELLOW = Object.freeze(new Color(1, 1, 0, 1)); class CurveEvaluator { evaluate(time, forceReset = false) { if (forceReset || time < this._left || time >= this._right) { this._reset(time); } let result; const type = this._curve.type; if (type === CURVE_STEP) { result = this._p0; } else { const t = this._recip === 0 ? 0 : (time - this._left) * this._recip; if (type === CURVE_LINEAR) { result = math.lerp(this._p0, this._p1, t); } else if (type === CURVE_SMOOTHSTEP) { result = math.lerp(this._p0, this._p1, t * t * (3 - 2 * t)); } else { result = this._evaluateHermite(this._p0, this._p1, this._m0, this._m1, t); } } return result; } _reset(time) { const keys = this._curve.keys; const len = keys.length; if (!len) { this._left = -Infinity; this._right = Infinity; this._recip = 0; this._p0 = this._p1 = this._m0 = this._m1 = 0; } else { if (time < keys[0][0]) { this._left = -Infinity; this._right = keys[0][0]; this._recip = 0; this._p0 = this._p1 = keys[0][1]; this._m0 = this._m1 = 0; } else if (time >= keys[len - 1][0]) { this._left = keys[len - 1][0]; this._right = Infinity; this._recip = 0; this._p0 = this._p1 = keys[len - 1][1]; this._m0 = this._m1 = 0; } else { let index = 0; while(time >= keys[index + 1][0]){ index++; } this._left = keys[index][0]; this._right = keys[index + 1][0]; const diff = 1.0 / (this._right - this._left); this._recip = isFinite(diff) ? diff : 0; this._p0 = keys[index][1]; this._p1 = keys[index + 1][1]; if (this._curve.type === CURVE_SPLINE) { this._calcTangents(keys, index); } } } } _calcTangents(keys, index) { let a; const b = keys[index]; const c = keys[index + 1]; let d; if (index === 0) { a = [ keys[0][0] + (keys[0][0] - keys[1][0]), keys[0][1] + (keys[0][1] - keys[1][1]) ]; } else { a = keys[index - 1]; } if (index === keys.length - 2) { d = [ keys[index + 1][0] + (keys[index + 1][0] - keys[index][0]), keys[index + 1][1] + (keys[index + 1][1] - keys[index][1]) ]; } else { d = keys[index + 2]; } if (this._curve.type === CURVE_SPLINE) { const s1_ = 2 * (c[0] - b[0]) / (c[0] - a[0]); const s2_ = 2 * (c[0] - b[0]) / (d[0] - b[0]); this._m0 = this._curve.tension * (isFinite(s1_) ? s1_ : 0) * (c[1] - a[1]); this._m1 = this._curve.tension * (isFinite(s2_) ? s2_ : 0) * (d[1] - b[1]); } else { const s1 = (c[0] - b[0]) / (b[0] - a[0]); const s2 = (c[0] - b[0]) / (d[0] - c[0]); const a_ = b[1] + (a[1] - b[1]) * (isFinite(s1) ? s1 : 0); const d_ = c[1] + (d[1] - c[1]) * (isFinite(s2) ? s2 : 0); const tension = this._curve.tension; this._m0 = tension * (c[1] - a_); this._m1 = tension * (d_ - b[1]); } } _evaluateHermite(p0, p1, m0, m1, t) { const t2 = t * t; const twot = t + t; const omt = 1 - t; const omt2 = omt * omt; return p0 * ((1 + twot) * omt2) + m0 * (t * omt2) + p1 * (t2 * (3 - twot)) + m1 * (t2 * (t - 1)); } constructor(curve, time = 0){ this._left = -Infinity; this._right = Infinity; this._recip = 0; this._p0 = 0; this._p1 = 0; this._m0 = 0; this._m1 = 0; this._curve = curve; this._reset(time); } } class Curve { get length() { return this.keys.length; } add(time, value) { const keys = this.keys; const len = keys.length; let i = 0; for(; i < len; i++){ if (keys[i][0] > time) { break; } } const key = [ time, value ]; this.keys.splice(i, 0, key); return key; } get(index) { return this.keys[index]; } sort() { this.keys.sort((a, b)=>a[0] - b[0]); } value(time) { return this._eval.evaluate(time, true); } closest(time) { const keys = this.keys; const length = keys.length; let min = 2; let result = null; for(let i = 0; i < length; i++){ const diff = Math.abs(time - keys[i][0]); if (min >= diff) { min = diff; result = keys[i]; } else { break; } } return result; } clone() { const result = new this.constructor(); result.keys = this.keys.map((key)=>[ ...key ]); result.type = this.type; result.tension = this.tension; return result; } quantize(precision) { precision = Math.max(precision, 2); const values = new Float32Array(precision); const step = 1.0 / (precision - 1); values[0] = this._eval.evaluate(0, true); for(let i = 1; i < precision; i++){ values[i] = this._eval.evaluate(step * i); } return values; } quantizeClamped(precision, min, max) { const result = this.quantize(precision); for(let i = 0; i < result.length; ++i){ result[i] = Math.min(max, Math.max(min, result[i])); } return result; } constructor(data){ this.keys = []; this.type = CURVE_SMOOTHSTEP; this.tension = 0.5; this._eval = new CurveEvaluator(this); if (data) { for(let i = 0; i < data.length - 1; i += 2){ this.keys.push([ data[i], data[i + 1] ]); } } this.sort(); } } class CurveSet { get length() { return this.curves.length; } set type(value) { this._type = value; for(let i = 0; i < this.curves.length; i++){ this.curves[i].type = value; } } get type() { return this._type; } get(index) { return this.curves[index]; } value(time, result = []) { const length = this.curves.length; result.length = length; for(let i = 0; i < length; i++){ result[i] = this.curves[i].value(time); } return result; } clone() { const result = new this.constructor(); result.curves = []; for(let i = 0; i < this.curves.length; i++){ result.curves.push(this.curves[i].clone()); } result._type = this._type; return result; } quantize(precision) { precision = Math.max(precision, 2); const numCurves = this.curves.length; const values = new Float32Array(precision * numCurves); const step = 1.0 / (precision - 1); for(let c = 0; c < numCurves; c++){ const ev = new CurveEvaluator(this.curves[c]); for(let i = 0; i < precision; i++){ values[i * numCurves + c] = ev.evaluate(step * i); } } return values; } quantizeClamped(precision, min, max) { const result = this.quantize(precision); for(let i = 0; i < result.length; ++i){ result[i] = Math.min(max, Math.max(min, result[i])); } return result; } constructor(...args){ this.curves = []; this._type = CURVE_SMOOTHSTEP; if (args.length > 1) { for(let i = 0; i < args.length; i++){ this.curves.push(new Curve(args[i])); } } else if (args.length === 0) { this.curves.push(new Curve()); } else { const arg = args[0]; if (typeof arg === 'number') { for(let i = 0; i < arg; i++){ this.curves.push(new Curve()); } } else { for(let i = 0; i < arg.length; i++){ this.curves.push(new Curve(arg[i])); } } } } } const floatView = new Float32Array(1); const int32View = new Int32Array(floatView.buffer); class FloatPacking { static float2Half(value) { floatView[0] = value; const x = int32View[0]; let bits = x >> 16 & 0x8000; let m = x >> 12 & 0x07ff; const e = x >> 23 & 0xff; if (e < 103) { return bits; } if (e > 142) { bits |= 0x7c00; bits |= (e === 255 ? 0 : 1) && x & 0x007fffff; return bits; } if (e < 113) { m |= 0x0800; bits |= (m >> 114 - e) + (m >> 113 - e & 1); return bits; } bits |= e - 112 << 10 | m >> 1; bits += m & 1; return bits; } static float2RGBA8(value, data) { floatView[0] = value; const intBits = int32View[0]; data.r = (intBits >> 24 & 0xFF) / 255.0; data.g = (intBits >> 16 & 0xFF) / 255.0; data.b = (intBits >> 8 & 0xFF) / 255.0; data.a = (intBits & 0xFF) / 255.0; } } class Kernel { static concentric(numRings, numPoints) { const kernel = []; kernel.push(0, 0); const spacing = 2 * Math.PI / numRings / numPoints; for(let ring = 1; ring <= numRings; ring++){ const radius = ring / numRings; const circumference = 2 * Math.PI * radius; const pointsPerRing = Math.max(1, Math.floor(circumference / spacing)); const angleStep = 2 * Math.PI / pointsPerRing; for(let point = 0; point < pointsPerRing; point++){ const angle = point * angleStep; const x = radius * Math.cos(angle); const y = radius * Math.sin(angle); kernel.push(x, y); } } return kernel; } } class Vec3 { add(rhs) { this.x += rhs.x; this.y += rhs.y; this.z += rhs.z; return this; } add2(lhs, rhs) { this.x = lhs.x + rhs.x; this.y = lhs.y + rhs.y; this.z = lhs.z + rhs.z; return this; } addScalar(scalar) { this.x += scalar; this.y += scalar; this.z += scalar; return this; } addScaled(rhs, scalar) { this.x += rhs.x * scalar; this.y += rhs.y * scalar; this.z += rhs.z * scalar; return this; } clone() { const cstr = this.constructor; return new cstr(this.x, this.y, this.z); } copy(rhs) { this.x = rhs.x; this.y = rhs.y; this.z = rhs.z; return this; } cross(lhs, rhs) { const lx = lhs.x; const ly = lhs.y; const lz = lhs.z; const rx = rhs.x; const ry = rhs.y; const rz = rhs.z; this.x = ly * rz - ry * lz; this.y = lz * rx - rz * lx; this.z = lx * ry - rx * ly; return this; } distance(rhs) { const x = this.x - rhs.x; const y = this.y - rhs.y; const z = this.z - rhs.z; return Math.sqrt(x * x + y * y + z * z); } div(rhs) { this.x /= rhs.x; this.y /= rhs.y; this.z /= rhs.z; return this; } div2(lhs, rhs) { this.x = lhs.x / rhs.x; this.y = lhs.y / rhs.y; this.z = lhs.z / rhs.z; return this; } divScalar(scalar) { this.x /= scalar; this.y /= scalar; this.z /= scalar; return this; } dot(rhs) { return this.x * rhs.x + this.y * rhs.y + this.z * rhs.z; } equals(rhs) { return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z; } equalsApprox(rhs, epsilon = 1e-6) { return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon; } length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } lengthSq() { return this.x * this.x + this.y * this.y + this.z * this.z; } lerp(lhs, rhs, alpha) { this.x = lhs.x + alpha * (rhs.x - lhs.x); this.y = lhs.y + alpha * (rhs.y - lhs.y); this.z = lhs.z + alpha * (rhs.z - lhs.z); return this; } mul(rhs) { this.x *= rhs.x; this.y *= rhs.y; this.z *= rhs.z; return this; } mul2(lhs, rhs) { this.x = lhs.x * rhs.x; this.y = lhs.y * rhs.y; this.z = lhs.z * rhs.z; return this; } mulScalar(scalar) { this.x *= scalar; this.y *= scalar; this.z *= scalar; return this; } normalize(src = this) { const lengthSq = src.x * src.x + src.y * src.y + src.z * src.z; if (lengthSq > 0) { const invLength = 1 / Math.sqrt(lengthSq); this.x = src.x * invLength; this.y = src.y * invLength; this.z = src.z * invLength; } return this; } floor(src = this) { this.x = Math.floor(src.x); this.y = Math.floor(src.y); this.z = Math.floor(src.z); return this; } ceil(src = this) { this.x = Math.ceil(src.x); this.y = Math.ceil(src.y); this.z = Math.ceil(src.z); return this; } round(src = this) { this.x = Math.round(src.x); this.y = Math.round(src.y); this.z = Math.round(src.z); return this; } min(rhs) { if (rhs.x < this.x) this.x = rhs.x; if (rhs.y < this.y) this.y = rhs.y; if (rhs.z < this.z) this.z = rhs.z; return this; } max(rhs) { if (rhs.x > this.x) this.x = rhs.x; if (rhs.y > this.y) this.y = rhs.y; if (rhs.z > this.z) this.z = rhs.z; return this; } project(rhs) { const a_dot_b = this.x * rhs.x + this.y * rhs.y + this.z * rhs.z; const b_dot_b = rhs.x * rhs.x + rhs.y * rhs.y + rhs.z * rhs.z; const s = a_dot_b / b_dot_b; this.x = rhs.x * s; this.y = rhs.y * s; this.z = rhs.z * s; return this; } set(x, y, z) { this.x = x; this.y = y; this.z = z; return this; } sub(rhs) { this.x -= rhs.x; this.y -= rhs.y; this.z -= rhs.z; return this; } sub2(lhs, rhs) { this.x = lhs.x - rhs.x; this.y = lhs.y - rhs.y; this.z = lhs.z - rhs.z; return this; } subScalar(scalar) { this.x -= scalar; this.y -= scalar; this.z -= scalar; return this; } fromArray(arr, offset = 0) { this.x = arr[offset] ?? this.x; this.y = arr[offset + 1] ?? this.y; this.z = arr[offset + 2] ?? this.z; return this; } toString() { return `[${this.x}, ${this.y}, ${this.z}]`; } toArray(arr = [], offset = 0) { arr[offset] = this.x; arr[offset + 1] = this.y; arr[offset + 2] = this.z; return arr; } constructor(x = 0, y = 0, z = 0){ if (x.length === 3) { this.x = x[0]; this.y = x[1]; this.z = x[2]; } else { this.x = x; this.y = y; this.z = z; } } } Vec3.ZERO = Object.freeze(new Vec3(0, 0, 0)); Vec3.HALF = Object.freeze(new Vec3(0.5, 0.5, 0.5)); Vec3.ONE = Object.freeze(new Vec3(1, 1, 1)); Vec3.UP = Object.freeze(new Vec3(0, 1, 0)); Vec3.DOWN = Object.freeze(new Vec3(0, -1, 0)); Vec3.RIGHT = Object.freeze(new Vec3(1, 0, 0)); Vec3.LEFT = Object.freeze(new Vec3(-1, 0, 0)); Vec3.FORWARD = Object.freeze(new Vec3(0, 0, -1)); Vec3.BACK = Object.freeze(new Vec3(0, 0, 1)); class Mat3 { clone() { const cstr = this.constructor; return new cstr().copy(this); } copy(rhs) { const src = rhs.data; const dst = this.data; dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = src[3]; dst[4] = src[4]; dst[5] = src[5]; dst[6] = src[6]; dst[7] = src[7]; dst[8] = src[8]; return this; } set(src) { const dst = this.data; dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = src[3]; dst[4] = src[4]; dst[5] = src[5]; dst[6] = src[6]; dst[7] = src[7]; dst[8] = src[8]; return this; } getX(x = new Vec3()) { return x.set(this.data[0], this.data[1], this.data[2]); } getY(y = new Vec3()) { return y.set(this.data[3], this.data[4], this.data[5]); } getZ(z = new Vec3()) { return z.set(this.data[6], this.data[7], this.data[8]); } equals(rhs) { const l = this.data; const r = rhs.data; return l[0] === r[0] && l[1] === r[1] && l[2] === r[2] && l[3] === r[3] && l[4] === r[4] && l[5] === r[5] && l[6] === r[6] && l[7] === r[7] && l[8] === r[8]; } isIdentity() { const m = this.data; return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 0 && m[4] === 1 && m[5] === 0 && m[6] === 0 && m[7] === 0 && m[8] === 1; } setIdentity() { const m = this.data; m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 1; m[5] = 0; m[6] = 0; m[7] = 0; m[8] = 1; return this; } toString() { return `[${this.data.join(', ')}]`; } transpose(src = this) { const s = src.data; const t = this.data; if (s === t) { let tmp; tmp = s[1]; t[1] = s[3]; t[3] = tmp; tmp = s[2]; t[2] = s[6]; t[6] = tmp; tmp = s[5]; t[5] = s[7]; t[7] = tmp; } else { t[0] = s[0]; t[1] = s[3]; t[2] = s[6]; t[3] = s[1]; t[4] = s[4]; t[5] = s[7]; t[6] = s[2]; t[7] = s[5]; t[8] = s[8]; } return this; } setFromMat4(m) { const src = m.data; const dst = this.data; dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = src[4]; dst[4] = src[5]; dst[5] = src[6]; dst[6] = src[8]; dst[7] = src[9]; dst[8] = src[10]; return this; } setFromQuat(r) { const qx = r.x; const qy = r.y; const qz = r.z; const qw = r.w; const x2 = qx + qx; const y2 = qy + qy; const z2 = qz + qz; const xx = qx * x2; const xy = qx * y2; const xz = qx * z2; const yy = qy * y2; const yz = qy * z2; const zz = qz * z2; const wx = qw * x2; const wy = qw * y2; const wz = qw * z2; const m = this.data; m[0] = 1 - (yy + zz); m[1] = xy + wz; m[2] = xz - wy; m[3] = xy - wz; m[4] = 1 - (xx + zz); m[5] = yz + wx; m[6] = xz + wy; m[7] = yz - wx; m[8] = 1 - (xx + yy); return this; } invertMat4(src) { const s = src.data; const a0 = s[0]; const a1 = s[1]; const a2 = s[2]; const a4 = s[4]; const a5 = s[5]; const a6 = s[6]; const a8 = s[8]; const a9 = s[9]; const a10 = s[10]; const b11 = a10 * a5 - a6 * a9; const b21 = -a10 * a1 + a2 * a9; const b31 = a6 * a1 - a2 * a5; const b12 = -a10 * a4 + a6 * a8; const b22 = a10 * a0 - a2 * a8; const b32 = -a6 * a0 + a2 * a4; const b13 = a9 * a4 - a5 * a8; const b23 = -a9 * a0 + a1 * a8; const b33 = a5 * a0 - a1 * a4; const det = a0 * b11 + a1 * b12 + a2 * b13; if (det === 0) { this.setIdentity(); } else { const invDet = 1 / det; const t = this.data; t[0] = b11 * invDet; t[1] = b21 * invDet; t[2] = b31 * invDet; t[3] = b12 * invDet; t[4] = b22 * invDet; t[5] = b32 * invDet; t[6] = b13 * invDet; t[7] = b23 * invDet; t[8] = b33 * invDet; } return this; } transformVector(vec, res = new Vec3()) { const m = this.data; const { x, y, z } = vec; res.x = x * m[0] + y * m[3] + z * m[6]; res.y = x * m[1] + y * m[4] + z * m[7]; res.z = x * m[2] + y * m[5] + z * m[8]; return res; } constructor(){ this.data = new Float32Array(9); this.data[0] = this.data[4] = this.data[8] = 1; } } Mat3.IDENTITY = Object.freeze(new Mat3()); Mat3.ZERO = Object.freeze(new Mat3().set([ 0, 0, 0, 0, 0, 0, 0, 0, 0 ])); class Vec2 { add(rhs) { this.x += rhs.x; this.y += rhs.y; return this; } add2(lhs, rhs) { this.x = lhs.x + rhs.x; this.y = lhs.y + rhs.y; return this; } addScalar(scalar) { this.x += scalar; this.y += scalar; return this; } addScaled(rhs, scalar) { this.x += rhs.x * scalar; this.y += rhs.y * scalar; return this; } clone() { const cstr = this.constructor; return new cstr(this.x, this.y); } copy(rhs) { this.x = rhs.x; this.y = rhs.y; return this; } cross(rhs) { return this.x * rhs.y - this.y * rhs.x; } distance(rhs) { const x = this.x - rhs.x; const y = this.y - rhs.y; return Math.sqrt(x * x + y * y); } div(rhs) { this.x /= rhs.x; this.y /= rhs.y; return this; } div2(lhs, rhs) { this.x = lhs.x / rhs.x; this.y = lhs.y / rhs.y; return this; } divScalar(scalar) { this.x /= scalar; this.y /= scalar; return this; } dot(rhs) { return this.x * rhs.x + this.y * rhs.y; } equals(rhs) { return this.x === rhs.x && this.y === rhs.y; } equalsApprox(rhs, epsilon = 1e-6) { return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon; } length() { return Math.sqrt(this.x * this.x + this.y * this.y); } lengthSq() { return this.x * this.x + this.y * this.y; } lerp(lhs, rhs, alpha) { this.x = lhs.x + alpha * (rhs.x - lhs.x); this.y = lhs.y + alpha * (rhs.y - lhs.y); return this; } mul(rhs) { this.x *= rhs.x; this.y *= rhs.y; return this; } mul2(lhs, rhs) { this.x = lhs.x * rhs.x; this.y = lhs.y * rhs.y; return this; } mulScalar(scalar) { this.x *= scalar; this.y *= scalar; return this; } normalize(src = this) { const lengthSq = src.x * src.x + src.y * src.y; if (lengthSq > 0) { const invLength = 1 / Math.sqrt(lengthSq); this.x = src.x * invLength; this.y = src.y * invLength; } return this; } rotate(degrees) { const angle = Math.atan2(this.x, this.y) + degrees * math.DEG_TO_RAD; const len = Math.sqrt(this.x * this.x + this.y * this.y); this.x = Math.sin(angle) * len; this.y = Math.cos(angle) * len; return this; } angle() { return Math.atan2(this.x, this.y) * math.RAD_TO_DEG; } angleTo(rhs) { return Math.atan2(this.x * rhs.y + this.y * rhs.x, this.x * rhs.x + this.y * rhs.y) * math.RAD_TO_DEG; } floor(src = this) { this.x = Math.floor(src.x); this.y = Math.floor(src.y); return this; } ceil(src = this) { this.x = Math.ceil(src.x); this.y = Math.ceil(src.y); return this; } round(src = this) { this.x = Math.round(src.x); this.y = Math.round(src.y); return this; } min(rhs) { if (rhs.x < this.x) this.x = rhs.x; if (rhs.y < this.y) this.y = rhs.y; return this; } max(rhs) { if (rhs.x > this.x) this.x = rhs.x; if (rhs.y > this.y) this.y = rhs.y; return this; } set(x, y) { this.x = x; this.y = y; return this; } sub(rhs) { this.x -= rhs.x; this.y -= rhs.y; return this; } sub2(lhs, rhs) { this.x = lhs.x - rhs.x; this.y = lhs.y - rhs.y; return this; } subScalar(scalar) { this.x -= scalar; this.y -= scalar; return this; } fromArray(arr, offset = 0) { this.x = arr[offset] ?? this.x; this.y = arr[offset + 1] ?? this.y; return this; } toString() { return `[${this.x}, ${this.y}]`; } toArray(arr = [], offset = 0) { arr[offset] = this.x; arr[offset + 1] = this.y; return arr; } static angleRad(lhs, rhs) { return Math.atan2(lhs.x * rhs.y - lhs.y * rhs.x, lhs.x * rhs.x + lhs.y * rhs.y); } constructor(x = 0, y = 0){ if (x.length === 2) { this.x = x[0]; this.y = x[1]; } else { this.x = x; this.y = y; } } } Vec2.ZERO = Object.freeze(new Vec2(0, 0)); Vec2.HALF = Object.freeze(new Vec2(0.5, 0.5)); Vec2.ONE = Object.freeze(new Vec2(1, 1)); Vec2.UP = Object.freeze(new Vec2(0, 1)); Vec2.DOWN = Object.freeze(new Vec2(0, -1)); Vec2.RIGHT = Object.freeze(new Vec2(1, 0)); Vec2.LEFT = Object.freeze(new Vec2(-1, 0)); class Vec4 { add(rhs) { this.x += rhs.x; this.y += rhs.y; this.z += rhs.z; this.w += rhs.w; return this; } add2(lhs, rhs) { this.x = lhs.x + rhs.x; this.y = lhs.y + rhs.y; this.z = lhs.z + rhs.z; this.w = lhs.w + rhs.w; return this; } addScalar(scalar) { this.x += scalar; this.y += scalar; this.z += scalar; this.w += scalar; return this; } addScaled(rhs, scalar) { this.x += rhs.x * scalar; this.y += rhs.y * scalar; this.z += rhs.z * scalar; this.w += rhs.w * scalar; return this; } clone() { const cstr = this.constructor; return new cstr(this.x, this.y, this.z, this.w); } copy(rhs) { this.x = rhs.x; this.y = rhs.y; this.z = rhs.z; this.w = rhs.w; return this; } div(rhs) { this.x /= rhs.x; this.y /= rhs.y; this.z /= rhs.z; this.w /= rhs.w; return this; } div2(lhs, rhs) { this.x = lhs.x / rhs.x; this.y = lhs.y / rhs.y; this.z = lhs.z / rhs.z; this.w = lhs.w / rhs.w; return this; } divScalar(scalar) { this.x /= scalar; this.y /= scalar; this.z /= scalar; this.w /= scalar; return this; } dot(rhs) { return this.x * rhs.x + this.y * rhs.y + this.z * rhs.z + this.w * rhs.w; } equals(rhs) { return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z && this.w === rhs.w; } equalsApprox(rhs, epsilon = 1e-6) { return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon && Math.abs(this.w - rhs.w) < epsilon; } length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); } lengthSq() { return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w; } lerp(lhs, rhs, alpha) { this.x = lhs.x + alpha * (rhs.x - lhs.x); this.y = lhs.y + alpha * (rhs.y - lhs.y); this.z = lhs.z + alpha * (rhs.z - lhs.z); this.w = lhs.w + alpha * (rhs.w - lhs.w); return this; } mul(rhs) { this.x *= rhs.x; this.y *= rhs.y; this.z *= rhs.z; this.w *= rhs.w; return this; } mul2(lhs, rhs) { this.x = lhs.x * rhs.x; this.y = lhs.y * rhs.y; this.z = lhs.z * rhs.z; this.w = lhs.w * rhs.w; return this; } mulScalar(scalar) { this.x *= scalar; this.y *= scalar; this.z *= scalar; this.w *= scalar; return this; } normalize(src = this) { const lengthSq = src.x * src.x + src.y * src.y + src.z * src.z + src.w * src.w; if (lengthSq > 0) { const invLength = 1 / Math.sqrt(lengthSq); this.x = src.x * invLength; this.y = src.y * invLength; this.z = src.z * invLength; this.w = src.w * invLength; } return this; } floor(src = this) { this.x = Math.floor(src.x); this.y = Math.floor(src.y); this.z = Math.floor(src.z); this.w = Math.floor(src.w); return this; } ceil(src = this) { this.x = Math.ceil(src.x); this.y = Math.ceil(src.y); this.z = Math.ceil(src.z); this.w = Math.ceil(src.w); return this; } round(src = this) { this.x = Math.round(src.x); this.y = Math.round(src.y); this.z = Math.round(src.z); this.w = Math.round(src.w); return this; } min(rhs) { if (rhs.x < this.x) this.x = rhs.x; if (rhs.y < this.y) this.y = rhs.y; if (rhs.z < this.z) this.z = rhs.z; if (rhs.w < this.w) this.w = rhs.w; return this; } max(rhs) { if (rhs.x > this.x) this.x = rhs.x; if (rhs.y > this.y) this.y = rhs.y; if (rhs.z > this.z) this.z = rhs.z; if (rhs.w > this.w) this.w = rhs.w; return this; } set(x, y, z, w) { this.x = x; this.y = y; this.z = z; this.w = w; return this; } sub(rhs) { this.x -= rhs.x; this.y -= rhs.y; this.z -= rhs.z; this.w -= rhs.w; return this; } sub2(lhs, rhs) { this.x = lhs.x - rhs.x; this.y = lhs.y - rhs.y; this.z = lhs.z - rhs.z; this.w = lhs.w - rhs.w; return this; } subScalar(scalar) { this.x -= scalar; this.y -= scalar; this.z -= scalar; this.w -= scalar; return this; } fromArray(arr, offset = 0) { this.x = arr[offset] ?? this.x; this.y = arr[offset + 1] ?? this.y; this.z = arr[offset + 2] ?? this.z; this.w = arr[offset + 3] ?? this.w; return this; } toString() { return `[${this.x}, ${this.y}, ${this.z}, ${this.w}]`; } toArray(arr = [], offset = 0) { arr[offset] = this.x; arr[offset + 1] = this.y; arr[offset + 2] = this.z; arr[offset + 3] = this.w; return arr; } constructor(x = 0, y = 0, z = 0, w = 0){ if (x.length === 4) { this.x = x[0]; this.y = x[1]; this.z = x[2]; this.w = x[3]; } else { this.x = x; this.y = y; this.z = z; this.w = w; } } } Vec4.ZERO = Object.freeze(new Vec4(0, 0, 0, 0)); Vec4.HALF = Object.freeze(new Vec4(0.5, 0.5, 0.5, 0.5)); Vec4.ONE = Object.freeze(new Vec4(1, 1, 1, 1)); const _halfSize$1 = new Vec2(); const x = new Vec3(); const y = new Vec3(); const z = new Vec3(); const scale = new Vec3(); class Mat4 { static _getPerspectiveHalfSize(halfSize, fov, aspect, znear, fovIsHorizontal) { if (fovIsHorizontal) { halfSize.x = znear * Math.tan(fov * Math.PI / 360); halfSize.y = halfSize.x / aspect; } else { halfSize.y = znear * Math.tan(fov * Math.PI / 360); halfSize.x = halfSize.y * aspect; } } add2(lhs, rhs) { const a = lhs.data, b = rhs.data, r = this.data; r[0] = a[0] + b[0]; r[1] = a[1] + b[1]; r[2] = a[2] + b[2]; r[3] = a[3] + b[3]; r[4] = a[4] + b[4]; r[5] = a[5] + b[5]; r[6] = a[6] + b[6]; r[7] = a[7] + b[7]; r[8] = a[8] + b[8]; r[9] = a[9] + b[9]; r[10] = a[10] + b[10]; r[11] = a[11] + b[11]; r[12] = a[12] + b[12]; r[13] = a[13] + b[13]; r[14] = a[14] + b[14]; r[15] = a[15] + b[15]; return this; } add(rhs) { return this.add2(this, rhs); } clone() { const cstr = this.constructor; return new cstr().copy(this); } copy(rhs) { const src = rhs.data, dst = this.data; dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = src[3]; dst[4] = src[4]; dst[5] = src[5]; dst[6] = src[6]; dst[7] = src[7]; dst[8] = src[8]; dst[9] = src[9]; dst[10] = src[10]; dst[11] = src[11]; dst[12] = src[12]; dst[13] = src[13]; dst[14] = src[14]; dst[15] = src[15]; return this; } equals(rhs) { const l = this.data, r = rhs.data; return l[0] === r[0] && l[1] === r[1] && l[2] === r[2] && l[3] === r[3] && l[4] === r[4] && l[5] === r[5] && l[6] === r[6] && l[7] === r[7] && l[8] === r[8] && l[9] === r[9] && l[10] === r[10] && l[11] === r[11] && l[12] === r[12] && l[13] === r[13] && l[14] === r[14] && l[15] === r[15]; } isIdentity() { const m = this.data; return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 0 && m[4] === 0 && m[5] === 1 && m[6] === 0 && m[7] === 0 && m[8] === 0 && m[9] === 0 && m[10] === 1 && m[11] === 0 && m[12] === 0 && m[13] === 0 && m[14] === 0 && m[15] === 1; } mul2(lhs, rhs) { const a = lhs.data; const b = rhs.data; const r = this.data; 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]; let b0, b1, b2, b3; b0 = b[0]; b1 = b[1]; b2 = b[2]; b3 = b[3]; r[0] = a00 * b0 + a10 * b1 + a20 * b2 + a30 * b3; r[1] = a01 * b0 + a11 * b1 + a21 * b2 + a31 * b3; r[2] = a02 * b0 + a12 * b1 + a22 * b2 + a32 * b3; r[3] = a03 * b0 + a13 * b1 + a23 * b2 + a33 * b3; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; r[4] = a00 * b0 + a10 * b1 + a20 * b2 + a30 * b3; r[5] = a01 * b0 + a11 * b1 + a21 * b2 + a31 * b3; r[6] = a02 * b0 + a12 * b1 + a22 * b2 + a32 * b3; r[7] = a03 * b0 + a13 * b1 + a23 * b2 + a33 * b3; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; r[8] = a00 * b0 + a10 * b1 + a20 * b2 + a30 * b3; r[9] = a01 * b0 + a11 * b1 + a21 * b2 + a31 * b3; r[10] = a02 * b0 + a12 * b1 + a22 * b2 + a32 * b3; r[11] = a03 * b0 + a13 * b1 + a23 * b2 + a33 * b3; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; r[12] = a00 * b0 + a10 * b1 + a20 * b2 + a30 * b3; r[13] = a01 * b0 + a11 * b1 + a21 * b2 + a31 * b3; r[14] = a02 * b0 + a12 * b1 + a22 * b2 + a32 * b3; r[15] = a03 * b0 + a13 * b1 + a23 * b2 + a33 * b3; return this; } mulAffine2(lhs, rhs) { const a = lhs.data; const b = rhs.data; const r = this.data; const a00 = a[0]; const a01 = a[1]; const a02 = a[2]; const a10 = a[4]; const a11 = a[5]; const a12 = a[6]; const a20 = a[8]; const a21 = a[9]; const a22 = a[10]; const a30 = a[12]; const a31 = a[13]; const a32 = a[14]; let b0, b1, b2; b0 = b[0]; b1 = b[1]; b2 = b[2]; r[0] = a00 * b0 + a10 * b1 + a20 * b2; r[1] = a01 * b0 + a11 * b1 + a21 * b2; r[2] = a02 * b0 + a12 * b1 + a22 * b2; r[3] = 0; b0 = b[4]; b1 = b[5]; b2 = b[6]; r[4] = a00 * b0 + a10 * b1 + a20 * b2; r[5] = a01 * b0 + a11 * b1 + a21 * b2; r[6] = a02 * b0 + a12 * b1 + a22 * b2; r[7] = 0; b0 = b[8]; b1 = b[9]; b2 = b[10]; r[8] = a00 * b0 + a10 * b1 + a20 * b2; r[9] = a01 * b0 + a11 * b1 + a21 * b2; r[10] = a02 * b0 + a12 * b1 + a22 * b2; r[11] = 0; b0 = b[12]; b1 = b[13]; b2 = b[14]; r[12] = a00 * b0 + a10 * b1 + a20 * b2 + a30; r[13] = a01 * b0 + a11 * b1 + a21 * b2 + a31; r[14] = a02 * b0 + a12 * b1 + a22 * b2 + a32; r[15] = 1; return this; } mul(rhs) { return this.mul2(this, rhs); } transformPoint(vec, res = new Vec3()) { const m = this.data; const { x, y, z } = vec; res.x = x * m[0] + y * m[4] + z * m[8] + m[12]; res.y = x * m[1] + y * m[5] + z * m[9] + m[13]; res.z = x * m[2] + y * m[6] + z * m[10] + m[14]; return res; } transformVector(vec, res = new Vec3()) { const m = this.data; const { x, y, z } = vec; res.x = x * m[0] + y * m[4] + z * m[8]; res.y = x * m[1] + y * m[5] + z * m[9]; res.z = x * m[2] + y * m[6] + z * m[10]; return res; } transformVec4(vec, res = new Vec4()) { const m = this.data; const { x, y, z, w } = vec; res.x = x * m[0] + y * m[4] + z * m[8] + w * m[12]; res.y = x * m[1] + y * m[5] + z * m[9] + w * m[13]; res.z = x * m[2] + y * m[6] + z * m[10] + w * m[14]; res.w = x * m[3] + y * m[7] + z * m[11] + w * m[15]; return res; } setLookAt(position, target, up) { z.sub2(position, target).normalize(); y.copy(up).normalize(); x.cross(y, z).normalize(); y.cross(z, x); const r = this.data; r[0] = x.x; r[1] = x.y; r[2] = x.z; r[3] = 0; r[4] = y.x; r[5] = y.y; r[6] = y.z; r[7] = 0; r[8] = z.x; r[9] = z.y; r[10] = z.z; r[11] = 0; r[12] = position.x; r[13] = position.y; r[14] = position.z; r[15] = 1; return this; } setFrustum(left, right, bottom, top, znear, zfar) { const temp1 = 2 * znear; const temp2 = right - left; const temp3 = top - bottom; const temp4 = zfar - znear; const r = this.data; r[0] = temp1 / temp2; r[1] = 0; r[2] = 0; r[3] = 0; r[4] = 0; r[5] = temp1 / temp3; r[6] = 0; r[7] = 0; r[8] = (right + left) / temp2; r[9] = (top + bottom) / temp3; r[10] = (-zfar - znear) / temp4; r[11] = -1; r[12] = 0; r[13] = 0; r[14] = -temp1 * zfar / temp4; r[15] = 0; return this; } setPerspective(fov, aspect, znear, zfar, fovIsHorizontal) { Mat4._getPerspectiveHalfSize(_halfSize$1, fov, aspect, znear, fovIsHorizontal); return this.setFrustum(-_halfSize$1.x, _halfSize$1.x, -_halfSize$1.y, _halfSize$1.y, znear, zfar); } setOrtho(left, right, bottom, top, near, far) { const r = this.data; r[0] = 2 / (right - left); r[1] = 0; r[2] = 0; r[3] = 0; r[4] = 0; r[5] = 2 / (top - bottom); r[6] = 0; r[7] = 0; r[8] = 0; r[9] = 0; r[10] = -2 / (far - near); r[11] = 0; r[12] = -(right + left) / (right - left); r[13] = -(top + bottom) / (top - bottom); r[14] = -(far + near) / (far - near); r[15] = 1; return this; } setFromAxisAngle(axis, angle) { angle *= math.DEG_TO_RAD; const { x, y, z } = axis; const c = Math.cos(angle); const s = Math.sin(angle); const t = 1 - c; const tx = t * x; const ty = t * y; const m = this.data; m[0] = tx * x + c; m[1] = tx * y + s * z; m[2] = tx * z - s * y; m[3] = 0; m[4] = tx * y - s * z; m[5] = ty * y + c; m[6] = ty * z + s * x; m[7] = 0; m[8] = tx * z + s * y; m[9] = ty * z - x * s; m[10] = t * z * z + c; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; return this; } setTranslate(x, y, z) { const m = this.data; m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0; m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0; m[12] = x; m[13] = y; m[14] = z; m[15] = 1; return this; } setScale(x, y, z) { const m = this.data; m[0] = x; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 0; m[5] = y; m[6] = 0; m[7] = 0; m[8] = 0; m[9] = 0; m[10] = z; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; return this; } setViewport(x, y, width, height) { const m = this.data; m[0] = width * 0.5; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 0; m[5] = height * 0.5; m[6] = 0; m[7] = 0; m[8] = 0; m[9] = 0; m[10] = 0.5; m[11] = 0; m[12] = x + width * 0.5; m[13] = y + height * 0.5; m[14] = 0.5; m[15] = 1; return this; } setReflection(normal, distance) { const a = normal.x; const b = normal.y; const c = normal.z; const data = this.data; data[0] = 1.0 - 2 * a * a; data[1] = -2 * a * b; data[2] = -2 * a * c; data[3] = 0; data[4] = -2 * a * b; data[5] = 1.0 - 2 * b * b; data[6] = -2 * b * c; data[7] = 0; data[8] = -2 * a * c; data[9] = -2 * b * c; data[10] = 1.0 - 2 * c * c; data[11] = 0; data[12] = -2 * a * distance; data[13] = -2 * b * distance; data[14] = -2 * c * distance; data[15] = 1; return this; } invert(src = this) { const s = src.data; const a00 = s[0]; const a01 = s[1]; const a02 = s[2]; const a03 = s[3]; const a10 = s[4]; const a11 = s[5]; const a12 = s[6]; const a13 = s[7]; const a20 = s[8]; const a21 = s[9]; const a22 = s[10]; const a23 = s[11]; const a30 = s[12]; const a31 = s[13]; const a32 = s[14]; const a33 = s[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; const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (det === 0) { this.setIdentity(); } else { const invDet = 1 / det; const t = this.data; t[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet; t[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * invDet; t[2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet; t[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * invDet; t[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * invDet; t[5] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet; t[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * invDet; t[7] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet; t[8] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet; t[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * invDet; t[10] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet; t[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * invDet; t[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * invDet; t[13] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet; t[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * invDet; t[15] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet; } return this; } set(src) { const dst = this.data; dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; dst[3] = src[3]; dst[4] = src[4]; dst[5] = src[5]; dst[6] = src[6]; dst[7] = src[7]; dst[8] = src[8]; dst[9] = src[9]; dst[10] = src[10]; dst[11] = src[11]; dst[12] = src[12]; dst[13] = src[13]; dst[14] = src[14]; dst[15] = src[15]; return this; } setIdentity() { const m = this.data; m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0; m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0; m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; return this; } setTRS(t, r, s) { const qx = r.x; const qy = r.y; const qz = r.z; const qw = r.w; const sx = s.x; const sy = s.y; const sz = s.z; const x2 = qx + qx; const y2 = qy + qy; const z2 = qz + qz; const xx = qx * x2; const xy = qx * y2; const xz = qx * z2; const yy = qy * y2; const yz = qy * z2; const zz = qz * z2; const wx = qw * x2; const wy = qw * y2; const wz = qw * z2; const m = this.data; m[0] = (1 - (yy + zz)) * sx; m[1] = (xy + wz) * sx; m[2] = (xz - wy) * sx; m[3] = 0; m[4] = (xy - wz) * sy; m[5] = (1 - (xx + zz)) * sy; m[6] = (yz + wx) * sy; m[7] = 0; m[8] = (xz + wy) * sz; m[9] = (yz - wx) * sz; m[10] = (1 - (xx + yy)) * sz; m[11] = 0; m[12] = t.x; m[13] = t.y; m[14] = t.z; m[15] = 1; return this; } transpose(src = this) { const s = src.data; const t = this.data; if (s === t) { let tmp; tmp = s[1]; t[1] = s[4]; t[4] = tmp; tmp = s[2]; t[2] = s[8]; t[8] = tmp; tmp = s[3]; t[3] = s[12]; t[12] = tmp; tmp = s[6]; t[6] = s[9]; t[9] = tmp; tmp = s[7]; t[7] = s[13]; t[13] = tmp; tmp = s[11]; t[11] = s[14]; t[14] = tmp; } else { t[0] = s[0]; t[1] = s[4]; t[2] = s[8]; t[3] = s[12]; t[4] = s[1]; t[5] = s[5]; t[6] = s[9]; t[7] = s[13]; t[8] = s[2]; t[9] = s[6]; t[10] = s[10]; t[11] = s[14]; t[12] = s[3]; t[13] = s[7]; t[14] = s[11]; t[15] = s[15]; } return this; } getTranslation(t = new Vec3()) { return t.set(this.data[12], this.data[13], this.data[14]); } getX(x = new Vec3()) { return x.set(this.data[0], this.data[1], this.data[2]); } getY(y = new Vec3()) { return y.set(this.data[4], this.data[5], this.data[6]); } getZ(z = new Vec3()) { return z.set(this.data[8], this.data[9], this.data[10]); } getScale(scale = new Vec3()) { this.getX(x); this.getY(y); this.getZ(z); scale.set(x.length(), y.length(), z.length()); return scale; } get scaleSign() { this.getX(x); this.getY(y); this.getZ(z); x.cross(x, y); return x.dot(z) < 0 ? -1 : 1; } setFromEulerAngles(ex, ey, ez) { ex *= math.DEG_TO_RAD; ey *= math.DEG_TO_RAD; ez *= math.DEG_TO_RAD; const s1 = Math.sin(-ex); const c1 = Math.cos(-ex); const s2 = Math.sin(-ey); const c2 = Math.cos(-ey); const s3 = Math.sin(-ez); const c3 = Math.cos(-ez); const m = this.data; m[0] = c2 * c3; m[1] = -c2 * s3; m[2] = s2; m[3] = 0; m[4] = c1 * s3 + c3 * s1 * s2; m[5] = c1 * c3 - s1 * s2 * s3; m[6] = -c2 * s1; m[7] = 0; m[8] = s1 * s3 - c1 * c3 * s2; m[9] = c3 * s1 + c1 * s2 * s3; m[10] = c1 * c2; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; return this; } getEulerAngles(eulers = new Vec3()) { this.getScale(scale); const sx = scale.x; const sy = scale.y; const sz = scale.z; if (sx === 0 || sy === 0 || sz === 0) { return eulers.set(0, 0, 0); } const m = this.data; const y = Math.asin(-m[2] / sx); const halfPi = Math.PI * 0.5; let x, z; if (y < halfPi) { if (y > -halfPi) { x = Math.atan2(m[6] / sy, m[10] / sz); z = Math.atan2(m[1] / sx, m[0] / sx); } else { z = 0; x = -Math.atan2(m[4] / sy, m[5] / sy); } } else { z = 0; x = Math.atan2(m[4] / sy, m[5] / sy); } return eulers.set(x, y, z).mulScalar(math.RAD_TO_DEG); } toString() { return `[${this.data.join(', ')}]`; } constructor(){ this.data = new Float32Array(16); this.data[0] = this.data[5] = this.data[10] = this.data[15] = 1; } } Mat4.IDENTITY = Object.freeze(new Mat4()); Mat4.ZERO = Object.freeze(new Mat4().set([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ])); class Quat { clone() { const cstr = this.constructor; return new cstr(this.x, this.y, this.z, this.w); } conjugate(src = this) { this.x = src.x * -1; this.y = src.y * -1; this.z = src.z * -1; this.w = src.w; return this; } copy(rhs) { this.x = rhs.x; this.y = rhs.y; this.z = rhs.z; this.w = rhs.w; return this; } dot(other) { return this.x * other.x + this.y * other.y + this.z * other.z + this.w * other.w; } equals(rhs) { return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z && this.w === rhs.w; } equalsApprox(rhs, epsilon = 1e-6) { return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon && Math.abs(this.w - rhs.w) < epsilon; } getAxisAngle(axis) { let rad = Math.acos(this.w) * 2; const s = Math.sin(rad / 2); if (s !== 0) { axis.x = this.x / s; axis.y = this.y / s; axis.z = this.z / s; if (axis.x < 0 || axis.y < 0 || axis.z < 0) { axis.x *= -1; axis.y *= -1; axis.z *= -1; rad *= -1; } } else { axis.x = 1; axis.y = 0; axis.z = 0; } return rad * math.RAD_TO_DEG; } getEulerAngles(eulers = new Vec3()) { let x, y, z; const qx = this.x; const qy = this.y; const qz = this.z; const qw = this.w; const a2 = 2 * (qw * qy - qx * qz); if (a2 <= -0.99999) { x = 2 * Math.atan2(qx, qw); y = -Math.PI / 2; z = 0; } else if (a2 >= 0.99999) { x = 2 * Math.atan2(qx, qw); y = Math.PI / 2; z = 0; } else { x = Math.atan2(2 * (qw * qx + qy * qz), 1 - 2 * (qx * qx + qy * qy)); y = Math.asin(a2); z = Math.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz)); } return eulers.set(x, y, z).mulScalar(math.RAD_TO_DEG); } invert(src = this) { return this.conjugate(src).normalize(); } length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); } lengthSq() { return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w; } lerp(lhs, rhs, alpha) { const omt = (1 - alpha) * (lhs.dot(rhs) < 0 ? -1 : 1); this.x = lhs.x * omt + rhs.x * alpha; this.y = lhs.y * omt + rhs.y * alpha; this.z = lhs.z * omt + rhs.z * alpha; this.w = lhs.w * omt + rhs.w * alpha; return this.normalize(); } mul(rhs) { const q1x = this.x; const q1y = this.y; const q1z = this.z; const q1w = this.w; const q2x = rhs.x; const q2y = rhs.y; const q2z = rhs.z; const q2w = rhs.w; this.x = q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y; this.y = q1w * q2y + q1y * q2w + q1z * q2x - q1x * q2z; this.z = q1w * q2z + q1z * q2w + q1x * q2y - q1y * q2x; this.w = q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z; return this; } mulScalar(scalar, src = this) { this.x = src.x * scalar; this.y = src.y * scalar; this.z = src.z * scalar; this.w = src.w * scalar; return this; } mul2(lhs, rhs) { const q1x = lhs.x; const q1y = lhs.y; const q1z = lhs.z; const q1w = lhs.w; const q2x = rhs.x; const q2y = rhs.y; const q2z = rhs.z; const q2w = rhs.w; this.x = q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y; this.y = q1w * q2y + q1y * q2w + q1z * q2x - q1x * q2z; this.z = q1w * q2z + q1z * q2w + q1x * q2y - q1y * q2x; this.w = q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z; return this; } normalize(src = this) { let len = src.length(); if (len === 0) { this.x = this.y = this.z = 0; this.w = 1; } else { len = 1 / len; this.x = src.x * len; this.y = src.y * len; this.z = src.z * len; this.w = src.w * len; } return this; } set(x, y, z, w) { this.x = x; this.y = y; this.z = z; this.w = w; return this; } setFromAxisAngle(axis, angle) { angle *= 0.5 * math.DEG_TO_RAD; const sa = Math.sin(angle); const ca = Math.cos(angle); this.x = sa * axis.x; this.y = sa * axis.y; this.z = sa * axis.z; this.w = ca; return this; } setFromEulerAngles(ex, ey, ez) { if (ex instanceof Vec3) { const vec = ex; ex = vec.x; ey = vec.y; ez = vec.z; } const halfToRad = 0.5 * math.DEG_TO_RAD; ex *= halfToRad; ey *= halfToRad; ez *= halfToRad; const sx = Math.sin(ex); const cx = Math.cos(ex); const sy = Math.sin(ey); const cy = Math.cos(ey); const sz = Math.sin(ez); const cz = Math.cos(ez); this.x = sx * cy * cz - cx * sy * sz; this.y = cx * sy * cz + sx * cy * sz; this.z = cx * cy * sz - sx * sy * cz; this.w = cx * cy * cz + sx * sy * sz; return this; } setFromMat4(m) { const d = m.data; let m00 = d[0]; let m01 = d[1]; let m02 = d[2]; let m10 = d[4]; let m11 = d[5]; let m12 = d[6]; let m20 = d[8]; let m21 = d[9]; let m22 = d[10]; const det = m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20); if (det < 0) { m00 = -m00; m01 = -m01; m02 = -m02; } let l; l = m00 * m00 + m01 * m01 + m02 * m02; if (l === 0) return this.set(0, 0, 0, 1); l = 1 / Math.sqrt(l); m00 *= l; m01 *= l; m02 *= l; l = m10 * m10 + m11 * m11 + m12 * m12; if (l === 0) return this.set(0, 0, 0, 1); l = 1 / Math.sqrt(l); m10 *= l; m11 *= l; m12 *= l; l = m20 * m20 + m21 * m21 + m22 * m22; if (l === 0) return this.set(0, 0, 0, 1); l = 1 / Math.sqrt(l); m20 *= l; m21 *= l; m22 *= l; if (m22 < 0) { if (m00 > m11) { this.set(1 + m00 - m11 - m22, m01 + m10, m20 + m02, m12 - m21); } else { this.set(m01 + m10, 1 - m00 + m11 - m22, m12 + m21, m20 - m02); } } else { if (m00 < -m11) { this.set(m20 + m02, m12 + m21, 1 - m00 - m11 + m22, m01 - m10); } else { this.set(m12 - m21, m20 - m02, m01 - m10, 1 + m00 + m11 + m22); } } return this.mulScalar(1.0 / this.length()); } setFromDirections(from, to) { const dotProduct = 1 + from.dot(to); if (dotProduct < Number.EPSILON) { if (Math.abs(from.x) > Math.abs(from.y)) { this.x = -from.z; this.y = 0; this.z = from.x; this.w = 0; } else { this.x = 0; this.y = -from.z; this.z = from.y; this.w = 0; } } else { this.x = from.y * to.z - from.z * to.y; this.y = from.z * to.x - from.x * to.z; this.z = from.x * to.y - from.y * to.x; this.w = dotProduct; } return this.normalize(); } slerp(lhs, rhs, alpha) { const lx = lhs.x; const ly = lhs.y; const lz = lhs.z; const lw = lhs.w; let rx = rhs.x; let ry = rhs.y; let rz = rhs.z; let rw = rhs.w; let cosHalfTheta = lw * rw + lx * rx + ly * ry + lz * rz; if (cosHalfTheta < 0) { rw = -rw; rx = -rx; ry = -ry; rz = -rz; cosHalfTheta = -cosHalfTheta; } if (Math.abs(cosHalfTheta) >= 1) { this.w = lw; this.x = lx; this.y = ly; this.z = lz; return this; } const halfTheta = Math.acos(cosHalfTheta); const sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta); if (Math.abs(sinHalfTheta) < 0.001) { this.w = lw * 0.5 + rw * 0.5; this.x = lx * 0.5 + rx * 0.5; this.y = ly * 0.5 + ry * 0.5; this.z = lz * 0.5 + rz * 0.5; return this; } const ratioA = Math.sin((1 - alpha) * halfTheta) / sinHalfTheta; const ratioB = Math.sin(alpha * halfTheta) / sinHalfTheta; this.w = lw * ratioA + rw * ratioB; this.x = lx * ratioA + rx * ratioB; this.y = ly * ratioA + ry * ratioB; this.z = lz * ratioA + rz * ratioB; return this; } transformVector(vec, res = new Vec3()) { const x = vec.x, y = vec.y, z = vec.z; const qx = this.x, qy = this.y, qz = this.z, qw = this.w; 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; res.x = ix * qw + iw * -qx + iy * -qz - iz * -qy; res.y = iy * qw + iw * -qy + iz * -qx - ix * -qz; res.z = iz * qw + iw * -qz + ix * -qy - iy * -qx; return res; } fromArray(arr, offset = 0) { this.x = arr[offset] ?? this.x; this.y = arr[offset + 1] ?? this.y; this.z = arr[offset + 2] ?? this.z; this.w = arr[offset + 3] ?? this.w; return this; } toString() { return `[${this.x}, ${this.y}, ${this.z}, ${this.w}]`; } toArray(arr = [], offset = 0) { arr[offset] = this.x; arr[offset + 1] = this.y; arr[offset + 2] = this.z; arr[offset + 3] = this.w; return arr; } constructor(x = 0, y = 0, z = 0, w = 1){ if (x.length === 4) { this.x = x[0]; this.y = x[1]; this.z = x[2]; this.w = x[3]; } else { this.x = x; this.y = y; this.z = z; this.w = w; } } } Quat.IDENTITY = Object.freeze(new Quat(0, 0, 0, 1)); Quat.ZERO = Object.freeze(new Quat(0, 0, 0, 0)); const tmpVecA$1 = new Vec3(); const tmpVecB$1 = new Vec3(); const tmpVecC = new Vec3(); const tmpVecD = new Vec3(); const tmpVecE = new Vec3(); class BoundingBox { add(other) { const tc = this.center; const tcx = tc.x; const tcy = tc.y; const tcz = tc.z; const th = this.halfExtents; const thx = th.x; const thy = th.y; const thz = th.z; let tminx = tcx - thx; let tmaxx = tcx + thx; let tminy = tcy - thy; let tmaxy = tcy + thy; let tminz = tcz - thz; let tmaxz = tcz + thz; const oc = other.center; const ocx = oc.x; const ocy = oc.y; const ocz = oc.z; const oh = other.halfExtents; const ohx = oh.x; const ohy = oh.y; const ohz = oh.z; const ominx = ocx - ohx; const omaxx = ocx + ohx; const ominy = ocy - ohy; const omaxy = ocy + ohy; const ominz = ocz - ohz; const omaxz = ocz + ohz; if (ominx < tminx) tminx = ominx; if (omaxx > tmaxx) tmaxx = omaxx; if (ominy < tminy) tminy = ominy; if (omaxy > tmaxy) tmaxy = omaxy; if (ominz < tminz) tminz = ominz; if (omaxz > tmaxz) tmaxz = omaxz; tc.x = (tminx + tmaxx) * 0.5; tc.y = (tminy + tmaxy) * 0.5; tc.z = (tminz + tmaxz) * 0.5; th.x = (tmaxx - tminx) * 0.5; th.y = (tmaxy - tminy) * 0.5; th.z = (tmaxz - tminz) * 0.5; } copy(src) { this.center.copy(src.center); this.halfExtents.copy(src.halfExtents); } clone() { return new BoundingBox(this.center, this.halfExtents); } intersects(other) { const aMax = this.getMax(); const aMin = this.getMin(); const bMax = other.getMax(); const bMin = other.getMin(); return aMin.x <= bMax.x && aMax.x >= bMin.x && aMin.y <= bMax.y && aMax.y >= bMin.y && aMin.z <= bMax.z && aMax.z >= bMin.z; } _intersectsRay(ray, point) { const tMin = tmpVecA$1.copy(this.getMin()).sub(ray.origin); const tMax = tmpVecB$1.copy(this.getMax()).sub(ray.origin); const dir = ray.direction; if (dir.x === 0) { tMin.x = tMin.x < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE; tMax.x = tMax.x < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE; } else { tMin.x /= dir.x; tMax.x /= dir.x; } if (dir.y === 0) { tMin.y = tMin.y < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE; tMax.y = tMax.y < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE; } else { tMin.y /= dir.y; tMax.y /= dir.y; } if (dir.z === 0) { tMin.z = tMin.z < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE; tMax.z = tMax.z < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE; } else { tMin.z /= dir.z; tMax.z /= dir.z; } const realMin = tmpVecC.set(Math.min(tMin.x, tMax.x), Math.min(tMin.y, tMax.y), Math.min(tMin.z, tMax.z)); const realMax = tmpVecD.set(Math.max(tMin.x, tMax.x), Math.max(tMin.y, tMax.y), Math.max(tMin.z, tMax.z)); const minMax = Math.min(Math.min(realMax.x, realMax.y), realMax.z); const maxMin = Math.max(Math.max(realMin.x, realMin.y), realMin.z); const intersects = minMax >= maxMin && maxMin >= 0; if (intersects) { point.copy(ray.direction).mulScalar(maxMin).add(ray.origin); } return intersects; } _fastIntersectsRay(ray) { const diff = tmpVecA$1; const cross = tmpVecB$1; const prod = tmpVecC; const absDiff = tmpVecD; const absDir = tmpVecE; const rayDir = ray.direction; diff.sub2(ray.origin, this.center); absDiff.set(Math.abs(diff.x), Math.abs(diff.y), Math.abs(diff.z)); prod.mul2(diff, rayDir); if (absDiff.x > this.halfExtents.x && prod.x >= 0) { return false; } if (absDiff.y > this.halfExtents.y && prod.y >= 0) { return false; } if (absDiff.z > this.halfExtents.z && prod.z >= 0) { return false; } absDir.set(Math.abs(rayDir.x), Math.abs(rayDir.y), Math.abs(rayDir.z)); cross.cross(rayDir, diff); cross.set(Math.abs(cross.x), Math.abs(cross.y), Math.abs(cross.z)); if (cross.x > this.halfExtents.y * absDir.z + this.halfExtents.z * absDir.y) { return false; } if (cross.y > this.halfExtents.x * absDir.z + this.halfExtents.z * absDir.x) { return false; } if (cross.z > this.halfExtents.x * absDir.y + this.halfExtents.y * absDir.x) { return false; } return true; } intersectsRay(ray, point) { if (point) { return this._intersectsRay(ray, point); } return this._fastIntersectsRay(ray); } setMinMax(min, max) { this.center.add2(max, min).mulScalar(0.5); this.halfExtents.sub2(max, min).mulScalar(0.5); } getMin() { return this._min.copy(this.center).sub(this.halfExtents); } getMax() { return this._max.copy(this.center).add(this.halfExtents); } containsPoint(point) { const c = this.center; const h = this.halfExtents; if (point.x < c.x - h.x || point.x > c.x + h.x || point.y < c.y - h.y || point.y > c.y + h.y || point.z < c.z - h.z || point.z > c.z + h.z) { return false; } return true; } closestPoint(point, result = new Vec3()) { const c = this.center; const h = this.halfExtents; return result.set(Math.max(c.x - h.x, Math.min(point.x, c.x + h.x)), Math.max(c.y - h.y, Math.min(point.y, c.y + h.y)), Math.max(c.z - h.z, Math.min(point.z, c.z + h.z))); } setFromTransformedAabb(aabb, m, ignoreScale = false) { const ac = aabb.center; const ar = aabb.halfExtents; const d = m.data; let mx0 = d[0]; let mx1 = d[4]; let mx2 = d[8]; let my0 = d[1]; let my1 = d[5]; let my2 = d[9]; let mz0 = d[2]; let mz1 = d[6]; let mz2 = d[10]; if (ignoreScale) { let lengthSq = mx0 * mx0 + mx1 * mx1 + mx2 * mx2; if (lengthSq > 0) { const invLength = 1 / Math.sqrt(lengthSq); mx0 *= invLength; mx1 *= invLength; mx2 *= invLength; } lengthSq = my0 * my0 + my1 * my1 + my2 * my2; if (lengthSq > 0) { const invLength = 1 / Math.sqrt(lengthSq); my0 *= invLength; my1 *= invLength; my2 *= invLength; } lengthSq = mz0 * mz0 + mz1 * mz1 + mz2 * mz2; if (lengthSq > 0) { const invLength = 1 / Math.sqrt(lengthSq); mz0 *= invLength; mz1 *= invLength; mz2 *= invLength; } } this.center.set(d[12] + mx0 * ac.x + mx1 * ac.y + mx2 * ac.z, d[13] + my0 * ac.x + my1 * ac.y + my2 * ac.z, d[14] + mz0 * ac.x + mz1 * ac.y + mz2 * ac.z); this.halfExtents.set(Math.abs(mx0) * ar.x + Math.abs(mx1) * ar.y + Math.abs(mx2) * ar.z, Math.abs(my0) * ar.x + Math.abs(my1) * ar.y + Math.abs(my2) * ar.z, Math.abs(mz0) * ar.x + Math.abs(mz1) * ar.y + Math.abs(mz2) * ar.z); } static computeMinMax(vertices, min, max, numVerts = vertices.length / 3) { if (numVerts > 0) { let minx = vertices[0]; let miny = vertices[1]; let minz = vertices[2]; let maxx = minx; let maxy = miny; let maxz = minz; const n = numVerts * 3; for(let i = 3; i < n; i += 3){ const x = vertices[i]; const y = vertices[i + 1]; const z = vertices[i + 2]; if (x < minx) minx = x; if (y < miny) miny = y; if (z < minz) minz = z; if (x > maxx) maxx = x; if (y > maxy) maxy = y; if (z > maxz) maxz = z; } min.set(minx, miny, minz); max.set(maxx, maxy, maxz); } } compute(vertices, numVerts) { BoundingBox.computeMinMax(vertices, tmpVecA$1, tmpVecB$1, numVerts); this.setMinMax(tmpVecA$1, tmpVecB$1); } intersectsBoundingSphere(sphere) { const sq = this._distanceToBoundingSphereSq(sphere); if (sq <= sphere.radius * sphere.radius) { return true; } return false; } _distanceToBoundingSphereSq(sphere) { const boxMin = this.getMin(); const boxMax = this.getMax(); let sq = 0; const axis = [ 'x', 'y', 'z' ]; for(let i = 0; i < 3; ++i){ let out = 0; const pn = sphere.center[axis[i]]; const bMin = boxMin[axis[i]]; const bMax = boxMax[axis[i]]; let val = 0; if (pn < bMin) { val = bMin - pn; out += val * val; } if (pn > bMax) { val = pn - bMax; out += val * val; } sq += out; } return sq; } _expand(expandMin, expandMax) { tmpVecA$1.add2(this.getMin(), expandMin); tmpVecB$1.add2(this.getMax(), expandMax); this.setMinMax(tmpVecA$1, tmpVecB$1); } constructor(center, halfExtents){ this.center = new Vec3(); this.halfExtents = new Vec3(0.5, 0.5, 0.5); this._min = new Vec3(); this._max = new Vec3(); if (center) { this.center.copy(center); } if (halfExtents) { this.halfExtents.copy(halfExtents); } } } const tmpVecA = new Vec3(); const tmpVecB = new Vec3(); class BoundingSphere { containsPoint(point) { const lenSq = tmpVecA.sub2(point, this.center).lengthSq(); const r = this.radius; return lenSq < r * r; } intersectsRay(ray, point) { const m = tmpVecA.copy(ray.origin).sub(this.center); const b = m.dot(tmpVecB.copy(ray.direction).normalize()); const c = m.dot(m) - this.radius * this.radius; if (c > 0 && b > 0) { return false; } const discr = b * b - c; if (discr < 0) { return false; } const t = Math.abs(-b - Math.sqrt(discr)); if (point) { point.copy(ray.direction).mulScalar(t).add(ray.origin); } return true; } intersectsBoundingSphere(sphere) { tmpVecA.sub2(sphere.center, this.center); const totalRadius = sphere.radius + this.radius; if (tmpVecA.lengthSq() <= totalRadius * totalRadius) { return true; } return false; } constructor(center = new Vec3(), radius = 0.5){ this.center = center; this.radius = radius; } } class Plane { clone() { const cstr = this.constructor; return new cstr().copy(this); } copy(src) { this.normal.copy(src.normal); this.distance = src.distance; return this; } intersectsLine(start, end, point) { const d = this.distance; const d0 = this.normal.dot(start) + d; const d1 = this.normal.dot(end) + d; const t = d0 / (d0 - d1); const intersects = t >= 0 && t <= 1; if (intersects && point) { point.lerp(start, end, t); } return intersects; } intersectsRay(ray, point) { const denominator = this.normal.dot(ray.direction); if (denominator === 0) { return false; } const t = -(this.normal.dot(ray.origin) + this.distance) / denominator; if (t >= 0 && point) { point.copy(ray.direction).mulScalar(t).add(ray.origin); } return t >= 0; } normalize() { const invLength = 1 / this.normal.length(); this.normal.mulScalar(invLength); this.distance *= invLength; return this; } set(nx, ny, nz, d) { this.normal.set(nx, ny, nz); this.distance = d; return this; } setFromPointNormal(point, normal) { this.normal.copy(normal); this.distance = -this.normal.dot(point); return this; } constructor(normal = Vec3.UP, distance = 0){ this.normal = new Vec3(); this.normal.copy(normal); this.distance = distance; } } class Frustum { clone() { const cstr = this.constructor; return new cstr().copy(this); } copy(src) { for(let i = 0; i < 6; i++){ this.planes[i].copy(src.planes[i]); } return this; } setFromMat4(matrix) { const [m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33] = matrix.data; const planes = this.planes; planes[0].set(m03 - m00, m13 - m10, m23 - m20, m33 - m30).normalize(); planes[1].set(m03 + m00, m13 + m10, m23 + m20, m33 + m30).normalize(); planes[2].set(m03 + m01, m13 + m11, m23 + m21, m33 + m31).normalize(); planes[3].set(m03 - m01, m13 - m11, m23 - m21, m33 - m31).normalize(); planes[4].set(m03 - m02, m13 - m12, m23 - m22, m33 - m32).normalize(); planes[5].set(m03 + m02, m13 + m12, m23 + m22, m33 + m32).normalize(); } containsPoint(point) { for(let p = 0; p < 6; p++){ const { normal, distance } = this.planes[p]; if (normal.dot(point) + distance <= 0) { return false; } } return true; } containsSphere(sphere) { const { center, radius } = sphere; let c = 0; for(let p = 0; p < 6; p++){ const { normal, distance } = this.planes[p]; const d = normal.dot(center) + distance; if (d <= -radius) { return 0; } if (d > radius) { c++; } } return c === 6 ? 2 : 1; } constructor(){ this.planes = []; for(let i = 0; i < 6; i++){ this.planes[i] = new Plane(); } } } class Ray { set(origin, direction) { this.origin.copy(origin); this.direction.copy(direction); return this; } copy(src) { return this.set(src.origin, src.direction); } clone() { return new this.constructor(this.origin, this.direction); } constructor(origin, direction){ this.origin = new Vec3(); this.direction = Vec3.FORWARD.clone(); if (origin) { this.origin.copy(origin); } if (direction) { this.direction.copy(direction); } } } const ADDRESS_REPEAT = 0; const ADDRESS_CLAMP_TO_EDGE = 1; const ADDRESS_MIRRORED_REPEAT = 2; const BLENDMODE_ZERO = 0; const BLENDMODE_ONE = 1; const BLENDMODE_SRC_COLOR = 2; const BLENDMODE_DST_COLOR = 4; const BLENDMODE_ONE_MINUS_DST_COLOR = 5; const BLENDMODE_SRC_ALPHA = 6; const BLENDMODE_ONE_MINUS_SRC_ALPHA = 8; const BLENDEQUATION_ADD = 0; const BLENDEQUATION_REVERSE_SUBTRACT = 2; const BLENDEQUATION_MIN = 3; const BLENDEQUATION_MAX = 4; const BUFFERUSAGE_READ = 0x0001; const BUFFERUSAGE_COPY_SRC = 0x0004; const BUFFERUSAGE_COPY_DST = 0x0008; const BUFFERUSAGE_INDEX = 0x0010; const BUFFERUSAGE_VERTEX = 0x0020; const BUFFERUSAGE_UNIFORM = 0x0040; const BUFFERUSAGE_STORAGE = 0x0080; const BUFFERUSAGE_INDIRECT = 0x0100; const BUFFER_STATIC = 0; const BUFFER_DYNAMIC = 1; const BUFFER_STREAM = 2; const BUFFER_GPUDYNAMIC = 3; const CLEARFLAG_COLOR = 1; const CLEARFLAG_DEPTH = 2; const CLEARFLAG_STENCIL = 4; const CULLFACE_NONE = 0; const CULLFACE_BACK = 1; const CULLFACE_FRONT = 2; const CULLFACE_FRONTANDBACK = 3; const FILTER_NEAREST = 0; const FILTER_LINEAR = 1; const FILTER_NEAREST_MIPMAP_NEAREST = 2; const FILTER_NEAREST_MIPMAP_LINEAR = 3; const FILTER_LINEAR_MIPMAP_NEAREST = 4; const FILTER_LINEAR_MIPMAP_LINEAR = 5; const FUNC_NEVER = 0; const FUNC_LESS = 1; const FUNC_EQUAL = 2; const FUNC_LESSEQUAL = 3; const FUNC_GREATER = 4; const FUNC_NOTEQUAL = 5; const FUNC_GREATEREQUAL = 6; const FUNC_ALWAYS = 7; const INDEXFORMAT_UINT8 = 0; const INDEXFORMAT_UINT16 = 1; const INDEXFORMAT_UINT32 = 2; const indexFormatByteSize = [ 1, 2, 4 ]; const PIXELFORMAT_A8 = 0; const PIXELFORMAT_L8 = 1; const PIXELFORMAT_LA8 = 2; const PIXELFORMAT_RGB565 = 3; const PIXELFORMAT_RGBA5551 = 4; const PIXELFORMAT_RGBA4 = 5; const PIXELFORMAT_RGB8 = 6; const PIXELFORMAT_RGBA8 = 7; const PIXELFORMAT_DXT1 = 8; const PIXELFORMAT_DXT3 = 9; const PIXELFORMAT_DXT5 = 10; const PIXELFORMAT_RGB16F = 11; const PIXELFORMAT_RGBA16F = 12; const PIXELFORMAT_RGB32F = 13; const PIXELFORMAT_RGBA32F = 14; const PIXELFORMAT_R32F = 15; const PIXELFORMAT_DEPTH = 16; const PIXELFORMAT_DEPTHSTENCIL = 17; const PIXELFORMAT_111110F = 18; const PIXELFORMAT_SRGB8 = 19; const PIXELFORMAT_SRGBA8 = 20; const PIXELFORMAT_ETC1 = 21; const PIXELFORMAT_ETC2_RGB = 22; const PIXELFORMAT_ETC2_RGBA = 23; const PIXELFORMAT_PVRTC_2BPP_RGB_1 = 24; const PIXELFORMAT_PVRTC_2BPP_RGBA_1 = 25; const PIXELFORMAT_PVRTC_4BPP_RGB_1 = 26; const PIXELFORMAT_PVRTC_4BPP_RGBA_1 = 27; const PIXELFORMAT_ASTC_4x4 = 28; const PIXELFORMAT_ATC_RGB = 29; const PIXELFORMAT_ATC_RGBA = 30; const PIXELFORMAT_BGRA8 = 31; const PIXELFORMAT_R8I = 32; const PIXELFORMAT_R8U = 33; const PIXELFORMAT_R16I = 34; const PIXELFORMAT_R16U = 35; const PIXELFORMAT_R32I = 36; const PIXELFORMAT_R32U = 37; const PIXELFORMAT_RG8I = 38; const PIXELFORMAT_RG8U = 39; const PIXELFORMAT_RG16I = 40; const PIXELFORMAT_RG16U = 41; const PIXELFORMAT_RG32I = 42; const PIXELFORMAT_RG32U = 43; const PIXELFORMAT_RGBA8I = 44; const PIXELFORMAT_RGBA8U = 45; const PIXELFORMAT_RGBA16I = 46; const PIXELFORMAT_RGBA16U = 47; const PIXELFORMAT_RGBA32I = 48; const PIXELFORMAT_RGBA32U = 49; const PIXELFORMAT_R16F = 50; const PIXELFORMAT_RG16F = 51; const PIXELFORMAT_R8 = 52; const PIXELFORMAT_RG8 = 53; const PIXELFORMAT_DXT1_SRGB = 54; const PIXELFORMAT_DXT3_SRGBA = 55; const PIXELFORMAT_DXT5_SRGBA = 56; const PIXELFORMAT_ETC2_SRGB = 61; const PIXELFORMAT_ETC2_SRGBA = 62; const PIXELFORMAT_ASTC_4x4_SRGB = 63; const PIXELFORMAT_SBGRA8 = 64; const PIXELFORMAT_BC6F = 65; const PIXELFORMAT_BC6UF = 66; const PIXELFORMAT_BC7 = 67; const PIXELFORMAT_BC7_SRGBA = 68; const PIXELFORMAT_DEPTH16 = 69; const PIXELFORMAT_RG32F = 70; const PIXELFORMAT_RGB9E5 = 71; const PIXELFORMAT_RG8S = 72; const PIXELFORMAT_RGBA8S = 73; const PIXELFORMAT_RGB10A2 = 74; const PIXELFORMAT_RGB10A2U = 75; const pixelFormatInfo = new Map([ [ PIXELFORMAT_A8, { name: 'A8', size: 1, ldr: true } ], [ PIXELFORMAT_R8, { name: 'R8', size: 1, ldr: true } ], [ PIXELFORMAT_L8, { name: 'L8', size: 1, ldr: true } ], [ PIXELFORMAT_LA8, { name: 'LA8', size: 2, ldr: true } ], [ PIXELFORMAT_RG8, { name: 'RG8', size: 2, ldr: true } ], [ PIXELFORMAT_RGB565, { name: 'RGB565', size: 2, ldr: true } ], [ PIXELFORMAT_RGBA5551, { name: 'RGBA5551', size: 2, ldr: true } ], [ PIXELFORMAT_RGBA4, { name: 'RGBA4', size: 2, ldr: true } ], [ PIXELFORMAT_RGB8, { name: 'RGB8', size: 4, ldr: true } ], [ PIXELFORMAT_RGBA8, { name: 'RGBA8', size: 4, ldr: true, srgbFormat: PIXELFORMAT_SRGBA8 } ], [ PIXELFORMAT_R16F, { name: 'R16F', size: 2 } ], [ PIXELFORMAT_RG16F, { name: 'RG16F', size: 4 } ], [ PIXELFORMAT_RGB16F, { name: 'RGB16F', size: 8 } ], [ PIXELFORMAT_RGBA16F, { name: 'RGBA16F', size: 8 } ], [ PIXELFORMAT_RGB32F, { name: 'RGB32F', size: 16 } ], [ PIXELFORMAT_RGBA32F, { name: 'RGBA32F', size: 16 } ], [ PIXELFORMAT_R32F, { name: 'R32F', size: 4 } ], [ PIXELFORMAT_RG32F, { name: 'RG32F', size: 8 } ], [ PIXELFORMAT_RGB9E5, { name: 'RGB9E5', size: 4 } ], [ PIXELFORMAT_RG8S, { name: 'RG8S', size: 2 } ], [ PIXELFORMAT_RGBA8S, { name: 'RGBA8S', size: 4 } ], [ PIXELFORMAT_RGB10A2, { name: 'RGB10A2', size: 4 } ], [ PIXELFORMAT_RGB10A2U, { name: 'RGB10A2U', size: 4, isInt: true } ], [ PIXELFORMAT_DEPTH, { name: 'DEPTH', size: 4 } ], [ PIXELFORMAT_DEPTH16, { name: 'DEPTH16', size: 2 } ], [ PIXELFORMAT_DEPTHSTENCIL, { name: 'DEPTHSTENCIL', size: 4 } ], [ PIXELFORMAT_111110F, { name: '111110F', size: 4 } ], [ PIXELFORMAT_SRGB8, { name: 'SRGB8', size: 4, ldr: true, srgb: true } ], [ PIXELFORMAT_SRGBA8, { name: 'SRGBA8', size: 4, ldr: true, srgb: true } ], [ PIXELFORMAT_BGRA8, { name: 'BGRA8', size: 4, ldr: true } ], [ PIXELFORMAT_SBGRA8, { name: 'SBGRA8', size: 4, ldr: true, srgb: true } ], [ PIXELFORMAT_DXT1, { name: 'DXT1', blockSize: 8, ldr: true, srgbFormat: PIXELFORMAT_DXT1_SRGB } ], [ PIXELFORMAT_DXT3, { name: 'DXT3', blockSize: 16, ldr: true, srgbFormat: PIXELFORMAT_DXT3_SRGBA } ], [ PIXELFORMAT_DXT5, { name: 'DXT5', blockSize: 16, ldr: true, srgbFormat: PIXELFORMAT_DXT5_SRGBA } ], [ PIXELFORMAT_ETC1, { name: 'ETC1', blockSize: 8, ldr: true } ], [ PIXELFORMAT_ETC2_RGB, { name: 'ETC2_RGB', blockSize: 8, ldr: true, srgbFormat: PIXELFORMAT_ETC2_SRGB } ], [ PIXELFORMAT_ETC2_RGBA, { name: 'ETC2_RGBA', blockSize: 16, ldr: true, srgbFormat: PIXELFORMAT_ETC2_SRGBA } ], [ PIXELFORMAT_PVRTC_2BPP_RGB_1, { name: 'PVRTC_2BPP_RGB_1', ldr: true, blockSize: 8 } ], [ PIXELFORMAT_PVRTC_2BPP_RGBA_1, { name: 'PVRTC_2BPP_RGBA_1', ldr: true, blockSize: 8 } ], [ PIXELFORMAT_PVRTC_4BPP_RGB_1, { name: 'PVRTC_4BPP_RGB_1', ldr: true, blockSize: 8 } ], [ PIXELFORMAT_PVRTC_4BPP_RGBA_1, { name: 'PVRTC_4BPP_RGBA_1', ldr: true, blockSize: 8 } ], [ PIXELFORMAT_ASTC_4x4, { name: 'ASTC_4x4', blockSize: 16, ldr: true, srgbFormat: PIXELFORMAT_ASTC_4x4_SRGB } ], [ PIXELFORMAT_ATC_RGB, { name: 'ATC_RGB', blockSize: 8, ldr: true } ], [ PIXELFORMAT_ATC_RGBA, { name: 'ATC_RGBA', blockSize: 16, ldr: true } ], [ PIXELFORMAT_BC6F, { name: 'BC6H_RGBF', blockSize: 16 } ], [ PIXELFORMAT_BC6UF, { name: 'BC6H_RGBUF', blockSize: 16 } ], [ PIXELFORMAT_BC7, { name: 'BC7_RGBA', blockSize: 16, ldr: true, srgbFormat: PIXELFORMAT_BC7_SRGBA } ], [ PIXELFORMAT_DXT1_SRGB, { name: 'DXT1_SRGB', blockSize: 8, ldr: true, srgb: true } ], [ PIXELFORMAT_DXT3_SRGBA, { name: 'DXT3_SRGBA', blockSize: 16, ldr: true, srgb: true } ], [ PIXELFORMAT_DXT5_SRGBA, { name: 'DXT5_SRGBA', blockSize: 16, ldr: true, srgb: true } ], [ PIXELFORMAT_ETC2_SRGB, { name: 'ETC2_SRGB', blockSize: 8, ldr: true, srgb: true } ], [ PIXELFORMAT_ETC2_SRGBA, { name: 'ETC2_SRGBA', blockSize: 16, ldr: true, srgb: true } ], [ PIXELFORMAT_ASTC_4x4_SRGB, { name: 'ASTC_4x4_SRGB', blockSize: 16, ldr: true, srgb: true } ], [ PIXELFORMAT_BC7_SRGBA, { name: 'BC7_SRGBA', blockSize: 16, ldr: true, srgb: true } ], [ PIXELFORMAT_R8I, { name: 'R8I', size: 1, isInt: true } ], [ PIXELFORMAT_R8U, { name: 'R8U', size: 1, isInt: true } ], [ PIXELFORMAT_R16I, { name: 'R16I', size: 2, isInt: true } ], [ PIXELFORMAT_R16U, { name: 'R16U', size: 2, isInt: true } ], [ PIXELFORMAT_R32I, { name: 'R32I', size: 4, isInt: true } ], [ PIXELFORMAT_R32U, { name: 'R32U', size: 4, isInt: true } ], [ PIXELFORMAT_RG8I, { name: 'RG8I', size: 2, isInt: true } ], [ PIXELFORMAT_RG8U, { name: 'RG8U', size: 2, isInt: true } ], [ PIXELFORMAT_RG16I, { name: 'RG16I', size: 4, isInt: true } ], [ PIXELFORMAT_RG16U, { name: 'RG16U', size: 4, isInt: true } ], [ PIXELFORMAT_RG32I, { name: 'RG32I', size: 8, isInt: true } ], [ PIXELFORMAT_RG32U, { name: 'RG32U', size: 8, isInt: true } ], [ PIXELFORMAT_RGBA8I, { name: 'RGBA8I', size: 4, isInt: true } ], [ PIXELFORMAT_RGBA8U, { name: 'RGBA8U', size: 4, isInt: true } ], [ PIXELFORMAT_RGBA16I, { name: 'RGBA16I', size: 8, isInt: true } ], [ PIXELFORMAT_RGBA16U, { name: 'RGBA16U', size: 8, isInt: true } ], [ PIXELFORMAT_RGBA32I, { name: 'RGBA32I', size: 16, isInt: true } ], [ PIXELFORMAT_RGBA32U, { name: 'RGBA32U', size: 16, isInt: true } ] ]); const isCompressedPixelFormat = (format)=>{ return pixelFormatInfo.get(format)?.blockSize !== undefined; }; const isSrgbPixelFormat = (format)=>{ return pixelFormatInfo.get(format)?.srgb === true; }; const isIntegerPixelFormat = (format)=>{ return pixelFormatInfo.get(format)?.isInt === true; }; const pixelFormatLinearToGamma = (format)=>{ return pixelFormatInfo.get(format)?.srgbFormat || format; }; const pixelFormatGammaToLinear = (format)=>{ for (const [key, value] of pixelFormatInfo){ if (value.srgbFormat === format) { return key; } } return format; }; const requiresManualGamma = (format)=>{ const info = pixelFormatInfo.get(format); return !!(info?.ldr && !info?.srgb); }; const getPixelFormatArrayType = (format)=>{ switch(format){ case PIXELFORMAT_R32F: case PIXELFORMAT_RG32F: case PIXELFORMAT_RGB32F: case PIXELFORMAT_RGBA32F: return Float32Array; case PIXELFORMAT_R32I: case PIXELFORMAT_RG32I: case PIXELFORMAT_RGBA32I: return Int32Array; case PIXELFORMAT_R32U: case PIXELFORMAT_RG32U: case PIXELFORMAT_RGBA32U: case PIXELFORMAT_RGB9E5: case PIXELFORMAT_RGB10A2: case PIXELFORMAT_RGB10A2U: return Uint32Array; case PIXELFORMAT_R16I: case PIXELFORMAT_RG16I: case PIXELFORMAT_RGBA16I: return Int16Array; case PIXELFORMAT_RG8: case PIXELFORMAT_R16U: case PIXELFORMAT_RG16U: case PIXELFORMAT_RGBA16U: case PIXELFORMAT_RGB565: case PIXELFORMAT_RGBA5551: case PIXELFORMAT_RGBA4: case PIXELFORMAT_R16F: case PIXELFORMAT_RG16F: case PIXELFORMAT_RGB16F: case PIXELFORMAT_RGBA16F: return Uint16Array; case PIXELFORMAT_R8I: case PIXELFORMAT_RG8I: case PIXELFORMAT_RGBA8I: case PIXELFORMAT_RG8S: case PIXELFORMAT_RGBA8S: return Int8Array; default: return Uint8Array; } }; const PRIMITIVE_POINTS = 0; const PRIMITIVE_LINES = 1; const PRIMITIVE_LINELOOP = 2; const PRIMITIVE_LINESTRIP = 3; const PRIMITIVE_TRIANGLES = 4; const PRIMITIVE_TRISTRIP = 5; const PRIMITIVE_TRIFAN = 6; const SEMANTIC_POSITION = 'POSITION'; const SEMANTIC_NORMAL = 'NORMAL'; const SEMANTIC_TANGENT = 'TANGENT'; const SEMANTIC_BLENDWEIGHT = 'BLENDWEIGHT'; const SEMANTIC_BLENDINDICES = 'BLENDINDICES'; const SEMANTIC_COLOR = 'COLOR'; const SEMANTIC_TEXCOORD = 'TEXCOORD'; const SEMANTIC_TEXCOORD0 = 'TEXCOORD0'; const SEMANTIC_TEXCOORD1 = 'TEXCOORD1'; const SEMANTIC_TEXCOORD2 = 'TEXCOORD2'; const SEMANTIC_TEXCOORD3 = 'TEXCOORD3'; const SEMANTIC_TEXCOORD4 = 'TEXCOORD4'; const SEMANTIC_TEXCOORD5 = 'TEXCOORD5'; const SEMANTIC_TEXCOORD6 = 'TEXCOORD6'; const SEMANTIC_TEXCOORD7 = 'TEXCOORD7'; const SEMANTIC_ATTR0 = 'ATTR0'; const SEMANTIC_ATTR1 = 'ATTR1'; const SEMANTIC_ATTR2 = 'ATTR2'; const SEMANTIC_ATTR3 = 'ATTR3'; const SEMANTIC_ATTR4 = 'ATTR4'; const SEMANTIC_ATTR5 = 'ATTR5'; const SEMANTIC_ATTR6 = 'ATTR6'; const SEMANTIC_ATTR7 = 'ATTR7'; const SEMANTIC_ATTR8 = 'ATTR8'; const SEMANTIC_ATTR9 = 'ATTR9'; const SEMANTIC_ATTR10 = 'ATTR10'; const SEMANTIC_ATTR11 = 'ATTR11'; const SEMANTIC_ATTR12 = 'ATTR12'; const SEMANTIC_ATTR13 = 'ATTR13'; const SEMANTIC_ATTR14 = 'ATTR14'; const SEMANTIC_ATTR15 = 'ATTR15'; const SHADERTAG_MATERIAL = 1; const STENCILOP_KEEP = 0; const STENCILOP_REPLACE = 2; const STENCILOP_INCREMENT = 3; const STENCILOP_DECREMENT = 5; const TEXTURELOCK_NONE = 0; const TEXTURELOCK_READ = 1; const TEXTURELOCK_WRITE = 2; const TEXTURETYPE_DEFAULT = 'default'; const TEXTURETYPE_RGBM = 'rgbm'; const TEXTURETYPE_RGBE = 'rgbe'; const TEXTURETYPE_RGBP = 'rgbp'; const TEXTURETYPE_SWIZZLEGGGR = 'swizzleGGGR'; const TEXTUREDIMENSION_1D = '1d'; const TEXTUREDIMENSION_2D = '2d'; const TEXTUREDIMENSION_2D_ARRAY = '2d-array'; const TEXTUREDIMENSION_CUBE = 'cube'; const TEXTUREDIMENSION_CUBE_ARRAY = 'cube-array'; const TEXTUREDIMENSION_3D = '3d'; const SAMPLETYPE_FLOAT = 0; const SAMPLETYPE_UNFILTERABLE_FLOAT = 1; const SAMPLETYPE_DEPTH = 2; const SAMPLETYPE_INT = 3; const SAMPLETYPE_UINT = 4; const TEXTUREPROJECTION_NONE = 'none'; const TEXTUREPROJECTION_CUBE = 'cube'; const TEXTUREPROJECTION_EQUIRECT = 'equirect'; const TEXTUREPROJECTION_OCTAHEDRAL = 'octahedral'; const SHADERLANGUAGE_GLSL = 'glsl'; const SHADERLANGUAGE_WGSL = 'wgsl'; const TYPE_INT8 = 0; const TYPE_UINT8 = 1; const TYPE_INT16 = 2; const TYPE_UINT16 = 3; const TYPE_INT32 = 4; const TYPE_UINT32 = 5; const TYPE_FLOAT32 = 6; const TYPE_FLOAT16 = 7; const UNIFORMTYPE_BOOL = 0; const UNIFORMTYPE_INT = 1; const UNIFORMTYPE_FLOAT = 2; const UNIFORMTYPE_VEC2 = 3; const UNIFORMTYPE_VEC3 = 4; const UNIFORMTYPE_VEC4 = 5; const UNIFORMTYPE_IVEC2 = 6; const UNIFORMTYPE_IVEC3 = 7; const UNIFORMTYPE_IVEC4 = 8; const UNIFORMTYPE_BVEC2 = 9; const UNIFORMTYPE_BVEC3 = 10; const UNIFORMTYPE_BVEC4 = 11; const UNIFORMTYPE_MAT2 = 12; const UNIFORMTYPE_MAT3 = 13; const UNIFORMTYPE_MAT4 = 14; const UNIFORMTYPE_TEXTURE2D = 15; const UNIFORMTYPE_TEXTURECUBE = 16; const UNIFORMTYPE_FLOATARRAY = 17; const UNIFORMTYPE_TEXTURE2D_SHADOW = 18; const UNIFORMTYPE_TEXTURECUBE_SHADOW = 19; const UNIFORMTYPE_TEXTURE3D = 20; const UNIFORMTYPE_VEC2ARRAY = 21; const UNIFORMTYPE_VEC3ARRAY = 22; const UNIFORMTYPE_VEC4ARRAY = 23; const UNIFORMTYPE_MAT4ARRAY = 24; const UNIFORMTYPE_TEXTURE2D_ARRAY = 25; const UNIFORMTYPE_UINT = 26; const UNIFORMTYPE_UVEC2 = 27; const UNIFORMTYPE_UVEC3 = 28; const UNIFORMTYPE_UVEC4 = 29; const UNIFORMTYPE_INTARRAY = 30; const UNIFORMTYPE_UINTARRAY = 31; const UNIFORMTYPE_BOOLARRAY = 32; const UNIFORMTYPE_IVEC2ARRAY = 33; const UNIFORMTYPE_UVEC2ARRAY = 34; const UNIFORMTYPE_BVEC2ARRAY = 35; const UNIFORMTYPE_IVEC3ARRAY = 36; const UNIFORMTYPE_UVEC3ARRAY = 37; const UNIFORMTYPE_BVEC3ARRAY = 38; const UNIFORMTYPE_IVEC4ARRAY = 39; const UNIFORMTYPE_UVEC4ARRAY = 40; const UNIFORMTYPE_BVEC4ARRAY = 41; const UNIFORMTYPE_ITEXTURE2D = 42; const UNIFORMTYPE_UTEXTURE2D = 43; const UNIFORMTYPE_ITEXTURECUBE = 44; const UNIFORMTYPE_UTEXTURECUBE = 45; const UNIFORMTYPE_ITEXTURE3D = 46; const UNIFORMTYPE_UTEXTURE3D = 47; const UNIFORMTYPE_ITEXTURE2D_ARRAY = 48; const UNIFORMTYPE_UTEXTURE2D_ARRAY = 49; const uniformTypeToName = [ 'bool', 'int', 'float', 'vec2', 'vec3', 'vec4', 'ivec2', 'ivec3', 'ivec4', 'bvec2', 'bvec3', 'bvec4', 'mat2', 'mat3', 'mat4', 'sampler2D', 'samplerCube', '', 'sampler2DShadow', 'samplerCubeShadow', 'sampler3D', '', '', '', '', 'sampler2DArray', 'uint', 'uvec2', 'uvec3', 'uvec4', '', '', '', '', '', '', '', '', '', '', '', '', 'isampler2D', 'usampler2D', 'isamplerCube', 'usamplerCube', 'isampler3D', 'usampler3D', 'isampler2DArray', 'usampler2DArray' ]; const uniformTypeToNameWGSL = [ [ 'bool' ], [ 'i32' ], [ 'f32' ], [ 'vec2f', 'vec2' ], [ 'vec3f', 'vec3' ], [ 'vec4f', 'vec4' ], [ 'vec2i', 'vec2' ], [ 'vec3i', 'vec3' ], [ 'vec4i', 'vec4' ], [ 'vec2' ], [ 'vec3' ], [ 'vec4' ], [ 'mat2x2f', 'mat2x2' ], [ 'mat3x3f', 'mat3x3' ], [ 'mat4x4f', 'mat4x4' ], [ 'texture_2d' ], [ 'texture_cube' ], [ 'array' ], [ 'texture_depth_2d' ], [ 'texture_depth_cube' ], [ 'texture_3d' ], [ 'array>' ], [ 'array>' ], [ 'array>' ], [ 'array>' ], [ 'texture_2d_array' ], [ 'u32' ], [ 'vec2u', 'vec2' ], [ 'vec3u', 'vec3' ], [ 'vec4u', 'vec4' ], [ 'array' ], [ 'array' ], [ 'array' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'array', 'array>' ], [ 'texture_2d' ], [ 'texture_2d' ], [ 'texture_cube' ], [ 'texture_cube' ], [ 'texture_3d' ], [ 'texture_3d' ], [ 'texture_2d_array' ], [ 'texture_2d_array' ] ]; const uniformTypeToNameMapWGSL = new Map(); uniformTypeToNameWGSL.forEach((names, index)=>{ names.forEach((name)=>uniformTypeToNameMapWGSL.set(name, index)); }); const DEVICETYPE_WEBGL2 = 'webgl2'; const DEVICETYPE_WEBGPU = 'webgpu'; const DEVICETYPE_NULL = 'null'; const SHADERSTAGE_VERTEX = 1; const SHADERSTAGE_FRAGMENT = 2; const SHADERSTAGE_COMPUTE = 4; const DISPLAYFORMAT_LDR = 'ldr'; const DISPLAYFORMAT_LDR_SRGB = 'ldr_srgb'; const DISPLAYFORMAT_HDR = 'hdr'; const TEXPROPERTY_MIN_FILTER = 1; const TEXPROPERTY_MAG_FILTER = 2; const TEXPROPERTY_ADDRESS_U = 4; const TEXPROPERTY_ADDRESS_V = 8; const TEXPROPERTY_ADDRESS_W = 16; const TEXPROPERTY_COMPARE_ON_READ = 32; const TEXPROPERTY_COMPARE_FUNC = 64; const TEXPROPERTY_ANISOTROPY = 128; const TEXPROPERTY_ALL = 255; const BINDGROUP_VIEW = 0; const BINDGROUP_MESH = 1; const BINDGROUP_MESH_UB = 2; const bindGroupNames = [ 'view', 'mesh', 'mesh_ub' ]; const UNIFORM_BUFFER_DEFAULT_SLOT_NAME = 'default'; const UNUSED_UNIFORM_NAME = '_unused_float_uniform'; const typedArrayTypes = [ Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Uint16Array ]; const typedArrayTypesByteSize = [ 1, 1, 2, 2, 4, 4, 4, 2 ]; const typedArrayIndexFormats = [ Uint8Array, Uint16Array, Uint32Array ]; const typedArrayIndexFormatsByteSize = [ 1, 2, 4 ]; const primitiveGlslToWgslTypeMap = new Map([ [ 'float', 'f32' ], [ 'vec2', 'vec2f' ], [ 'vec3', 'vec3f' ], [ 'vec4', 'vec4f' ], [ 'int', 'i32' ], [ 'ivec2', 'vec2i' ], [ 'ivec3', 'vec3i' ], [ 'ivec4', 'vec4i' ], [ 'uint', 'u32' ], [ 'uvec2', 'vec2u' ], [ 'uvec3', 'vec3u' ], [ 'uvec4', 'vec4u' ] ]); const semanticToLocation = {}; semanticToLocation[SEMANTIC_POSITION] = 0; semanticToLocation[SEMANTIC_NORMAL] = 1; semanticToLocation[SEMANTIC_BLENDWEIGHT] = 2; semanticToLocation[SEMANTIC_BLENDINDICES] = 3; semanticToLocation[SEMANTIC_COLOR] = 4; semanticToLocation[SEMANTIC_TEXCOORD0] = 5; semanticToLocation[SEMANTIC_TEXCOORD1] = 6; semanticToLocation[SEMANTIC_TEXCOORD2] = 7; semanticToLocation[SEMANTIC_TEXCOORD3] = 8; semanticToLocation[SEMANTIC_TEXCOORD4] = 9; semanticToLocation[SEMANTIC_TEXCOORD5] = 10; semanticToLocation[SEMANTIC_TEXCOORD6] = 11; semanticToLocation[SEMANTIC_TEXCOORD7] = 12; semanticToLocation[SEMANTIC_TANGENT] = 13; semanticToLocation[SEMANTIC_ATTR0] = 0; semanticToLocation[SEMANTIC_ATTR1] = 1; semanticToLocation[SEMANTIC_ATTR2] = 2; semanticToLocation[SEMANTIC_ATTR3] = 3; semanticToLocation[SEMANTIC_ATTR4] = 4; semanticToLocation[SEMANTIC_ATTR5] = 5; semanticToLocation[SEMANTIC_ATTR6] = 6; semanticToLocation[SEMANTIC_ATTR7] = 7; semanticToLocation[SEMANTIC_ATTR8] = 8; semanticToLocation[SEMANTIC_ATTR9] = 9; semanticToLocation[SEMANTIC_ATTR10] = 10; semanticToLocation[SEMANTIC_ATTR11] = 11; semanticToLocation[SEMANTIC_ATTR12] = 12; semanticToLocation[SEMANTIC_ATTR13] = 13; semanticToLocation[SEMANTIC_ATTR14] = 14; semanticToLocation[SEMANTIC_ATTR15] = 15; let id$d = 0; class BindBaseFormat { constructor(name, visibility){ this.slot = -1; this.scopeId = null; this.name = name; this.visibility = visibility; } } class BindUniformBufferFormat extends BindBaseFormat { } class BindStorageBufferFormat extends BindBaseFormat { constructor(name, visibility, readOnly = false){ super(name, visibility), this.format = ''; this.readOnly = readOnly; } } class BindTextureFormat extends BindBaseFormat { constructor(name, visibility, textureDimension = TEXTUREDIMENSION_2D, sampleType = SAMPLETYPE_FLOAT, hasSampler = true, samplerName = null){ super(name, visibility); this.textureDimension = textureDimension; this.sampleType = sampleType; this.hasSampler = hasSampler; this.samplerName = samplerName ?? `${name}_sampler`; } } class BindStorageTextureFormat extends BindBaseFormat { constructor(name, format = PIXELFORMAT_RGBA8, textureDimension = TEXTUREDIMENSION_2D, write = true, read = false){ super(name, SHADERSTAGE_COMPUTE); this.format = format; this.textureDimension = textureDimension; this.write = write; this.read = read; } } class BindGroupFormat { destroy() { this.impl.destroy(); } getTexture(name) { const index = this.textureFormatsMap.get(name); if (index !== undefined) { return this.textureFormats[index]; } return null; } getStorageTexture(name) { const index = this.storageTextureFormatsMap.get(name); if (index !== undefined) { return this.storageTextureFormats[index]; } return null; } loseContext() {} constructor(graphicsDevice, formats){ this.uniformBufferFormats = []; this.textureFormats = []; this.storageTextureFormats = []; this.storageBufferFormats = []; this.id = id$d++; let slot = 0; formats.forEach((format)=>{ format.slot = slot++; if (format instanceof BindTextureFormat && format.hasSampler) { slot++; } if (format instanceof BindUniformBufferFormat) { this.uniformBufferFormats.push(format); } else if (format instanceof BindTextureFormat) { this.textureFormats.push(format); } else if (format instanceof BindStorageTextureFormat) { this.storageTextureFormats.push(format); } else if (format instanceof BindStorageBufferFormat) { this.storageBufferFormats.push(format); } else ; }); this.device = graphicsDevice; const scope = graphicsDevice.scope; this.bufferFormatsMap = new Map(); this.uniformBufferFormats.forEach((bf, i)=>this.bufferFormatsMap.set(bf.name, i)); this.textureFormatsMap = new Map(); this.textureFormats.forEach((tf, i)=>{ this.textureFormatsMap.set(tf.name, i); tf.scopeId = scope.resolve(tf.name); }); this.storageTextureFormatsMap = new Map(); this.storageTextureFormats.forEach((tf, i)=>{ this.storageTextureFormatsMap.set(tf.name, i); tf.scopeId = scope.resolve(tf.name); }); this.storageBufferFormatsMap = new Map(); this.storageBufferFormats.forEach((bf, i)=>{ this.storageBufferFormatsMap.set(bf.name, i); bf.scopeId = scope.resolve(bf.name); }); this.impl = graphicsDevice.createBindGroupFormatImpl(this); } } class DeviceCache { get(device, onCreate) { if (!this._cache.has(device)) { this._cache.set(device, onCreate()); device.on('destroy', ()=>{ this.remove(device); }); device.on('devicelost', ()=>{ this._cache.get(device)?.loseContext?.(device); }); } return this._cache.get(device); } remove(device) { this._cache.get(device)?.destroy?.(device); this._cache.delete(device); } constructor(){ this._cache = new Map(); } } class TextureUtils { static calcLevelDimension(dimension, mipLevel) { return Math.max(dimension >> mipLevel, 1); } static calcMipLevelsCount(width, height, depth = 1) { return 1 + Math.floor(Math.log2(Math.max(width, height, depth))); } static calcLevelGpuSize(width, height, depth, format) { const formatInfo = pixelFormatInfo.get(format); const pixelSize = pixelFormatInfo.get(format)?.size ?? 0; if (pixelSize > 0) { return width * height * depth * pixelSize; } const blockSize = formatInfo.blockSize ?? 0; let blockWidth = Math.floor((width + 3) / 4); const blockHeight = Math.floor((height + 3) / 4); const blockDepth = Math.floor((depth + 3) / 4); if (format === PIXELFORMAT_PVRTC_2BPP_RGB_1 || format === PIXELFORMAT_PVRTC_2BPP_RGBA_1) { blockWidth = Math.max(Math.floor(blockWidth / 2), 1); } return blockWidth * blockHeight * blockDepth * blockSize; } static calcGpuSize(width, height, depth, format, mipmaps, cubemap) { let result = 0; while(1){ result += TextureUtils.calcLevelGpuSize(width, height, depth, format); if (!mipmaps || width === 1 && height === 1 && depth === 1) { break; } width = Math.max(width >> 1, 1); height = Math.max(height >> 1, 1); depth = Math.max(depth >> 1, 1); } return result * (cubemap ? 6 : 1); } } class StringIds { get(name) { let value = this.map.get(name); if (value === undefined) { value = this.id++; this.map.set(name, value); } return value; } constructor(){ this.map = new Map(); this.id = 0; } } const stringIds$5 = new StringIds(); class TextureView { constructor(texture, baseMipLevel = 0, mipLevelCount = 1, baseArrayLayer = 0, arrayLayerCount = 1){ this.texture = texture; this.baseMipLevel = baseMipLevel; this.mipLevelCount = mipLevelCount; this.baseArrayLayer = baseArrayLayer; this.arrayLayerCount = arrayLayerCount; this.key = stringIds$5.get(`${baseMipLevel}:${mipLevelCount}:${baseArrayLayer}:${arrayLayerCount}`); } } let id$c = 0; class Texture { destroy() { const device = this.device; if (device) { device.onTextureDestroyed(this); this.impl.destroy(device); this.adjustVramSizeTracking(device._vram, -this._gpuSize); this._levels = null; this.device = null; } } recreateImpl(upload = true) { const { device } = this; this.impl?.destroy(device); this.impl = null; this.impl = device.createTextureImpl(this); this.dirtyAll(); if (upload) { this.upload(); } } _clearLevels() { this._levels = this._cubemap ? [ [ null, null, null, null, null, null ] ] : [ null ]; } resize(width, height, depth = 1) { if (this.width !== width || this.height !== height || this.depth !== depth) { const device = this.device; this.adjustVramSizeTracking(device._vram, -this._gpuSize); this._gpuSize = 0; this.impl.destroy(device); this._clearLevels(); this._width = Math.floor(width); this._height = Math.floor(height); this._depth = Math.floor(depth); this._updateNumLevel(); this.impl = device.createTextureImpl(this); this.dirtyAll(); } } loseContext() { this.impl.loseContext(); this.dirtyAll(); } adjustVramSizeTracking(vram, size) { vram.tex += size; } propertyChanged(flag) { this.impl.propertyChanged(flag); this.renderVersionDirty = this.device.renderVersion; } _updateNumLevel() { const maxLevels = this.mipmaps ? TextureUtils.calcMipLevelsCount(this.width, this.height) : 1; const requestedLevels = this._numLevelsRequested; this._numLevels = Math.min(requestedLevels ?? maxLevels, maxLevels); this._mipmaps = this._numLevels > 1; } get lockedMode() { return this._lockedMode; } set minFilter(v) { if (this._minFilter !== v) { if (isIntegerPixelFormat(this._format)) ; else { this._minFilter = v; this.propertyChanged(TEXPROPERTY_MIN_FILTER); } } } get minFilter() { return this._minFilter; } set magFilter(v) { if (this._magFilter !== v) { if (isIntegerPixelFormat(this._format)) ; else { this._magFilter = v; this.propertyChanged(TEXPROPERTY_MAG_FILTER); } } } get magFilter() { return this._magFilter; } set addressU(v) { if (this._addressU !== v) { this._addressU = v; this.propertyChanged(TEXPROPERTY_ADDRESS_U); } } get addressU() { return this._addressU; } set addressV(v) { if (this._addressV !== v) { this._addressV = v; this.propertyChanged(TEXPROPERTY_ADDRESS_V); } } get addressV() { return this._addressV; } set addressW(addressW) { if (!this._volume) { return; } if (addressW !== this._addressW) { this._addressW = addressW; this.propertyChanged(TEXPROPERTY_ADDRESS_W); } } get addressW() { return this._addressW; } set compareOnRead(v) { if (this._compareOnRead !== v) { this._compareOnRead = v; this.propertyChanged(TEXPROPERTY_COMPARE_ON_READ); } } get compareOnRead() { return this._compareOnRead; } set compareFunc(v) { if (this._compareFunc !== v) { this._compareFunc = v; this.propertyChanged(TEXPROPERTY_COMPARE_FUNC); } } get compareFunc() { return this._compareFunc; } set anisotropy(v) { if (this._anisotropy !== v) { this._anisotropy = v; this.propertyChanged(TEXPROPERTY_ANISOTROPY); } } get anisotropy() { return this._anisotropy; } set mipmaps(v) { if (this._mipmaps !== v) { if (this.device.isWebGPU) ; else if (isIntegerPixelFormat(this._format)) ; else { this._mipmaps = v; } if (v) { this._needsMipmapsUpload = true; this.device?.texturesToUpload?.add(this); } } } get mipmaps() { return this._mipmaps; } get numLevels() { return this._numLevels; } get storage() { return this._storage; } get width() { return this._width; } get height() { return this._height; } get depth() { return this._depth; } get format() { return this._format; } get cubemap() { return this._cubemap; } get gpuSize() { const mips = this.pot && this._mipmaps && !(this._compressed && this._levels.length === 1); return TextureUtils.calcGpuSize(this._width, this._height, this._depth, this._format, mips, this._cubemap); } get array() { return this._arrayLength > 0; } get arrayLength() { return this._arrayLength; } get volume() { return this._volume; } set type(value) { if (this._type !== value) { this._type = value; this.device._shadersDirty = true; } } get type() { return this._type; } set srgb(value) { const currentSrgb = isSrgbPixelFormat(this.format); if (value !== currentSrgb) { if (value) { const srgbFormat = pixelFormatLinearToGamma(this.format); if (this._format !== srgbFormat) { this._format = srgbFormat; this.recreateImpl(); this.device._shadersDirty = true; } } else { const linearFormat = pixelFormatGammaToLinear(this.format); if (this._format !== linearFormat) { this._format = linearFormat; this.recreateImpl(); this.device._shadersDirty = true; } } } } get srgb() { return isSrgbPixelFormat(this.format); } set flipY(flipY) { if (this._flipY !== flipY) { this._flipY = flipY; this.markForUpload(); } } get flipY() { return this._flipY; } set premultiplyAlpha(premultiplyAlpha) { if (this._premultiplyAlpha !== premultiplyAlpha) { this._premultiplyAlpha = premultiplyAlpha; this.markForUpload(); } } get premultiplyAlpha() { return this._premultiplyAlpha; } get pot() { return math.powerOfTwo(this._width) && math.powerOfTwo(this._height); } get encoding() { switch(this.type){ case TEXTURETYPE_RGBM: return 'rgbm'; case TEXTURETYPE_RGBE: return 'rgbe'; case TEXTURETYPE_RGBP: return 'rgbp'; } return requiresManualGamma(this.format) ? 'srgb' : 'linear'; } dirtyAll() { this._levelsUpdated = this._cubemap ? [ [ true, true, true, true, true, true ] ] : [ true ]; this.markForUpload(); this._needsMipmapsUpload = this._mipmaps; this._mipmapsUploaded = false; this.propertyChanged(TEXPROPERTY_ALL); } lock(options = {}) { var _options, _options1, _options2; (_options = options).level ?? (_options.level = 0); (_options1 = options).face ?? (_options1.face = 0); (_options2 = options).mode ?? (_options2.mode = TEXTURELOCK_WRITE); this._lockedMode = options.mode; this._lockedLevel = options.level; const levels = this.cubemap ? this._levels[options.face] : this._levels; if (levels[options.level] === null) { const width = Math.max(1, this._width >> options.level); const height = Math.max(1, this._height >> options.level); const depth = Math.max(1, this._depth >> options.level); const data = new ArrayBuffer(TextureUtils.calcLevelGpuSize(width, height, depth, this._format)); levels[options.level] = new (getPixelFormatArrayType(this._format))(data); } return levels[options.level]; } setSource(source, mipLevel = 0) { let invalid = false; let width, height; if (this._cubemap) { if (source[0]) { width = source[0].width || 0; height = source[0].height || 0; for(let i = 0; i < 6; i++){ const face = source[i]; if (!face || face.width !== width || face.height !== height || !this.device._isBrowserInterface(face)) { invalid = true; break; } } } else { invalid = true; } if (!invalid) { for(let i = 0; i < 6; i++){ if (this._levels[mipLevel][i] !== source[i]) { this._levelsUpdated[mipLevel][i] = true; } } } } else { if (!this.device._isBrowserInterface(source)) { invalid = true; } if (!invalid) { if (source !== this._levels[mipLevel]) { this._levelsUpdated[mipLevel] = true; } if (source instanceof HTMLVideoElement) { width = source.videoWidth; height = source.videoHeight; } else { width = source.width; height = source.height; } } } if (invalid) { this._width = 4; this._height = 4; if (this._cubemap) { for(let i = 0; i < 6; i++){ this._levels[mipLevel][i] = null; this._levelsUpdated[mipLevel][i] = true; } } else { this._levels[mipLevel] = null; this._levelsUpdated[mipLevel] = true; } } else { if (mipLevel === 0) { this._width = width; this._height = height; } this._levels[mipLevel] = source; } if (this._invalid !== invalid || !invalid) { this._invalid = invalid; this.upload(); } } getSource(mipLevel = 0) { return this._levels[mipLevel]; } unlock() { if (this._lockedMode === TEXTURELOCK_NONE) ; if (this._lockedMode === TEXTURELOCK_WRITE) { this.upload(); } this._lockedLevel = -1; this._lockedMode = TEXTURELOCK_NONE; } markForUpload() { this._needsUpload = true; this.device?.texturesToUpload?.add(this); } upload() { this.markForUpload(); this._needsMipmapsUpload = this._mipmaps; this.impl.uploadImmediate?.(this.device, this); } read(x, y, width, height, options = {}) { return this.impl.read?.(x, y, width, height, options); } write(x, y, width, height, data) { return this.impl.write?.(x, y, width, height, data); } getView(baseMipLevel = 0, mipLevelCount = 1, baseArrayLayer = 0, arrayLayerCount = 1) { return new TextureView(this, baseMipLevel, mipLevelCount, baseArrayLayer, arrayLayerCount); } constructor(graphicsDevice, options = {}){ this._gpuSize = 0; this.id = id$c++; this._invalid = false; this._lockedLevel = -1; this._lockedMode = TEXTURELOCK_NONE; this.renderVersionDirty = 0; this._storage = false; this._numLevels = 0; this.device = graphicsDevice; this.name = options.name ?? ''; this._width = Math.floor(options.width ?? 4); this._height = Math.floor(options.height ?? 4); this._format = options.format ?? PIXELFORMAT_RGBA8; this._compressed = isCompressedPixelFormat(this._format); this._integerFormat = isIntegerPixelFormat(this._format); if (this._integerFormat) { options.minFilter = FILTER_NEAREST; options.magFilter = FILTER_NEAREST; } this._volume = options.volume ?? false; this._depth = Math.floor(options.depth ?? 1); this._arrayLength = Math.floor(options.arrayLength ?? 0); this._storage = options.storage ?? false; this._cubemap = options.cubemap ?? false; this._flipY = options.flipY ?? false; this._premultiplyAlpha = options.premultiplyAlpha ?? false; this._mipmaps = options.mipmaps ?? true; this._numLevelsRequested = options.numLevels; if (options.numLevels !== undefined) { this._numLevels = options.numLevels; } this._updateNumLevel(); this._minFilter = options.minFilter ?? FILTER_LINEAR_MIPMAP_LINEAR; this._magFilter = options.magFilter ?? FILTER_LINEAR; this._anisotropy = options.anisotropy ?? 1; this._addressU = options.addressU ?? ADDRESS_REPEAT; this._addressV = options.addressV ?? ADDRESS_REPEAT; this._addressW = options.addressW ?? ADDRESS_REPEAT; this._compareOnRead = options.compareOnRead ?? false; this._compareFunc = options.compareFunc ?? FUNC_LESS; this._type = options.type ?? TEXTURETYPE_DEFAULT; this.projection = TEXTUREPROJECTION_NONE; if (this._cubemap) { this.projection = TEXTUREPROJECTION_CUBE; } else if (options.projection && options.projection !== TEXTUREPROJECTION_CUBE) { this.projection = options.projection; } this._levels = options.levels; const upload = !!options.levels; if (!this._levels) { this._clearLevels(); } this.recreateImpl(upload); } } const textureData = { white: [ 255, 255, 255, 255 ], gray: [ 128, 128, 128, 255 ], black: [ 0, 0, 0, 255 ], normal: [ 128, 128, 255, 255 ], pink: [ 255, 128, 255, 255 ] }; class BuiltInTextures { destroy() { this.map.forEach((texture)=>{ texture.destroy(); }); } constructor(){ this.map = new Map(); } } const deviceCache$3 = new DeviceCache(); const getBuiltInTexture = (device, name)=>{ const cache = deviceCache$3.get(device, ()=>{ return new BuiltInTextures(); }); if (!cache.map.has(name)) { const texture = new Texture(device, { name: `built-in-texture-${name}`, width: 1, height: 1, format: PIXELFORMAT_RGBA8 }); const pixels = texture.lock(); const data = textureData[name]; pixels.set(data); texture.unlock(); cache.map.set(name, texture); } return cache.map.get(name); }; let id$b = 0; class DynamicBindGroup { constructor(){ this.offsets = []; } } class BindGroup { destroy() { this.impl.destroy(); this.impl = null; this.format = null; this.defaultUniformBuffer = null; } setUniformBuffer(name, uniformBuffer) { const index = this.format.bufferFormatsMap.get(name); if (this.uniformBuffers[index] !== uniformBuffer) { this.uniformBuffers[index] = uniformBuffer; this.dirty = true; } } setStorageBuffer(name, storageBuffer) { const index = this.format.storageBufferFormatsMap.get(name); if (this.storageBuffers[index] !== storageBuffer) { this.storageBuffers[index] = storageBuffer; this.dirty = true; } } setTexture(name, value) { const index = this.format.textureFormatsMap.get(name); const texture = value instanceof TextureView ? value.texture : value; if (this.textures[index] !== value) { this.textures[index] = value; this.dirty = true; } else if (this.renderVersionUpdated < texture.renderVersionDirty) { this.dirty = true; } } setStorageTexture(name, value) { const index = this.format.storageTextureFormatsMap.get(name); const texture = value instanceof TextureView ? value.texture : value; if (this.storageTextures[index] !== value) { this.storageTextures[index] = value; this.dirty = true; } else if (this.renderVersionUpdated < texture.renderVersionDirty) { this.dirty = true; } } updateUniformBuffers() { for(let i = 0; i < this.uniformBuffers.length; i++){ this.uniformBuffers[i].update(); } } update() { const { textureFormats, storageTextureFormats, storageBufferFormats } = this.format; for(let i = 0; i < textureFormats.length; i++){ const textureFormat = textureFormats[i]; let value = textureFormat.scopeId.value; if (!value) { if (textureFormat.name === 'uSceneDepthMap') { value = getBuiltInTexture(this.device, 'white'); } if (textureFormat.name === 'uSceneColorMap') { value = getBuiltInTexture(this.device, 'pink'); } if (!value) { value = getBuiltInTexture(this.device, 'pink'); } } this.setTexture(textureFormat.name, value); } for(let i = 0; i < storageTextureFormats.length; i++){ const storageTextureFormat = storageTextureFormats[i]; const value = storageTextureFormat.scopeId.value; this.setStorageTexture(storageTextureFormat.name, value); } for(let i = 0; i < storageBufferFormats.length; i++){ const storageBufferFormat = storageBufferFormats[i]; const value = storageBufferFormat.scopeId.value; this.setStorageBuffer(storageBufferFormat.name, value); } this.uniformBufferOffsets.length = this.uniformBuffers.length; for(let i = 0; i < this.uniformBuffers.length; i++){ const uniformBuffer = this.uniformBuffers[i]; this.uniformBufferOffsets[i] = uniformBuffer.offset; if (this.renderVersionUpdated < uniformBuffer.renderVersionDirty) { this.dirty = true; } } if (this.dirty) { this.dirty = false; this.renderVersionUpdated = this.device.renderVersion; this.impl.update(this); } } constructor(graphicsDevice, format, defaultUniformBuffer){ this.renderVersionUpdated = -1; this.uniformBufferOffsets = []; this.id = id$b++; this.device = graphicsDevice; this.format = format; this.dirty = true; this.impl = graphicsDevice.createBindGroupImpl(this); this.textures = []; this.storageTextures = []; this.storageBuffers = []; this.uniformBuffers = []; this.defaultUniformBuffer = defaultUniformBuffer; if (defaultUniformBuffer) { this.setUniformBuffer(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, defaultUniformBuffer); } } } const BitPacking = { set (storage, value, shift, mask = 1) { const data = storage & ~(mask << shift); return data | value << shift; }, get (storage, shift, mask = 1) { return storage >> shift & mask; }, all (storage, shift, mask = 1) { const shifted = mask << shift; return (storage & shifted) === shifted; }, any (storage, shift, mask = 1) { return (storage & mask << shift) !== 0; } }; const opMask = 0b111; const factorMask = 0b1111; const colorOpShift = 0; const colorSrcFactorShift = 3; const colorDstFactorShift = 7; const alphaOpShift = 11; const alphaSrcFactorShift = 14; const alphaDstFactorShift = 18; const redWriteShift = 22; const greenWriteShift = 23; const blueWriteShift = 24; const alphaWriteShift = 25; const blendShift = 26; const allWriteMasks = 0b1111; const allWriteShift = redWriteShift; class BlendState { set blend(value) { this.target0 = BitPacking.set(this.target0, value ? 1 : 0, blendShift); } get blend() { return BitPacking.all(this.target0, blendShift); } setColorBlend(op, srcFactor, dstFactor) { this.target0 = BitPacking.set(this.target0, op, colorOpShift, opMask); this.target0 = BitPacking.set(this.target0, srcFactor, colorSrcFactorShift, factorMask); this.target0 = BitPacking.set(this.target0, dstFactor, colorDstFactorShift, factorMask); } setAlphaBlend(op, srcFactor, dstFactor) { this.target0 = BitPacking.set(this.target0, op, alphaOpShift, opMask); this.target0 = BitPacking.set(this.target0, srcFactor, alphaSrcFactorShift, factorMask); this.target0 = BitPacking.set(this.target0, dstFactor, alphaDstFactorShift, factorMask); } setColorWrite(redWrite, greenWrite, blueWrite, alphaWrite) { this.redWrite = redWrite; this.greenWrite = greenWrite; this.blueWrite = blueWrite; this.alphaWrite = alphaWrite; } get colorOp() { return BitPacking.get(this.target0, colorOpShift, opMask); } get colorSrcFactor() { return BitPacking.get(this.target0, colorSrcFactorShift, factorMask); } get colorDstFactor() { return BitPacking.get(this.target0, colorDstFactorShift, factorMask); } get alphaOp() { return BitPacking.get(this.target0, alphaOpShift, opMask); } get alphaSrcFactor() { return BitPacking.get(this.target0, alphaSrcFactorShift, factorMask); } get alphaDstFactor() { return BitPacking.get(this.target0, alphaDstFactorShift, factorMask); } set redWrite(value) { this.target0 = BitPacking.set(this.target0, value ? 1 : 0, redWriteShift); } get redWrite() { return BitPacking.all(this.target0, redWriteShift); } set greenWrite(value) { this.target0 = BitPacking.set(this.target0, value ? 1 : 0, greenWriteShift); } get greenWrite() { return BitPacking.all(this.target0, greenWriteShift); } set blueWrite(value) { this.target0 = BitPacking.set(this.target0, value ? 1 : 0, blueWriteShift); } get blueWrite() { return BitPacking.all(this.target0, blueWriteShift); } set alphaWrite(value) { this.target0 = BitPacking.set(this.target0, value ? 1 : 0, alphaWriteShift); } get alphaWrite() { return BitPacking.all(this.target0, alphaWriteShift); } get allWrite() { return BitPacking.get(this.target0, allWriteShift, allWriteMasks); } copy(rhs) { this.target0 = rhs.target0; return this; } clone() { const clone = new this.constructor(); return clone.copy(this); } get key() { return this.target0; } equals(rhs) { return this.target0 === rhs.target0; } constructor(blend = false, colorOp = BLENDEQUATION_ADD, colorSrcFactor = BLENDMODE_ONE, colorDstFactor = BLENDMODE_ZERO, alphaOp, alphaSrcFactor, alphaDstFactor, redWrite = true, greenWrite = true, blueWrite = true, alphaWrite = true){ this.target0 = 0; this.setColorBlend(colorOp, colorSrcFactor, colorDstFactor); this.setAlphaBlend(alphaOp ?? colorOp, alphaSrcFactor ?? colorSrcFactor, alphaDstFactor ?? colorDstFactor); this.setColorWrite(redWrite, greenWrite, blueWrite, alphaWrite); this.blend = blend; } } BlendState.NOBLEND = Object.freeze(new BlendState()); BlendState.NOWRITE = Object.freeze(new BlendState(undefined, undefined, undefined, undefined, undefined, undefined, undefined, false, false, false, false)); BlendState.ALPHABLEND = Object.freeze(new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA)); BlendState.ADDBLEND = Object.freeze(new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE)); const stringIds$4 = new StringIds(); const funcMask = 0b111; const funcShift = 0; const writeShift = 3; class DepthState { set test(value) { this.func = value ? FUNC_LESSEQUAL : FUNC_ALWAYS; this.updateKey(); } get test() { return this.func !== FUNC_ALWAYS; } set write(value) { this.data = BitPacking.set(this.data, value ? 1 : 0, writeShift); this.updateKey(); } get write() { return BitPacking.all(this.data, writeShift); } set func(value) { this.data = BitPacking.set(this.data, value, funcShift, funcMask); this.updateKey(); } get func() { return BitPacking.get(this.data, funcShift, funcMask); } set depthBias(value) { this._depthBias = value; this.updateKey(); } get depthBias() { return this._depthBias; } set depthBiasSlope(value) { this._depthBiasSlope = value; this.updateKey(); } get depthBiasSlope() { return this._depthBiasSlope; } copy(rhs) { this.data = rhs.data; this._depthBias = rhs._depthBias; this._depthBiasSlope = rhs._depthBiasSlope; this.key = rhs.key; return this; } clone() { const clone = new this.constructor(); return clone.copy(this); } updateKey() { const { data, _depthBias, _depthBiasSlope } = this; const key = `${data}-${_depthBias}-${_depthBiasSlope}`; this.key = stringIds$4.get(key); } equals(rhs) { return this.key === rhs.key; } constructor(func = FUNC_LESSEQUAL, write = true){ this.data = 0; this._depthBias = 0; this._depthBiasSlope = 0; this.key = 0; this.func = func; this.write = write; } } DepthState.DEFAULT = Object.freeze(new DepthState()); DepthState.NODEPTH = Object.freeze(new DepthState(FUNC_ALWAYS, false)); DepthState.WRITEDEPTH = Object.freeze(new DepthState(FUNC_ALWAYS, true)); class Version { equals(other) { return this.globalId === other.globalId && this.revision === other.revision; } copy(other) { this.globalId = other.globalId; this.revision = other.revision; } reset() { this.globalId = 0; this.revision = 0; } constructor(){ this.globalId = 0; this.revision = 0; } } let idCounter = 0; class VersionedObject { increment() { this.version.revision++; } constructor(){ idCounter++; this.version = new Version(); this.version.globalId = idCounter; } } class ScopeId { toJSON(key) { return undefined; } setValue(value) { this.value = value; this.versionObject.increment(); } getValue() { return this.value; } constructor(name){ this.name = name; this.value = null; this.versionObject = new VersionedObject(); } } class ScopeSpace { resolve(name) { if (!this.variables.has(name)) { this.variables.set(name, new ScopeId(name)); } return this.variables.get(name); } removeValue(value) { for (const uniform of this.variables.values()){ if (uniform.value === value) { uniform.value = null; } } } constructor(name){ this.name = name; this.variables = new Map(); } } let id$a = 0; class VertexBuffer { destroy() { const device = this.device; device.buffers.delete(this); if (this.impl.initialized) { this.impl.destroy(device); this.adjustVramSizeTracking(device._vram, -this.storage.byteLength); } } adjustVramSizeTracking(vram, size) { vram.vb += size; } loseContext() { this.impl.loseContext(); } getFormat() { return this.format; } getUsage() { return this.usage; } getNumVertices() { return this.numVertices; } lock() { return this.storage; } unlock() { this.impl.unlock(this); } setData(data) { if (data.byteLength !== this.numBytes) { return false; } this.storage = data; this.unlock(); return true; } constructor(graphicsDevice, format, numVertices, options){ this.usage = BUFFER_STATIC; this.usage = options?.usage ?? BUFFER_STATIC; this.device = graphicsDevice; this.format = format; this.numVertices = numVertices; this.id = id$a++; this.impl = graphicsDevice.createVertexBufferImpl(this, format, options); this.numBytes = format.verticesByteSize ? format.verticesByteSize : format.size * numVertices; this.adjustVramSizeTracking(graphicsDevice._vram, this.numBytes); const initialData = options?.data; if (initialData) { this.setData(initialData); } else { this.storage = new ArrayBuffer(this.numBytes); } this.device.buffers.add(this); } } function hashCode(str) { if (str === null || str === undefined) { return 0; } let hash = 0; for(let i = 0, len = str.length; i < len; i++){ hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } return hash; } function hash32Fnv1a(array) { const prime = 16777619; let hash = 2166136261; for(let i = 0; i < array.length; i++){ hash ^= array[i]; hash *= prime; } return hash >>> 0; } const stringIds$3 = new StringIds(); const webgpuValidElementSizes = [ 2, 4, 8, 12, 16 ]; const deviceCache$2 = new DeviceCache(); class VertexFormat { get elements() { return this._elements; } static getDefaultInstancingFormat(graphicsDevice) { return deviceCache$2.get(graphicsDevice, ()=>{ return new VertexFormat(graphicsDevice, [ { semantic: SEMANTIC_ATTR11, components: 4, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR12, components: 4, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR14, components: 4, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR15, components: 4, type: TYPE_FLOAT32 } ]); }); } static isElementValid(graphicsDevice, elementDesc) { const elementSize = elementDesc.components * typedArrayTypesByteSize[elementDesc.type]; if (graphicsDevice.isWebGPU && !webgpuValidElementSizes.includes(elementSize)) { return false; } return true; } update() { this._evaluateHash(); } _evaluateHash() { const stringElementsBatch = []; const stringElementsRender = []; const len = this._elements.length; for(let i = 0; i < len; i++){ const { name, dataType, numComponents, normalize, offset, stride, size, asInt } = this._elements[i]; const stringElementBatch = name + dataType + numComponents + normalize + asInt; stringElementsBatch.push(stringElementBatch); const stringElementRender = stringElementBatch + offset + stride + size; stringElementsRender.push(stringElementRender); } stringElementsBatch.sort(); const batchingString = stringElementsBatch.join(); this.batchingHash = hashCode(batchingString); this.shaderProcessingHashString = batchingString; this.renderingHashString = stringElementsRender.join('_'); this.renderingHash = stringIds$3.get(this.renderingHashString); } constructor(graphicsDevice, description, vertexCount){ this.device = graphicsDevice; this._elements = []; this.hasUv0 = false; this.hasUv1 = false; this.hasColor = false; this.hasTangents = false; this.verticesByteSize = 0; this.vertexCount = vertexCount; this.interleaved = vertexCount === undefined; this.instancing = false; this.size = description.reduce((total, desc)=>{ return total + Math.ceil(desc.components * typedArrayTypesByteSize[desc.type] / 4) * 4; }, 0); let offset = 0, elementSize; for(let i = 0, len = description.length; i < len; i++){ const elementDesc = description[i]; elementSize = elementDesc.components * typedArrayTypesByteSize[elementDesc.type]; if (vertexCount) { offset = math.roundUp(offset, elementSize); } const asInt = elementDesc.asInt ?? false; const normalize = asInt ? false : elementDesc.normalize ?? false; const element = { name: elementDesc.semantic, offset: vertexCount ? offset : elementDesc.hasOwnProperty('offset') ? elementDesc.offset : offset, stride: vertexCount ? elementSize : elementDesc.hasOwnProperty('stride') ? elementDesc.stride : this.size, dataType: elementDesc.type, numComponents: elementDesc.components, normalize: normalize, size: elementSize, asInt: asInt }; this._elements.push(element); if (vertexCount) { offset += elementSize * vertexCount; } else { offset += Math.ceil(elementSize / 4) * 4; } if (elementDesc.semantic === SEMANTIC_TEXCOORD0) { this.hasUv0 = true; } else if (elementDesc.semantic === SEMANTIC_TEXCOORD1) { this.hasUv1 = true; } else if (elementDesc.semantic === SEMANTIC_COLOR) { this.hasColor = true; } else if (elementDesc.semantic === SEMANTIC_TANGENT) { this.hasTangents = true; } } if (vertexCount) { this.verticesByteSize = offset; } this._evaluateHash(); } } const stringIds$2 = new StringIds(); class StencilParameters { set func(value) { this._func = value; this._dirty = true; } get func() { return this._func; } set ref(value) { this._ref = value; this._dirty = true; } get ref() { return this._ref; } set fail(value) { this._fail = value; this._dirty = true; } get fail() { return this._fail; } set zfail(value) { this._zfail = value; this._dirty = true; } get zfail() { return this._zfail; } set zpass(value) { this._zpass = value; this._dirty = true; } get zpass() { return this._zpass; } set readMask(value) { this._readMask = value; this._dirty = true; } get readMask() { return this._readMask; } set writeMask(value) { this._writeMask = value; this._dirty = true; } get writeMask() { return this._writeMask; } _evalKey() { const { _func, _ref, _fail, _zfail, _zpass, _readMask, _writeMask } = this; const key = `${_func},${_ref},${_fail},${_zfail},${_zpass},${_readMask},${_writeMask}`; this._key = stringIds$2.get(key); this._dirty = false; } get key() { if (this._dirty) { this._evalKey(); } return this._key; } copy(rhs) { this._func = rhs._func; this._ref = rhs._ref; this._readMask = rhs._readMask; this._writeMask = rhs._writeMask; this._fail = rhs._fail; this._zfail = rhs._zfail; this._zpass = rhs._zpass; this._dirty = rhs._dirty; this._key = rhs._key; return this; } clone() { const clone = new this.constructor(); return clone.copy(this); } constructor(options = {}){ this._dirty = true; this._func = options.func ?? FUNC_ALWAYS; this._ref = options.ref ?? 0; this._readMask = options.readMask ?? 0xFF; this._writeMask = options.writeMask ?? 0xFF; this._fail = options.fail ?? STENCILOP_KEEP; this._zfail = options.zfail ?? STENCILOP_KEEP; this._zpass = options.zpass ?? STENCILOP_KEEP; this._evalKey(); } } StencilParameters.DEFAULT = Object.freeze(new StencilParameters()); class GraphicsDevice extends EventHandler { postInit() { const vertexFormat = new VertexFormat(this, [ { semantic: SEMANTIC_POSITION, components: 2, type: TYPE_FLOAT32 } ]); const positions = new Float32Array([ -1, -1, 1, -1, -1, 1, 1, 1 ]); this.quadVertexBuffer = new VertexBuffer(this, vertexFormat, 4, { data: positions }); } initCapsDefines() { const { capsDefines } = this; capsDefines.clear(); if (this.textureFloatFilterable) capsDefines.set('CAPS_TEXTURE_FLOAT_FILTERABLE', ''); if (this.textureFloatRenderable) capsDefines.set('CAPS_TEXTURE_FLOAT_RENDERABLE', ''); if (this.supportsMultiDraw) capsDefines.set('CAPS_MULTI_DRAW', ''); if (this.supportsPrimitiveIndex) capsDefines.set('CAPS_PRIMITIVE_INDEX', ''); if (platform.desktop) capsDefines.set('PLATFORM_DESKTOP', ''); if (platform.mobile) capsDefines.set('PLATFORM_MOBILE', ''); if (platform.android) capsDefines.set('PLATFORM_ANDROID', ''); if (platform.ios) capsDefines.set('PLATFORM_IOS', ''); } destroy() { this.fire('destroy'); this.quadVertexBuffer?.destroy(); this.quadVertexBuffer = null; this.dynamicBuffers?.destroy(); this.dynamicBuffers = null; this.gpuProfiler?.destroy(); this.gpuProfiler = null; this._destroyed = true; } onDestroyShader(shader) { this.fire('destroy:shader', shader); const idx = this.shaders.indexOf(shader); if (idx !== -1) { this.shaders.splice(idx, 1); } } onTextureDestroyed(texture) { this.textures.delete(texture); this.texturesToUpload.delete(texture); this.scope.removeValue(texture); } postDestroy() { this.scope = null; this.canvas = null; } loseContext() { this.contextLost = true; this.backBufferSize.set(-1, -1); for (const texture of this.textures){ texture.loseContext(); } for (const buffer of this.buffers){ buffer.loseContext(); } for (const target of this.targets){ target.loseContext(); } this.gpuProfiler?.loseContext(); } restoreContext() { this.contextLost = false; this.initializeRenderState(); this.initializeContextCaches(); for (const buffer of this.buffers){ buffer.unlock(); } this.gpuProfiler?.restoreContext?.(); } toJSON(key) { return undefined; } initializeContextCaches() { this.vertexBuffers = []; this.shader = null; this.shaderValid = undefined; this.shaderAsyncCompile = false; this.renderTarget = null; } initializeRenderState() { this.blendState = new BlendState(); this.depthState = new DepthState(); this.cullMode = CULLFACE_BACK; this.vx = this.vy = this.vw = this.vh = 0; this.sx = this.sy = this.sw = this.sh = 0; this.blendColor = new Color(0, 0, 0, 0); } setStencilState(stencilFront, stencilBack) {} setBlendState(blendState) {} setBlendColor(r, g, b, a) {} setDepthState(depthState) {} setCullMode(cullMode) {} setRenderTarget(renderTarget) { this.renderTarget = renderTarget; } setVertexBuffer(vertexBuffer) { if (vertexBuffer) { this.vertexBuffers.push(vertexBuffer); } } clearVertexBuffer() { this.vertexBuffers.length = 0; } getIndirectDrawSlot(count = 1) { return 0; } get indirectDrawBuffer() { return null; } getIndirectDispatchSlot(count = 1) { return 0; } get indirectDispatchBuffer() { return null; } getRenderTarget() { return this.renderTarget; } initRenderTarget(target) { if (target.initialized) return; target.init(); this.targets.add(target); } draw(primitive, indexBuffer, numInstances, drawCommands, first = true, last = true) {} _isBrowserInterface(texture) { return this._isImageBrowserInterface(texture) || this._isImageCanvasInterface(texture) || this._isImageVideoInterface(texture); } _isImageBrowserInterface(texture) { return typeof ImageBitmap !== 'undefined' && texture instanceof ImageBitmap || typeof HTMLImageElement !== 'undefined' && texture instanceof HTMLImageElement; } _isImageCanvasInterface(texture) { return typeof HTMLCanvasElement !== 'undefined' && texture instanceof HTMLCanvasElement; } _isImageVideoInterface(texture) { return typeof HTMLVideoElement !== 'undefined' && texture instanceof HTMLVideoElement; } resizeCanvas(width, height) { const pixelRatio = Math.min(this._maxPixelRatio, platform.browser ? window.devicePixelRatio : 1); const w = Math.floor(width * pixelRatio); const h = Math.floor(height * pixelRatio); if (w !== this.canvas.width || h !== this.canvas.height) { this.setResolution(w, h); } } setResolution(width, height) { this.canvas.width = width; this.canvas.height = height; this.fire(GraphicsDevice.EVENT_RESIZE, width, height); } update() { this.updateClientRect(); } updateClientRect() { if (platform.worker) { this.clientRect.width = this.canvas.width; this.clientRect.height = this.canvas.height; } else { const rect = this.canvas.getBoundingClientRect(); this.clientRect.width = rect.width; this.clientRect.height = rect.height; } } get width() { return this.canvas.width; } get height() { return this.canvas.height; } set fullscreen(fullscreen) {} get fullscreen() { return false; } set maxPixelRatio(ratio) { this._maxPixelRatio = ratio; } get maxPixelRatio() { return this._maxPixelRatio; } get deviceType() { return this._deviceType; } startRenderPass(renderPass) {} endRenderPass(renderPass) {} startComputePass(name) {} endComputePass() {} frameStart() { this.renderPassIndex = 0; this.renderVersion++; } frameEnd() { this.mapsToClear.forEach((map)=>map.clear()); this.mapsToClear.clear(); } computeDispatch(computes, name = 'Unnamed') {} getRenderableHdrFormat(formats = [ PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F ], filterable = true, samples = 1) { for(let i = 0; i < formats.length; i++){ const format = formats[i]; switch(format){ case PIXELFORMAT_111110F: { if (this.textureRG11B10Renderable) { return format; } break; } case PIXELFORMAT_RGBA16F: if (this.textureHalfFloatRenderable) { return format; } break; case PIXELFORMAT_RGBA32F: if (this.isWebGPU && samples > 1) { continue; } if (this.textureFloatRenderable && (!filterable || this.textureFloatFilterable)) { return format; } break; } } return undefined; } validateAttributes(shader, vb0Format, vb1Format) {} constructor(canvas, options){ var _this_initOptions, _this_initOptions1, _this_initOptions2, _this_initOptions3, _this_initOptions4, _this_initOptions5; super(), this.backBuffer = null, this.backBufferSize = new Vec2(), this.backBufferAntialias = false, this.isWebGPU = false, this.isWebGL2 = false, this.isNull = false, this.isHdr = false, this.maxIndirectDrawCount = 1024, this.maxIndirectDispatchCount = 256, this.maxColorAttachments = 1, this.maxSamples = 1, this.supportsMultiDraw = true, this.supportsCompute = false, this.supportsStorageTextureRead = false, this.renderTarget = null, this.shaders = [], this.textures = new Set(), this.texturesToUpload = new Set(), this.targets = new Set(), this.renderVersion = 0, this.insideRenderPass = false, this.supportsUniformBuffers = false, this.supportsClipDistances = false, this.supportsPrimitiveIndex = false, this.textureRG11B10Renderable = false, this.textureFloatFilterable = false, this.blendState = new BlendState(), this.depthState = new DepthState(), this.stencilEnabled = false, this.stencilFront = new StencilParameters(), this.stencilBack = new StencilParameters(), this._destroyed = false, this.defaultClearOptions = { color: [ 0, 0, 0, 1 ], depth: 1, stencil: 0, flags: CLEARFLAG_COLOR | CLEARFLAG_DEPTH }, this.clientRect = { width: 0, height: 0 }, this._shadersDirty = false, this.capsDefines = new Map(), this.mapsToClear = new Set(); this.canvas = canvas; if ('setAttribute' in canvas) { canvas.setAttribute('data-engine', `PlayCanvas ${version$1}`); } this.initOptions = { ...options }; (_this_initOptions = this.initOptions).alpha ?? (_this_initOptions.alpha = true); (_this_initOptions1 = this.initOptions).depth ?? (_this_initOptions1.depth = true); (_this_initOptions2 = this.initOptions).stencil ?? (_this_initOptions2.stencil = true); (_this_initOptions3 = this.initOptions).antialias ?? (_this_initOptions3.antialias = true); (_this_initOptions4 = this.initOptions).powerPreference ?? (_this_initOptions4.powerPreference = 'high-performance'); (_this_initOptions5 = this.initOptions).displayFormat ?? (_this_initOptions5.displayFormat = DISPLAYFORMAT_LDR); this._maxPixelRatio = platform.browser ? Math.min(1, window.devicePixelRatio) : 1; this.buffers = new Set(); this._vram = { tex: 0, vb: 0, ib: 0, ub: 0, sb: 0 }; this._shaderStats = { vsCompiled: 0, fsCompiled: 0, linked: 0, materialShaders: 0, compileTime: 0 }; this.initializeContextCaches(); this._drawCallsPerFrame = 0; this._shaderSwitchesPerFrame = 0; this._primsPerFrame = []; for(let i = PRIMITIVE_POINTS; i <= PRIMITIVE_TRIFAN; i++){ this._primsPerFrame[i] = 0; } this._renderTargetCreationTime = 0; this.scope = new ScopeSpace('Device'); this.textureBias = this.scope.resolve('textureBias'); this.textureBias.setValue(0.0); } } GraphicsDevice.EVENT_RESIZE = 'resizecanvas'; let id$9 = 0; class RenderTarget { destroy() { const device = this._device; if (device) { device.targets.delete(this); if (device.renderTarget === this) { device.setRenderTarget(null); } this.destroyFrameBuffers(); } } destroyFrameBuffers() { const device = this._device; if (device) { this.impl.destroy(device); } } destroyTextureBuffers() { this._depthBuffer?.destroy(); this._depthBuffer = null; this._colorBuffers?.forEach((colorBuffer)=>{ colorBuffer.destroy(); }); this._colorBuffers = null; this._colorBuffer = null; } resize(width, height) { if (this.mipLevel > 0) { return; } this._depthBuffer?.resize(width, height); this._colorBuffers?.forEach((colorBuffer)=>{ colorBuffer.resize(width, height); }); if (this._width !== width || this._height !== height) { this.destroyFrameBuffers(); const device = this._device; if (device.renderTarget === this) { device.setRenderTarget(null); } this.evaluateDimensions(); this.validateMrt(); this.impl = device.createRenderTargetImpl(this); } } validateMrt() {} evaluateDimensions() { const buffer = this._colorBuffer ?? this._depthBuffer; if (buffer) { this._width = buffer.width; this._height = buffer.height; if (this._mipLevel > 0) { this._width = TextureUtils.calcLevelDimension(this._width, this._mipLevel); this._height = TextureUtils.calcLevelDimension(this._height, this._mipLevel); } } } init() { this.impl.init(this._device, this); } get initialized() { return this.impl.initialized; } get device() { return this._device; } loseContext() { this.impl.loseContext(); } resolve(color = true, depth = !!this._depthBuffer) { if (this._device && this._samples > 1) { this.impl.resolve(this._device, this, color, depth); } } copy(source, color, depth) { if (!this._device) { if (source._device) { this._device = source._device; } else { return false; } } const success = this._device.copyRenderTarget(source, this, color, depth); return success; } get samples() { return this._samples; } get depth() { return this._depth; } get stencil() { return this._stencil; } get colorBuffer() { return this._colorBuffer; } getColorBuffer(index) { return this._colorBuffers?.[index]; } get depthBuffer() { return this._depthBuffer; } get face() { return this._face; } get mipLevel() { return this._mipLevel; } get mipmaps() { return this._mipmaps; } get width() { return this._width ?? this._device.width; } get height() { return this._height ?? this._device.height; } isColorBufferSrgb(index = 0) { if (this.device.backBuffer === this) { return isSrgbPixelFormat(this.device.backBufferFormat); } const colorBuffer = this.getColorBuffer(index); return colorBuffer ? isSrgbPixelFormat(colorBuffer.format) : false; } constructor(options = {}){ this.id = id$9++; const device = options.colorBuffer?.device ?? options.colorBuffers?.[0].device ?? options.depthBuffer?.device ?? options.graphicsDevice; this._device = device; const { maxSamples } = this._device; this._samples = Math.min(options.samples ?? 1, maxSamples); if (device.isWebGPU) { this._samples = this._samples > 1 ? maxSamples : 1; } this._colorBuffer = options.colorBuffer; if (options.colorBuffer) { this._colorBuffers = [ options.colorBuffer ]; } this._depthBuffer = options.depthBuffer; this._face = options.face ?? 0; if (this._depthBuffer) { const format = this._depthBuffer._format; if (format === PIXELFORMAT_DEPTH || format === PIXELFORMAT_DEPTH16) { this._depth = true; this._stencil = false; } else if (format === PIXELFORMAT_DEPTHSTENCIL) { this._depth = true; this._stencil = true; } else if (format === PIXELFORMAT_R32F && this._depthBuffer.device.isWebGPU && this._samples > 1) { this._depth = true; this._stencil = false; } else { this._depth = false; this._stencil = false; } } else { this._depth = options.depth ?? true; this._stencil = options.stencil ?? false; } if (options.colorBuffers) { if (!this._colorBuffers) { this._colorBuffers = [ ...options.colorBuffers ]; this._colorBuffer = options.colorBuffers[0]; } } this.autoResolve = options.autoResolve ?? true; this.name = options.name; if (!this.name) { this.name = this._colorBuffer?.name; } if (!this.name) { this.name = this._depthBuffer?.name; } if (!this.name) { this.name = 'Untitled'; } this.flipY = options.flipY ?? false; this._mipLevel = options.mipLevel ?? 0; if (this._mipLevel > 0 && this._depth) { this._mipLevel = 0; } this._mipmaps = options.mipLevel === undefined; this.evaluateDimensions(); this.validateMrt(); this.impl = device.createRenderTargetImpl(this); } } class WebgpuBindGroup { update(bindGroup) { this.destroy(); const device = bindGroup.device; const desc = this.createDescriptor(device, bindGroup); this.bindGroup = device.wgpu.createBindGroup(desc); } destroy() { this.bindGroup = null; } createDescriptor(device, bindGroup) { const entries = []; const format = bindGroup.format; const uniformBufferFormats = bindGroup.format.uniformBufferFormats; bindGroup.uniformBuffers.forEach((ub, i)=>{ const slot = uniformBufferFormats[i].slot; const buffer = ub.persistent ? ub.impl.buffer : ub.allocation.gpuBuffer.buffer; entries.push({ binding: slot, resource: { buffer: buffer, offset: 0, size: ub.format.byteSize } }); }); const textureFormats = bindGroup.format.textureFormats; bindGroup.textures.forEach((value, textureIndex)=>{ const isTextureView = value instanceof TextureView; const texture = isTextureView ? value.texture : value; const wgpuTexture = texture.impl; const textureFormat = format.textureFormats[textureIndex]; const slot = textureFormats[textureIndex].slot; const view = wgpuTexture.getView(device, isTextureView ? value : undefined); entries.push({ binding: slot, resource: view }); if (textureFormat.hasSampler) { const sampler = wgpuTexture.getSampler(device, textureFormat.sampleType); entries.push({ binding: slot + 1, resource: sampler }); } }); const storageTextureFormats = bindGroup.format.storageTextureFormats; bindGroup.storageTextures.forEach((value, textureIndex)=>{ const isTextureView = value instanceof TextureView; const texture = isTextureView ? value.texture : value; const wgpuTexture = texture.impl; const slot = storageTextureFormats[textureIndex].slot; const view = wgpuTexture.getView(device, isTextureView ? value : undefined); entries.push({ binding: slot, resource: view }); }); const storageBufferFormats = bindGroup.format.storageBufferFormats; bindGroup.storageBuffers.forEach((buffer, bufferIndex)=>{ const wgpuBuffer = buffer.impl.buffer; const slot = storageBufferFormats[bufferIndex].slot; entries.push({ binding: slot, resource: { buffer: wgpuBuffer } }); }); const desc = { layout: bindGroup.format.impl.bindGroupLayout, entries: entries }; return desc; } } class WebgpuUtils { static shaderStage(stage) { let ret = 0; if (stage & SHADERSTAGE_VERTEX) ret |= GPUShaderStage.VERTEX; if (stage & SHADERSTAGE_FRAGMENT) ret |= GPUShaderStage.FRAGMENT; if (stage & SHADERSTAGE_COMPUTE) ret |= GPUShaderStage.COMPUTE; return ret; } } const gpuTextureFormats = []; gpuTextureFormats[PIXELFORMAT_A8] = ''; gpuTextureFormats[PIXELFORMAT_L8] = ''; gpuTextureFormats[PIXELFORMAT_LA8] = ''; gpuTextureFormats[PIXELFORMAT_R8] = 'r8unorm'; gpuTextureFormats[PIXELFORMAT_RG8] = 'rg8unorm'; gpuTextureFormats[PIXELFORMAT_RGB565] = ''; gpuTextureFormats[PIXELFORMAT_RGBA5551] = ''; gpuTextureFormats[PIXELFORMAT_RGBA4] = ''; gpuTextureFormats[PIXELFORMAT_RGB8] = 'rgba8unorm'; gpuTextureFormats[PIXELFORMAT_RGBA8] = 'rgba8unorm'; gpuTextureFormats[PIXELFORMAT_DXT1] = 'bc1-rgba-unorm'; gpuTextureFormats[PIXELFORMAT_DXT3] = 'bc2-rgba-unorm'; gpuTextureFormats[PIXELFORMAT_DXT5] = 'bc3-rgba-unorm'; gpuTextureFormats[PIXELFORMAT_RGB16F] = ''; gpuTextureFormats[PIXELFORMAT_RGBA16F] = 'rgba16float'; gpuTextureFormats[PIXELFORMAT_R16F] = 'r16float'; gpuTextureFormats[PIXELFORMAT_RG16F] = 'rg16float'; gpuTextureFormats[PIXELFORMAT_RGB32F] = ''; gpuTextureFormats[PIXELFORMAT_RGBA32F] = 'rgba32float'; gpuTextureFormats[PIXELFORMAT_R32F] = 'r32float'; gpuTextureFormats[PIXELFORMAT_RG32F] = 'rg32float'; gpuTextureFormats[PIXELFORMAT_DEPTH] = 'depth32float'; gpuTextureFormats[PIXELFORMAT_DEPTH16] = 'depth16unorm'; gpuTextureFormats[PIXELFORMAT_DEPTHSTENCIL] = 'depth24plus-stencil8'; gpuTextureFormats[PIXELFORMAT_111110F] = 'rg11b10ufloat'; gpuTextureFormats[PIXELFORMAT_SRGB8] = ''; gpuTextureFormats[PIXELFORMAT_SRGBA8] = 'rgba8unorm-srgb'; gpuTextureFormats[PIXELFORMAT_ETC1] = ''; gpuTextureFormats[PIXELFORMAT_ETC2_RGB] = 'etc2-rgb8unorm'; gpuTextureFormats[PIXELFORMAT_ETC2_RGBA] = 'etc2-rgba8unorm'; gpuTextureFormats[PIXELFORMAT_PVRTC_2BPP_RGB_1] = ''; gpuTextureFormats[PIXELFORMAT_PVRTC_2BPP_RGBA_1] = ''; gpuTextureFormats[PIXELFORMAT_PVRTC_4BPP_RGB_1] = ''; gpuTextureFormats[PIXELFORMAT_PVRTC_4BPP_RGBA_1] = ''; gpuTextureFormats[PIXELFORMAT_ASTC_4x4] = 'astc-4x4-unorm'; gpuTextureFormats[PIXELFORMAT_ATC_RGB] = ''; gpuTextureFormats[PIXELFORMAT_ATC_RGBA] = ''; gpuTextureFormats[PIXELFORMAT_BGRA8] = 'bgra8unorm'; gpuTextureFormats[PIXELFORMAT_SBGRA8] = 'bgra8unorm-srgb'; gpuTextureFormats[PIXELFORMAT_R8I] = 'r8sint'; gpuTextureFormats[PIXELFORMAT_R8U] = 'r8uint'; gpuTextureFormats[PIXELFORMAT_R16I] = 'r16sint'; gpuTextureFormats[PIXELFORMAT_R16U] = 'r16uint'; gpuTextureFormats[PIXELFORMAT_R32I] = 'r32sint'; gpuTextureFormats[PIXELFORMAT_R32U] = 'r32uint'; gpuTextureFormats[PIXELFORMAT_RG8I] = 'rg8sint'; gpuTextureFormats[PIXELFORMAT_RG8U] = 'rg8uint'; gpuTextureFormats[PIXELFORMAT_RG16I] = 'rg16sint'; gpuTextureFormats[PIXELFORMAT_RG16U] = 'rg16uint'; gpuTextureFormats[PIXELFORMAT_RG32I] = 'rg32sint'; gpuTextureFormats[PIXELFORMAT_RG32U] = 'rg32uint'; gpuTextureFormats[PIXELFORMAT_RGBA8I] = 'rgba8sint'; gpuTextureFormats[PIXELFORMAT_RGBA8U] = 'rgba8uint'; gpuTextureFormats[PIXELFORMAT_RGBA16I] = 'rgba16sint'; gpuTextureFormats[PIXELFORMAT_RGBA16U] = 'rgba16uint'; gpuTextureFormats[PIXELFORMAT_RGBA32I] = 'rgba32sint'; gpuTextureFormats[PIXELFORMAT_RGBA32U] = 'rgba32uint'; gpuTextureFormats[PIXELFORMAT_BC6F] = 'bc6h-rgb-float'; gpuTextureFormats[PIXELFORMAT_BC6UF] = 'bc6h-rgb-ufloat'; gpuTextureFormats[PIXELFORMAT_BC7] = 'bc7-rgba-unorm'; gpuTextureFormats[PIXELFORMAT_RGB9E5] = 'rgb9e5ufloat'; gpuTextureFormats[PIXELFORMAT_RG8S] = 'rg8snorm'; gpuTextureFormats[PIXELFORMAT_RGBA8S] = 'rgba8snorm'; gpuTextureFormats[PIXELFORMAT_RGB10A2] = 'rgb10a2unorm'; gpuTextureFormats[PIXELFORMAT_RGB10A2U] = 'rgb10a2uint'; gpuTextureFormats[PIXELFORMAT_DXT1_SRGB] = 'bc1-rgba-unorm-srgb'; gpuTextureFormats[PIXELFORMAT_DXT3_SRGBA] = 'bc2-rgba-unorm-srgb'; gpuTextureFormats[PIXELFORMAT_DXT5_SRGBA] = 'bc3-rgba-unorm-srgb'; gpuTextureFormats[PIXELFORMAT_ETC2_SRGB] = 'etc2-rgb8unorm-srgb'; gpuTextureFormats[PIXELFORMAT_ETC2_SRGBA] = 'etc2-rgba8unorm-srgb'; gpuTextureFormats[PIXELFORMAT_BC7_SRGBA] = 'bc7-rgba-unorm-srgb'; gpuTextureFormats[PIXELFORMAT_ASTC_4x4_SRGB] = 'astc-4x4-unorm-srgb'; const samplerTypes = []; samplerTypes[SAMPLETYPE_FLOAT] = 'filtering'; samplerTypes[SAMPLETYPE_UNFILTERABLE_FLOAT] = 'non-filtering'; samplerTypes[SAMPLETYPE_DEPTH] = 'comparison'; samplerTypes[SAMPLETYPE_INT] = 'comparison'; samplerTypes[SAMPLETYPE_UINT] = 'comparison'; const sampleTypes = []; sampleTypes[SAMPLETYPE_FLOAT] = 'float'; sampleTypes[SAMPLETYPE_UNFILTERABLE_FLOAT] = 'unfilterable-float'; sampleTypes[SAMPLETYPE_DEPTH] = 'depth'; sampleTypes[SAMPLETYPE_INT] = 'sint'; sampleTypes[SAMPLETYPE_UINT] = 'uint'; const stringIds$1 = new StringIds(); class WebgpuBindGroupFormat { destroy() { this.bindGroupLayout = null; } loseContext() {} createDescriptor(bindGroupFormat) { const entries = []; let key = ''; bindGroupFormat.uniformBufferFormats.forEach((bufferFormat)=>{ const visibility = WebgpuUtils.shaderStage(bufferFormat.visibility); key += `#${bufferFormat.slot}U:${visibility}`; entries.push({ binding: bufferFormat.slot, visibility: visibility, buffer: { type: 'uniform', hasDynamicOffset: true } }); }); bindGroupFormat.textureFormats.forEach((textureFormat)=>{ const visibility = WebgpuUtils.shaderStage(textureFormat.visibility); const sampleType = textureFormat.sampleType; const viewDimension = textureFormat.textureDimension; const multisampled = false; const gpuSampleType = sampleTypes[sampleType]; key += `#${textureFormat.slot}T:${visibility}-${gpuSampleType}-${viewDimension}-${multisampled}`; entries.push({ binding: textureFormat.slot, visibility: visibility, texture: { sampleType: gpuSampleType, viewDimension: viewDimension, multisampled: multisampled } }); if (textureFormat.hasSampler) { const gpuSamplerType = samplerTypes[sampleType]; key += `#${textureFormat.slot + 1}S:${visibility}-${gpuSamplerType}`; entries.push({ binding: textureFormat.slot + 1, visibility: visibility, sampler: { type: gpuSamplerType } }); } }); bindGroupFormat.storageTextureFormats.forEach((textureFormat)=>{ const { format, textureDimension } = textureFormat; const { read, write } = textureFormat; key += `#${textureFormat.slot}ST:${format}-${textureDimension}-${read ? 'r1' : 'r0'}-${write ? 'w1' : 'w0'}`; entries.push({ binding: textureFormat.slot, visibility: GPUShaderStage.COMPUTE, storageTexture: { access: read ? write ? 'read-write' : 'read-only' : 'write-only', format: gpuTextureFormats[format], viewDimension: textureDimension } }); }); bindGroupFormat.storageBufferFormats.forEach((bufferFormat)=>{ const readOnly = bufferFormat.readOnly; const visibility = WebgpuUtils.shaderStage(bufferFormat.visibility); key += `#${bufferFormat.slot}SB:${visibility}-${readOnly ? 'ro' : 'rw'}`; entries.push({ binding: bufferFormat.slot, visibility: visibility, buffer: { type: readOnly ? 'read-only-storage' : 'storage' } }); }); const desc = { entries: entries }; return { key, desc }; } constructor(bindGroupFormat){ const device = bindGroupFormat.device; const { key, desc } = this.createDescriptor(bindGroupFormat); this.key = stringIds$1.get(key); this.bindGroupLayout = device.wgpu.createBindGroupLayout(desc); } } class WebgpuBuffer { destroy(device) { if (this.buffer) { this.buffer.destroy(); this.buffer = null; } } get initialized() { return !!this.buffer; } loseContext() {} allocate(device, size) { this.buffer = device.wgpu.createBuffer({ size, usage: this.usageFlags }); } unlock(device, storage) { const wgpu = device.wgpu; if (!this.buffer) { const size = storage.byteLength + 3 & -4; this.usageFlags |= GPUBufferUsage.COPY_DST; this.allocate(device, size); } const srcOffset = storage.byteOffset ?? 0; const srcData = new Uint8Array(storage.buffer ?? storage, srcOffset, storage.byteLength); const data = new Uint8Array(this.buffer.size); data.set(srcData); wgpu.queue.writeBuffer(this.buffer, 0, data, 0, data.length); } read(device, offset, size, data, immediate) { return device.readStorageBuffer(this, offset, size, data, immediate); } write(device, bufferOffset, data, dataOffset, size) { device.writeStorageBuffer(this, bufferOffset, data, dataOffset, size); } clear(device, offset, size) { device.clearStorageBuffer(this, offset, size); } constructor(usageFlags = 0){ this.buffer = null; this.usageFlags = 0; this.usageFlags = usageFlags; } } class WebgpuIndexBuffer extends WebgpuBuffer { unlock(indexBuffer) { const device = indexBuffer.device; super.unlock(device, indexBuffer.storage); } constructor(indexBuffer, options){ super(BUFFERUSAGE_INDEX | (options?.storage ? BUFFERUSAGE_STORAGE : 0)), this.format = null; this.format = indexBuffer.format === INDEXFORMAT_UINT16 ? 'uint16' : 'uint32'; } } const array$1 = { equals (arr1, arr2) { if (arr1.length !== arr2.length) { return false; } for(let i = 0; i < arr1.length; i++){ if (arr1[i] !== arr2[i]) { return false; } } return true; } }; const gpuVertexFormats = []; gpuVertexFormats[TYPE_INT8] = 'sint8'; gpuVertexFormats[TYPE_UINT8] = 'uint8'; gpuVertexFormats[TYPE_INT16] = 'sint16'; gpuVertexFormats[TYPE_UINT16] = 'uint16'; gpuVertexFormats[TYPE_INT32] = 'sint32'; gpuVertexFormats[TYPE_UINT32] = 'uint32'; gpuVertexFormats[TYPE_FLOAT32] = 'float32'; gpuVertexFormats[TYPE_FLOAT16] = 'float16'; const gpuVertexFormatsNormalized = []; gpuVertexFormatsNormalized[TYPE_INT8] = 'snorm8'; gpuVertexFormatsNormalized[TYPE_UINT8] = 'unorm8'; gpuVertexFormatsNormalized[TYPE_INT16] = 'snorm16'; gpuVertexFormatsNormalized[TYPE_UINT16] = 'unorm16'; gpuVertexFormatsNormalized[TYPE_INT32] = 'sint32'; gpuVertexFormatsNormalized[TYPE_UINT32] = 'uint32'; gpuVertexFormatsNormalized[TYPE_FLOAT32] = 'float32'; gpuVertexFormatsNormalized[TYPE_FLOAT16] = 'float16'; class WebgpuVertexBufferLayout { get(vertexFormat0, vertexFormat1 = null) { const key = this.getKey(vertexFormat0, vertexFormat1); let layout = this.cache.get(key); if (!layout) { layout = this.create(vertexFormat0, vertexFormat1); this.cache.set(key, layout); } return layout; } getKey(vertexFormat0, vertexFormat1 = null) { return `${vertexFormat0?.renderingHashString}-${vertexFormat1?.renderingHashString}`; } create(vertexFormat0, vertexFormat1) { const layout = []; const addFormat = (format)=>{ const interleaved = format.interleaved; const stepMode = format.instancing ? 'instance' : 'vertex'; let attributes = []; const elementCount = format.elements.length; for(let i = 0; i < elementCount; i++){ const element = format.elements[i]; const location = semanticToLocation[element.name]; const formatTable = element.normalize ? gpuVertexFormatsNormalized : gpuVertexFormats; attributes.push({ shaderLocation: location, offset: interleaved ? element.offset : 0, format: `${formatTable[element.dataType]}${element.numComponents > 1 ? `x${element.numComponents}` : ''}` }); if (!interleaved || i === elementCount - 1) { layout.push({ attributes: attributes, arrayStride: element.stride, stepMode: stepMode }); attributes = []; } } }; if (vertexFormat0) { addFormat(vertexFormat0); } if (vertexFormat1) { addFormat(vertexFormat1); } return layout; } constructor(){ this.cache = new Map(); } } class WebgpuPipeline { getPipelineLayout(bindGroupFormats) { const bindGroupLayouts = []; bindGroupFormats.forEach((format)=>{ bindGroupLayouts.push(format.bindGroupLayout); }); const desc = { bindGroupLayouts: bindGroupLayouts }; const pipelineLayout = this.device.wgpu.createPipelineLayout(desc); return pipelineLayout; } constructor(device){ this.device = device; } } const _primitiveTopology = [ 'point-list', 'line-list', undefined, 'line-strip', 'triangle-list', 'triangle-strip', undefined ]; const _blendOperation = [ 'add', 'subtract', 'reverse-subtract', 'min', 'max' ]; const _blendFactor = [ 'zero', 'one', 'src', 'one-minus-src', 'dst', 'one-minus-dst', 'src-alpha', 'src-alpha-saturated', 'one-minus-src-alpha', 'dst-alpha', 'one-minus-dst-alpha', 'constant', 'one-minus-constant' ]; const _compareFunction = [ 'never', 'less', 'equal', 'less-equal', 'greater', 'not-equal', 'greater-equal', 'always' ]; const _cullModes = [ 'none', 'back', 'front' ]; const _stencilOps = [ 'keep', 'zero', 'replace', 'increment-clamp', 'increment-wrap', 'decrement-clamp', 'decrement-wrap', 'invert' ]; const _indexFormat = [ '', 'uint16', 'uint32' ]; let CacheEntry$1 = class CacheEntry { }; class WebgpuRenderPipeline extends WebgpuPipeline { get(primitive, vertexFormat0, vertexFormat1, ibFormat, shader, renderTarget, bindGroupFormats, blendState, depthState, cullMode, stencilEnabled, stencilFront, stencilBack) { const primitiveType = primitive.type; if (ibFormat && primitiveType !== PRIMITIVE_LINESTRIP && primitiveType !== PRIMITIVE_TRISTRIP) { ibFormat = undefined; } const lookupHashes = this.lookupHashes; lookupHashes[0] = primitiveType; lookupHashes[1] = shader.id; lookupHashes[2] = cullMode; lookupHashes[3] = depthState.key; lookupHashes[4] = blendState.key; lookupHashes[5] = vertexFormat0?.renderingHash ?? 0; lookupHashes[6] = vertexFormat1?.renderingHash ?? 0; lookupHashes[7] = renderTarget.impl.key; lookupHashes[8] = bindGroupFormats[0]?.key ?? 0; lookupHashes[9] = bindGroupFormats[1]?.key ?? 0; lookupHashes[10] = bindGroupFormats[2]?.key ?? 0; lookupHashes[11] = stencilEnabled ? stencilFront.key : 0; lookupHashes[12] = stencilEnabled ? stencilBack.key : 0; lookupHashes[13] = ibFormat ?? 0; const hash = hash32Fnv1a(lookupHashes); let cacheEntries = this.cache.get(hash); if (cacheEntries) { for(let i = 0; i < cacheEntries.length; i++){ const entry = cacheEntries[i]; if (array$1.equals(entry.hashes, lookupHashes)) { return entry.pipeline; } } } const primitiveTopology = _primitiveTopology[primitiveType]; const pipelineLayout = this.getPipelineLayout(bindGroupFormats); const vertexBufferLayout = this.vertexBufferLayout.get(vertexFormat0, vertexFormat1); const cacheEntry = new CacheEntry$1(); cacheEntry.hashes = new Uint32Array(lookupHashes); cacheEntry.pipeline = this.create(primitiveTopology, ibFormat, shader, renderTarget, pipelineLayout, blendState, depthState, vertexBufferLayout, cullMode, stencilEnabled, stencilFront, stencilBack); if (cacheEntries) { cacheEntries.push(cacheEntry); } else { cacheEntries = [ cacheEntry ]; } this.cache.set(hash, cacheEntries); return cacheEntry.pipeline; } getBlend(blendState) { let blend; if (blendState.blend) { blend = { color: { operation: _blendOperation[blendState.colorOp], srcFactor: _blendFactor[blendState.colorSrcFactor], dstFactor: _blendFactor[blendState.colorDstFactor] }, alpha: { operation: _blendOperation[blendState.alphaOp], srcFactor: _blendFactor[blendState.alphaSrcFactor], dstFactor: _blendFactor[blendState.alphaDstFactor] } }; } return blend; } getDepthStencil(depthState, renderTarget, stencilEnabled, stencilFront, stencilBack, primitiveTopology) { let depthStencil; const { depth, stencil } = renderTarget; if (depth || stencil) { depthStencil = { format: renderTarget.impl.depthAttachment.format }; if (depth) { depthStencil.depthWriteEnabled = depthState.write; depthStencil.depthCompare = _compareFunction[depthState.func]; const biasAllowed = primitiveTopology === 'triangle-list' || primitiveTopology === 'triangle-strip'; depthStencil.depthBias = biasAllowed ? depthState.depthBias : 0; depthStencil.depthBiasSlopeScale = biasAllowed ? depthState.depthBiasSlope : 0; } else { depthStencil.depthWriteEnabled = false; depthStencil.depthCompare = 'always'; } if (stencil && stencilEnabled) { depthStencil.stencilReadMas = stencilFront.readMask; depthStencil.stencilWriteMask = stencilFront.writeMask; depthStencil.stencilFront = { compare: _compareFunction[stencilFront.func], failOp: _stencilOps[stencilFront.fail], passOp: _stencilOps[stencilFront.zpass], depthFailOp: _stencilOps[stencilFront.zfail] }; depthStencil.stencilBack = { compare: _compareFunction[stencilBack.func], failOp: _stencilOps[stencilBack.fail], passOp: _stencilOps[stencilBack.zpass], depthFailOp: _stencilOps[stencilBack.zfail] }; } } return depthStencil; } create(primitiveTopology, ibFormat, shader, renderTarget, pipelineLayout, blendState, depthState, vertexBufferLayout, cullMode, stencilEnabled, stencilFront, stencilBack) { const wgpu = this.device.wgpu; const webgpuShader = shader.impl; const desc = { vertex: { module: webgpuShader.getVertexShaderModule(), entryPoint: webgpuShader.vertexEntryPoint, buffers: vertexBufferLayout }, primitive: { topology: primitiveTopology, frontFace: 'ccw', cullMode: _cullModes[cullMode] }, depthStencil: this.getDepthStencil(depthState, renderTarget, stencilEnabled, stencilFront, stencilBack, primitiveTopology), multisample: { count: renderTarget.samples }, layout: pipelineLayout }; if (ibFormat) { desc.primitive.stripIndexFormat = _indexFormat[ibFormat]; } desc.fragment = { module: webgpuShader.getFragmentShaderModule(), entryPoint: webgpuShader.fragmentEntryPoint, targets: [] }; const colorAttachments = renderTarget.impl.colorAttachments; if (colorAttachments.length > 0) { let writeMask = 0; if (blendState.redWrite) writeMask |= GPUColorWrite.RED; if (blendState.greenWrite) writeMask |= GPUColorWrite.GREEN; if (blendState.blueWrite) writeMask |= GPUColorWrite.BLUE; if (blendState.alphaWrite) writeMask |= GPUColorWrite.ALPHA; const blend = this.getBlend(blendState); colorAttachments.forEach((attachment)=>{ desc.fragment.targets.push({ format: attachment.format, writeMask: writeMask, blend: blend }); }); } const pipeline = wgpu.createRenderPipeline(desc); return pipeline; } constructor(device){ super(device), this.lookupHashes = new Uint32Array(14); this.vertexBufferLayout = new WebgpuVertexBufferLayout(); this.cache = new Map(); } } class CacheEntry { constructor(){ this.pipeline = null; this.hashes = null; } } class WebgpuComputePipeline extends WebgpuPipeline { get(shader, bindGroupFormat) { const lookupHashes = this.lookupHashes; lookupHashes[0] = shader.impl.computeKey; lookupHashes[1] = bindGroupFormat.impl.key; const hash = hash32Fnv1a(lookupHashes); let cacheEntries = this.cache.get(hash); if (cacheEntries) { for(let i = 0; i < cacheEntries.length; i++){ const entry = cacheEntries[i]; if (array$1.equals(entry.hashes, lookupHashes)) { return entry.pipeline; } } } const pipelineLayout = this.getPipelineLayout([ bindGroupFormat.impl ]); const cacheEntry = new CacheEntry(); cacheEntry.hashes = new Uint32Array(lookupHashes); cacheEntry.pipeline = this.create(shader, pipelineLayout); if (cacheEntries) { cacheEntries.push(cacheEntry); } else { cacheEntries = [ cacheEntry ]; } this.cache.set(hash, cacheEntries); return cacheEntry.pipeline; } create(shader, pipelineLayout) { const wgpu = this.device.wgpu; const webgpuShader = shader.impl; const desc = { compute: { module: webgpuShader.getComputeShaderModule(), entryPoint: webgpuShader.computeEntryPoint }, layout: pipelineLayout }; const pipeline = wgpu.createComputePipeline(desc); return pipeline; } constructor(...args){ super(...args), this.lookupHashes = new Uint32Array(2), this.cache = new Map(); } } class RefCountedObject { incRefCount() { this._refCount++; } decRefCount() { this._refCount--; } get refCount() { return this._refCount; } constructor(){ this._refCount = 0; } } class Entry extends RefCountedObject { constructor(obj){ super(); this.object = obj; this.incRefCount(); } } class RefCountedKeyCache { destroy() { this.cache.forEach((entry)=>{ entry.object?.destroy(); }); this.cache.clear(); } clear() { this.cache.clear(); } get(key) { const entry = this.cache.get(key); if (entry) { entry.incRefCount(); return entry.object; } return null; } set(key, object) { this.cache.set(key, new Entry(object)); } release(key) { const entry = this.cache.get(key); if (entry) { entry.decRefCount(); if (entry.refCount === 0) { this.cache.delete(key); entry.object?.destroy(); } } } constructor(){ this.cache = new Map(); } } class MultisampledTextureCache extends RefCountedKeyCache { loseContext(device) { this.clear(); } } const multisampledTextureCache = new DeviceCache(); const getMultisampledTextureCache = (device)=>{ return multisampledTextureCache.get(device, ()=>{ return new MultisampledTextureCache(); }); }; const stringIds = new StringIds(); class ColorAttachment { destroy() { this.multisampledBuffer?.destroy(); this.multisampledBuffer = null; } } class DepthAttachment { destroy(device) { if (this.depthTextureInternal) { this.depthTexture?.destroy(); this.depthTexture = null; } if (this.multisampledDepthBuffer) { this.multisampledDepthBuffer = null; getMultisampledTextureCache(device).release(this.multisampledDepthBufferKey); } } constructor(gpuFormat){ this.depthTexture = null; this.depthTextureInternal = false; this.multisampledDepthBuffer = null; this.format = gpuFormat; this.hasStencil = gpuFormat === 'depth24plus-stencil8'; } } class WebgpuRenderTarget { destroy(device) { this.initialized = false; this.assignedColorTexture = null; this.colorAttachments.forEach((colorAttachment)=>{ colorAttachment.destroy(); }); this.colorAttachments.length = 0; this.depthAttachment?.destroy(device); this.depthAttachment = null; } updateKey() { const rt = this.renderTarget; let key = `${rt.samples}:${this.depthAttachment ? this.depthAttachment.format : 'nodepth'}`; this.colorAttachments.forEach((colorAttachment)=>{ key += `:${colorAttachment.format}`; }); this.key = stringIds.get(key); } assignColorTexture(device, gpuTexture) { this.assignedColorTexture = gpuTexture; const view = gpuTexture.createView({ format: device.backBufferViewFormat }); const colorAttachment = this.renderPassDescriptor.colorAttachments[0]; const samples = this.renderTarget.samples; if (samples > 1) { colorAttachment.resolveTarget = view; } else { colorAttachment.view = view; } this.setColorAttachment(0, undefined, device.backBufferViewFormat); this.updateKey(); } setColorAttachment(index, multisampledBuffer, format) { if (!this.colorAttachments[index]) { this.colorAttachments[index] = new ColorAttachment(); } if (multisampledBuffer) { this.colorAttachments[index].multisampledBuffer = multisampledBuffer; } if (format) { this.colorAttachments[index].format = format; } } init(device, renderTarget) { const wgpu = device.wgpu; this.initDepthStencil(device, wgpu, renderTarget); if (renderTarget._colorBuffers) { renderTarget._colorBuffers.forEach((colorBuffer, index)=>{ this.setColorAttachment(index, undefined, colorBuffer.impl.format); }); } this.renderPassDescriptor.colorAttachments = []; const count = this.isBackbuffer ? 1 : renderTarget._colorBuffers?.length ?? 0; for(let i = 0; i < count; ++i){ const colorAttachment = this.initColor(device, wgpu, renderTarget, i); const isDefaultFramebuffer = i === 0 && this.colorAttachments[0]?.format; if (colorAttachment.view || isDefaultFramebuffer) { this.renderPassDescriptor.colorAttachments.push(colorAttachment); } } this.updateKey(); this.initialized = true; } initDepthStencil(device, wgpu, renderTarget) { const { samples, width, height, depth, depthBuffer } = renderTarget; if (depth || depthBuffer) { let renderingView; if (!depthBuffer) { this.depthAttachment = new DepthAttachment('depth24plus-stencil8'); const depthTextureDesc = { size: [ width, height, 1 ], dimension: '2d', sampleCount: samples, format: this.depthAttachment.format, usage: GPUTextureUsage.RENDER_ATTACHMENT }; if (samples > 1) { depthTextureDesc.usage |= GPUTextureUsage.TEXTURE_BINDING; } else { depthTextureDesc.usage |= GPUTextureUsage.COPY_SRC; } const depthTexture = wgpu.createTexture(depthTextureDesc); this.depthAttachment.depthTexture = depthTexture; this.depthAttachment.depthTextureInternal = true; renderingView = depthTexture.createView(); } else { this.depthAttachment = new DepthAttachment(depthBuffer.impl.format); if (samples > 1) { const depthFormat = 'depth24plus-stencil8'; this.depthAttachment.format = depthFormat; this.depthAttachment.hasStencil = depthFormat === 'depth24plus-stencil8'; const key = `${depthBuffer.id}:${width}:${height}:${samples}:${depthFormat}`; const msTextures = getMultisampledTextureCache(device); let msDepthTexture = msTextures.get(key); if (!msDepthTexture) { const multisampledDepthDesc = { size: [ width, height, 1 ], dimension: '2d', sampleCount: samples, format: depthFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT | (depthFormat !== depthBuffer.impl.format ? GPUTextureUsage.TEXTURE_BINDING : 0) }; msDepthTexture = wgpu.createTexture(multisampledDepthDesc); msTextures.set(key, msDepthTexture); } this.depthAttachment.multisampledDepthBuffer = msDepthTexture; this.depthAttachment.multisampledDepthBufferKey = key; renderingView = msDepthTexture.createView(); } else { const depthTexture = depthBuffer.impl.gpuTexture; this.depthAttachment.depthTexture = depthTexture; renderingView = depthTexture.createView(); } } this.renderPassDescriptor.depthStencilAttachment = { view: renderingView }; } } initColor(device, wgpu, renderTarget, index) { const colorAttachment = {}; const { samples, width, height, mipLevel } = renderTarget; const colorBuffer = renderTarget.getColorBuffer(index); let colorView = null; if (colorBuffer) { const mipLevelCount = 1; if (colorBuffer.cubemap) { colorView = colorBuffer.impl.createView({ dimension: '2d', baseArrayLayer: renderTarget.face, arrayLayerCount: 1, mipLevelCount, baseMipLevel: mipLevel }); } else { colorView = colorBuffer.impl.createView({ mipLevelCount, baseMipLevel: mipLevel }); } } if (samples > 1) { const format = this.isBackbuffer ? device.backBufferViewFormat : colorBuffer.impl.format; const multisampledTextureDesc = { size: [ width, height, 1 ], dimension: '2d', sampleCount: samples, format: format, usage: GPUTextureUsage.RENDER_ATTACHMENT }; const multisampledColorBuffer = wgpu.createTexture(multisampledTextureDesc); this.setColorAttachment(index, multisampledColorBuffer, multisampledTextureDesc.format); colorAttachment.view = multisampledColorBuffer.createView(); colorAttachment.resolveTarget = colorView; } else { colorAttachment.view = colorView; } return colorAttachment; } setupForRenderPass(renderPass, renderTarget) { const count = this.renderPassDescriptor.colorAttachments?.length ?? 0; for(let i = 0; i < count; ++i){ const colorAttachment = this.renderPassDescriptor.colorAttachments[i]; const colorOps = renderPass.colorArrayOps[i]; const srgb = renderTarget.isColorBufferSrgb(i); colorAttachment.clearValue = srgb ? colorOps.clearValueLinear : colorOps.clearValue; colorAttachment.loadOp = colorOps.clear ? 'clear' : 'load'; colorAttachment.storeOp = colorOps.store ? 'store' : 'discard'; } const depthAttachment = this.renderPassDescriptor.depthStencilAttachment; if (depthAttachment) { depthAttachment.depthClearValue = renderPass.depthStencilOps.clearDepthValue; depthAttachment.depthLoadOp = renderPass.depthStencilOps.clearDepth ? 'clear' : 'load'; depthAttachment.depthStoreOp = renderPass.depthStencilOps.storeDepth ? 'store' : 'discard'; depthAttachment.depthReadOnly = false; if (this.depthAttachment.hasStencil) { depthAttachment.stencilClearValue = renderPass.depthStencilOps.clearStencilValue; depthAttachment.stencilLoadOp = renderPass.depthStencilOps.clearStencil ? 'clear' : 'load'; depthAttachment.stencilStoreOp = renderPass.depthStencilOps.storeStencil ? 'store' : 'discard'; depthAttachment.stencilReadOnly = false; } } } loseContext() { this.initialized = false; } resolve(device, target, color, depth) {} constructor(renderTarget){ this.initialized = false; this.colorAttachments = []; this.depthAttachment = null; this.assignedColorTexture = null; this.renderPassDescriptor = {}; this.isBackbuffer = false; this.renderTarget = renderTarget; } } const uniformTypeToNumComponents = []; uniformTypeToNumComponents[UNIFORMTYPE_FLOAT] = 1; uniformTypeToNumComponents[UNIFORMTYPE_VEC2] = 2; uniformTypeToNumComponents[UNIFORMTYPE_VEC3] = 3; uniformTypeToNumComponents[UNIFORMTYPE_VEC4] = 4; uniformTypeToNumComponents[UNIFORMTYPE_INT] = 1; uniformTypeToNumComponents[UNIFORMTYPE_IVEC2] = 2; uniformTypeToNumComponents[UNIFORMTYPE_IVEC3] = 3; uniformTypeToNumComponents[UNIFORMTYPE_IVEC4] = 4; uniformTypeToNumComponents[UNIFORMTYPE_BOOL] = 1; uniformTypeToNumComponents[UNIFORMTYPE_BVEC2] = 2; uniformTypeToNumComponents[UNIFORMTYPE_BVEC3] = 3; uniformTypeToNumComponents[UNIFORMTYPE_BVEC4] = 4; uniformTypeToNumComponents[UNIFORMTYPE_MAT2] = 8; uniformTypeToNumComponents[UNIFORMTYPE_MAT3] = 12; uniformTypeToNumComponents[UNIFORMTYPE_MAT4] = 16; uniformTypeToNumComponents[UNIFORMTYPE_UINT] = 1; uniformTypeToNumComponents[UNIFORMTYPE_UVEC2] = 2; uniformTypeToNumComponents[UNIFORMTYPE_UVEC3] = 3; uniformTypeToNumComponents[UNIFORMTYPE_UVEC4] = 4; class UniformFormat { get isArrayType() { return this.count > 0; } calculateOffset(offset) { let alignment = this.byteSize <= 8 ? this.byteSize : 16; if (this.count) { alignment = 16; } offset = math.roundUp(offset, alignment); this.offset = offset / 4; } constructor(name, type, count = 0){ this.shortName = name; this.name = count ? `${name}[0]` : name; this.type = type; this.numComponents = uniformTypeToNumComponents[type]; this.updateType = type; if (count > 0) { switch(type){ case UNIFORMTYPE_FLOAT: this.updateType = UNIFORMTYPE_FLOATARRAY; break; case UNIFORMTYPE_INT: this.updateType = UNIFORMTYPE_INTARRAY; break; case UNIFORMTYPE_UINT: this.updateType = UNIFORMTYPE_UINTARRAY; break; case UNIFORMTYPE_BOOL: this.updateType = UNIFORMTYPE_BOOLARRAY; break; case UNIFORMTYPE_VEC2: this.updateType = UNIFORMTYPE_VEC2ARRAY; break; case UNIFORMTYPE_IVEC2: this.updateType = UNIFORMTYPE_IVEC2ARRAY; break; case UNIFORMTYPE_UVEC2: this.updateType = UNIFORMTYPE_UVEC2ARRAY; break; case UNIFORMTYPE_BVEC2: this.updateType = UNIFORMTYPE_BVEC2ARRAY; break; case UNIFORMTYPE_VEC3: this.updateType = UNIFORMTYPE_VEC3ARRAY; break; case UNIFORMTYPE_IVEC3: this.updateType = UNIFORMTYPE_IVEC3ARRAY; break; case UNIFORMTYPE_UVEC3: this.updateType = UNIFORMTYPE_UVEC3ARRAY; break; case UNIFORMTYPE_BVEC3: this.updateType = UNIFORMTYPE_BVEC3ARRAY; break; case UNIFORMTYPE_VEC4: this.updateType = UNIFORMTYPE_VEC4ARRAY; break; case UNIFORMTYPE_IVEC4: this.updateType = UNIFORMTYPE_IVEC4ARRAY; break; case UNIFORMTYPE_UVEC4: this.updateType = UNIFORMTYPE_UVEC4ARRAY; break; case UNIFORMTYPE_BVEC4: this.updateType = UNIFORMTYPE_BVEC4ARRAY; break; case UNIFORMTYPE_MAT4: this.updateType = UNIFORMTYPE_MAT4ARRAY; break; } } this.count = count; let componentSize = this.numComponents; if (count) { componentSize = math.roundUp(componentSize, 4); } this.byteSize = componentSize * 4; if (count) { this.byteSize *= count; } } } class UniformBufferFormat { get(name) { return this.map.get(name); } constructor(graphicsDevice, uniforms){ this.byteSize = 0; this.map = new Map(); this.scope = graphicsDevice.scope; this.uniforms = uniforms; let offset = 0; for(let i = 0; i < uniforms.length; i++){ const uniform = uniforms[i]; uniform.calculateOffset(offset); offset = uniform.offset * 4 + uniform.byteSize; uniform.scopeId = this.scope.resolve(uniform.name); this.map.set(uniform.name, uniform); } this.byteSize = math.roundUp(offset, 16); } } const KEYWORD$2 = /[ \t]*(\battribute\b|\bvarying\b|\buniform\b)/g; const KEYWORD_LINE$1 = /(\battribute\b|\bvarying\b|\bout\b|\buniform\b)[ \t]*([^;]+)(;+)/g; const MARKER$1 = '@@@'; const ARRAY_IDENTIFIER = /([\w-]+)\[(.*?)\]/; const precisionQualifiers = new Set([ 'highp', 'mediump', 'lowp' ]); const shadowSamplers = new Set([ 'sampler2DShadow', 'samplerCubeShadow', 'sampler2DArrayShadow' ]); const textureDimensions = { sampler2D: TEXTUREDIMENSION_2D, sampler3D: TEXTUREDIMENSION_3D, samplerCube: TEXTUREDIMENSION_CUBE, samplerCubeShadow: TEXTUREDIMENSION_CUBE, sampler2DShadow: TEXTUREDIMENSION_2D, sampler2DArray: TEXTUREDIMENSION_2D_ARRAY, sampler2DArrayShadow: TEXTUREDIMENSION_2D_ARRAY, isampler2D: TEXTUREDIMENSION_2D, usampler2D: TEXTUREDIMENSION_2D, isampler3D: TEXTUREDIMENSION_3D, usampler3D: TEXTUREDIMENSION_3D, isamplerCube: TEXTUREDIMENSION_CUBE, usamplerCube: TEXTUREDIMENSION_CUBE, isampler2DArray: TEXTUREDIMENSION_2D_ARRAY, usampler2DArray: TEXTUREDIMENSION_2D_ARRAY }; const textureDimensionInfo = { [TEXTUREDIMENSION_2D]: 'texture2D', [TEXTUREDIMENSION_CUBE]: 'textureCube', [TEXTUREDIMENSION_3D]: 'texture3D', [TEXTUREDIMENSION_2D_ARRAY]: 'texture2DArray' }; let UniformLine$1 = class UniformLine { constructor(line, shader){ this.line = line; const words = line.trim().split(/\s+/); if (precisionQualifiers.has(words[0])) { this.precision = words.shift(); } this.type = words.shift(); if (line.includes(',')) ; if (line.includes('[')) { const rest = words.join(' '); const match = ARRAY_IDENTIFIER.exec(rest); this.name = match[1]; this.arraySize = Number(match[2]); if (isNaN(this.arraySize)) { shader.failed = true; } } else { this.name = words.shift(); this.arraySize = 0; } this.isSampler = this.type.indexOf('sampler') !== -1; this.isSignedInt = this.type.indexOf('isampler') !== -1; this.isUnsignedInt = this.type.indexOf('usampler') !== -1; } }; class ShaderProcessorGLSL { static run(device, shaderDefinition, shader) { const varyingMap = new Map(); const vertexExtracted = ShaderProcessorGLSL.extract(shaderDefinition.vshader); const fragmentExtracted = ShaderProcessorGLSL.extract(shaderDefinition.fshader); const attributesMap = new Map(); const attributesBlock = ShaderProcessorGLSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions); const vertexVaryingsBlock = ShaderProcessorGLSL.processVaryings(vertexExtracted.varyings, varyingMap, true); const fragmentVaryingsBlock = ShaderProcessorGLSL.processVaryings(fragmentExtracted.varyings, varyingMap, false); const outBlock = ShaderProcessorGLSL.processOuts(fragmentExtracted.outs); const concatUniforms = vertexExtracted.uniforms.concat(fragmentExtracted.uniforms); const uniforms = Array.from(new Set(concatUniforms)); const parsedUniforms = uniforms.map((line)=>new UniformLine$1(line, shader)); const uniformsData = ShaderProcessorGLSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader); const vBlock = `${attributesBlock}\n${vertexVaryingsBlock}\n${uniformsData.code}`; const vshader = vertexExtracted.src.replace(MARKER$1, vBlock); const fBlock = `${fragmentVaryingsBlock}\n${outBlock}\n${uniformsData.code}`; const fshader = fragmentExtracted.src.replace(MARKER$1, fBlock); return { vshader: vshader, fshader: fshader, attributes: attributesMap, meshUniformBufferFormat: uniformsData.meshUniformBufferFormat, meshBindGroupFormat: uniformsData.meshBindGroupFormat }; } static extract(src) { const attributes = []; const varyings = []; const outs = []; const uniforms = []; let replacement = `${MARKER$1}\n`; let match; while((match = KEYWORD$2.exec(src)) !== null){ const keyword = match[1]; switch(keyword){ case 'attribute': case 'varying': case 'uniform': case 'out': { KEYWORD_LINE$1.lastIndex = match.index; const lineMatch = KEYWORD_LINE$1.exec(src); if (keyword === 'attribute') { attributes.push(lineMatch[2]); } else if (keyword === 'varying') { varyings.push(lineMatch[2]); } else if (keyword === 'out') { outs.push(lineMatch[2]); } else if (keyword === 'uniform') { uniforms.push(lineMatch[2]); } src = ShaderProcessorGLSL.cutOut(src, match.index, KEYWORD_LINE$1.lastIndex, replacement); KEYWORD$2.lastIndex = match.index + replacement.length; replacement = ''; break; } } } return { src, attributes, varyings, outs, uniforms }; } static processUniforms(device, uniforms, processingOptions, shader) { const uniformLinesSamplers = []; const uniformLinesNonSamplers = []; uniforms.forEach((uniform)=>{ if (uniform.isSampler) { uniformLinesSamplers.push(uniform); } else { uniformLinesNonSamplers.push(uniform); } }); const meshUniforms = []; uniformLinesNonSamplers.forEach((uniform)=>{ if (!processingOptions.hasUniform(uniform.name)) { const uniformType = uniformTypeToName.indexOf(uniform.type); const uniformFormat = new UniformFormat(uniform.name, uniformType, uniform.arraySize); meshUniforms.push(uniformFormat); } }); if (meshUniforms.length === 0) { meshUniforms.push(new UniformFormat(UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT)); } const meshUniformBufferFormat = meshUniforms.length ? new UniformBufferFormat(device, meshUniforms) : null; const textureFormats = []; uniformLinesSamplers.forEach((uniform)=>{ if (!processingOptions.hasTexture(uniform.name)) { let sampleType = SAMPLETYPE_FLOAT; if (uniform.isSignedInt) { sampleType = SAMPLETYPE_INT; } else if (uniform.isUnsignedInt) { sampleType = SAMPLETYPE_UINT; } else { if (uniform.precision === 'highp') { sampleType = SAMPLETYPE_UNFILTERABLE_FLOAT; } if (shadowSamplers.has(uniform.type)) { sampleType = SAMPLETYPE_DEPTH; } } const dimension = textureDimensions[uniform.type]; textureFormats.push(new BindTextureFormat(uniform.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType)); } }); const meshBindGroupFormat = new BindGroupFormat(device, textureFormats); let code = ''; processingOptions.uniformFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += ShaderProcessorGLSL.getUniformShaderDeclaration(format, bindGroupIndex, 0); } }); if (meshUniformBufferFormat) { code += ShaderProcessorGLSL.getUniformShaderDeclaration(meshUniformBufferFormat, BINDGROUP_MESH_UB, 0); } processingOptions.bindGroupFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += ShaderProcessorGLSL.getTexturesShaderDeclaration(format, bindGroupIndex); } }); code += ShaderProcessorGLSL.getTexturesShaderDeclaration(meshBindGroupFormat, BINDGROUP_MESH); return { code, meshUniformBufferFormat, meshBindGroupFormat }; } static processVaryings(varyingLines, varyingMap, isVertex) { let block = ''; const op = isVertex ? 'out' : 'in'; varyingLines.forEach((line, index)=>{ const words = ShaderProcessorGLSL.splitToWords(line); const type = words.slice(0, -1).join(' '); const name = words[words.length - 1]; if (isVertex) { varyingMap.set(name, index); } else { index = varyingMap.get(name); } block += `layout(location = ${index}) ${op} ${type} ${name};\n`; }); return block; } static processOuts(outsLines) { let block = ''; outsLines.forEach((line, index)=>{ block += `layout(location = ${index}) out ${line};\n`; }); return block; } static getTypeCount(type) { const lastChar = type.substring(type.length - 1); const num = parseInt(lastChar, 10); return isNaN(num) ? 1 : num; } static processAttributes(attributeLines, shaderDefinitionAttributes, attributesMap, processingOptions) { let block = ''; attributeLines.forEach((line)=>{ const words = ShaderProcessorGLSL.splitToWords(line); let type = words[0]; let name = words[1]; if (shaderDefinitionAttributes.hasOwnProperty(name)) { const semantic = shaderDefinitionAttributes[name]; const location = semanticToLocation[semantic]; attributesMap.set(location, name); let copyCode; const element = processingOptions.getVertexElement(semantic); if (element) { const dataType = element.dataType; if (dataType !== TYPE_FLOAT32 && dataType !== TYPE_FLOAT16 && !element.normalize && !element.asInt) { const attribNumElements = ShaderProcessorGLSL.getTypeCount(type); const newName = `_private_${name}`; copyCode = `vec${attribNumElements} ${name} = vec${attribNumElements}(${newName});\n`; name = newName; const isSignedType = dataType === TYPE_INT8 || dataType === TYPE_INT16 || dataType === TYPE_INT32; if (attribNumElements === 1) { type = isSignedType ? 'int' : 'uint'; } else { type = isSignedType ? `ivec${attribNumElements}` : `uvec${attribNumElements}`; } } } block += `layout(location = ${location}) in ${type} ${name};\n`; if (copyCode) { block += copyCode; } } }); return block; } static splitToWords(line) { line = line.replace(/\s+/g, ' ').trim(); return line.split(' '); } static cutOut(src, start, end, replacement) { return src.substring(0, start) + replacement + src.substring(end); } static getUniformShaderDeclaration(format, bindGroup, bindIndex) { const name = bindGroupNames[bindGroup]; let code = `layout(set = ${bindGroup}, binding = ${bindIndex}, std140) uniform ub_${name} {\n`; format.uniforms.forEach((uniform)=>{ const typeString = uniformTypeToName[uniform.type]; code += ` ${typeString} ${uniform.shortName}${uniform.count ? `[${uniform.count}]` : ''};\n`; }); return `${code}};\n`; } static getTexturesShaderDeclaration(bindGroupFormat, bindGroup) { let code = ''; bindGroupFormat.textureFormats.forEach((format)=>{ let textureType = textureDimensionInfo[format.textureDimension]; const isArray = textureType === 'texture2DArray'; const sampleTypePrefix = format.sampleType === SAMPLETYPE_UINT ? 'u' : format.sampleType === SAMPLETYPE_INT ? 'i' : ''; textureType = `${sampleTypePrefix}${textureType}`; let namePostfix = ''; let extraCode = ''; if (isArray) { namePostfix = '_texture'; extraCode = `#define ${format.name} ${sampleTypePrefix}sampler2DArray(${format.name}${namePostfix}, ${format.name}_sampler)\n`; } code += `layout(set = ${bindGroup}, binding = ${format.slot}) uniform ${textureType} ${format.name}${namePostfix};\n`; if (format.hasSampler) { code += `layout(set = ${bindGroup}, binding = ${format.slot + 1}) uniform sampler ${format.name}_sampler;\n`; } code += extraCode; }); return code; } } const KEYWORD$1 = /^[ \t]*(attribute|varying|uniform)[\t ]+/gm; const KEYWORD_LINE = /^[ \t]*(attribute|varying|uniform)[ \t]*([^;]+)(;+)/gm; const KEYWORD_RESOURCE = /^[ \t]*var\s*(?:(]*>)\s*([\w\d_]+)\s*:\s*(.*?)\s*;|(<(?!storage,)[^>]*>)?\s*([\w\d_]+)\s*:\s*(texture_.*|storage_texture_.*|storage\w.*|external_texture|sampler(?:_comparison)?)\s*;)\s*$/gm; const VARYING = /(?:@interpolate\([^)]*\)\s*)?([\w]+)\s*:\s*([\w<>]+)/; const MARKER = '@@@'; const ENTRY_FUNCTION = /(@vertex|@fragment)\s*fn\s+\w+\s*\(\s*(\w+)\s*:[\s\S]*?\{/; const textureBaseInfo = { 'texture_1d': { viewDimension: TEXTUREDIMENSION_1D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_3d': { viewDimension: TEXTUREDIMENSION_3D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_multisampled_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_depth_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_external': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_UNFILTERABLE_FLOAT } }; const getTextureInfo = (baseType, componentType)=>{ const baseInfo = textureBaseInfo[baseType]; let finalSampleType = baseInfo.baseSampleType; if (baseInfo.baseSampleType === SAMPLETYPE_FLOAT && baseType !== 'texture_multisampled_2d') { switch(componentType){ case 'u32': finalSampleType = SAMPLETYPE_UINT; break; case 'i32': finalSampleType = SAMPLETYPE_INT; break; case 'f32': finalSampleType = SAMPLETYPE_FLOAT; break; case 'uff': finalSampleType = SAMPLETYPE_UNFILTERABLE_FLOAT; break; } } return { viewDimension: baseInfo.viewDimension, sampleType: finalSampleType }; }; const getTextureDeclarationType = (viewDimension, sampleType)=>{ if (sampleType === SAMPLETYPE_DEPTH) { switch(viewDimension){ case TEXTUREDIMENSION_2D: return 'texture_depth_2d'; case TEXTUREDIMENSION_2D_ARRAY: return 'texture_depth_2d_array'; case TEXTUREDIMENSION_CUBE: return 'texture_depth_cube'; case TEXTUREDIMENSION_CUBE_ARRAY: return 'texture_depth_cube_array'; } } let baseTypeString; switch(viewDimension){ case TEXTUREDIMENSION_1D: baseTypeString = 'texture_1d'; break; case TEXTUREDIMENSION_2D: baseTypeString = 'texture_2d'; break; case TEXTUREDIMENSION_2D_ARRAY: baseTypeString = 'texture_2d_array'; break; case TEXTUREDIMENSION_3D: baseTypeString = 'texture_3d'; break; case TEXTUREDIMENSION_CUBE: baseTypeString = 'texture_cube'; break; case TEXTUREDIMENSION_CUBE_ARRAY: baseTypeString = 'texture_cube_array'; break; } let coreFormatString; switch(sampleType){ case SAMPLETYPE_FLOAT: case SAMPLETYPE_UNFILTERABLE_FLOAT: coreFormatString = 'f32'; break; case SAMPLETYPE_UINT: coreFormatString = 'u32'; break; case SAMPLETYPE_INT: coreFormatString = 'i32'; break; } return `${baseTypeString}<${coreFormatString}>`; }; const wrappedArrayTypes = { 'f32': 'WrappedF32', 'i32': 'WrappedI32', 'u32': 'WrappedU32', 'vec2f': 'WrappedVec2F', 'vec2i': 'WrappedVec2I', 'vec2u': 'WrappedVec2U' }; const splitToWords = (line)=>{ line = line.replace(/\s+/g, ' ').trim(); return line.split(/[\s:]+/); }; const UNIFORM_ARRAY_REGEX = /array<([^,]+),\s*([^>]+)>/; class UniformLine { constructor(line, shader){ this.ubName = null; this.arraySize = 0; this.line = line; const parts = splitToWords(line); if (parts.length < 2) { shader.failed = true; return; } this.name = parts[0]; this.type = parts.slice(1).join(' '); if (this.type.includes('array<')) { const match = UNIFORM_ARRAY_REGEX.exec(this.type); this.type = match[1].trim(); this.arraySize = Number(match[2]); if (isNaN(this.arraySize)) { shader.failed = true; } } } } const TEXTURE_REGEX = /^\s*var\s+(\w+)\s*:\s*(texture_\w+)(?:<(\w+)>)?;\s*$/; const STORAGE_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(texture_storage_2d|texture_storage_2d_array)<([\w\d_]+),\s*(\w+)>\s*;\s*$/; const STORAGE_BUFFER_REGEX = /^\s*var\s*\s*([\w\d_]+)\s*:\s*(.*)\s*;\s*$/; const EXTERNAL_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*texture_external;\s*$/; const SAMPLER_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(sampler|sampler_comparison)\s*;\s*$/; class ResourceLine { equals(other) { if (this.name !== other.name) return false; if (this.type !== other.type) return false; if (this.isTexture !== other.isTexture) return false; if (this.isSampler !== other.isSampler) return false; if (this.isStorageTexture !== other.isStorageTexture) return false; if (this.isStorageBuffer !== other.isStorageBuffer) return false; if (this.isExternalTexture !== other.isExternalTexture) return false; if (this.textureFormat !== other.textureFormat) return false; if (this.textureDimension !== other.textureDimension) return false; if (this.sampleType !== other.sampleType) return false; if (this.textureType !== other.textureType) return false; if (this.format !== other.format) return false; if (this.access !== other.access) return false; if (this.accessMode !== other.accessMode) return false; if (this.samplerType !== other.samplerType) return false; return true; } constructor(line, shader){ this.originalLine = line; this.line = line; this.isTexture = false; this.isSampler = false; this.isStorageTexture = false; this.isStorageBuffer = false; this.isExternalTexture = false; this.type = ''; this.matchedElements = []; const textureMatch = this.line.match(TEXTURE_REGEX); if (textureMatch) { this.name = textureMatch[1]; this.type = textureMatch[2]; this.textureFormat = textureMatch[3]; this.isTexture = true; this.matchedElements.push(...textureMatch); const info = getTextureInfo(this.type, this.textureFormat); this.textureDimension = info.viewDimension; this.sampleType = info.sampleType; } const storageTextureMatch = this.line.match(STORAGE_TEXTURE_REGEX); if (storageTextureMatch) { this.isStorageTexture = true; this.name = storageTextureMatch[1]; this.textureType = storageTextureMatch[2]; this.format = storageTextureMatch[3]; this.access = storageTextureMatch[4]; this.matchedElements.push(...storageTextureMatch); } const storageBufferMatch = this.line.match(STORAGE_BUFFER_REGEX); if (storageBufferMatch) { this.isStorageBuffer = true; this.accessMode = storageBufferMatch[1] || 'none'; this.name = storageBufferMatch[2]; this.type = storageBufferMatch[3]; this.matchedElements.push(...storageBufferMatch); } const externalTextureMatch = this.line.match(EXTERNAL_TEXTURE_REGEX); if (externalTextureMatch) { this.name = externalTextureMatch[1]; this.isExternalTexture = true; this.matchedElements.push(...storageBufferMatch); } const samplerMatch = this.line.match(SAMPLER_REGEX); if (samplerMatch) { this.name = samplerMatch[1]; this.samplerType = samplerMatch[2]; this.isSampler = true; this.matchedElements.push(...samplerMatch); } if (this.matchedElements.length === 0) { shader.failed = true; } } } class WebgpuShaderProcessorWGSL { static run(device, shaderDefinition, shader) { const varyingMap = new Map(); const vertexExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.vshader); const fragmentExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.fshader); const attributesMap = new Map(); const attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions, shader); const vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true, device); const fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false, device); const concatUniforms = vertexExtracted.uniforms.concat(fragmentExtracted.uniforms); const uniforms = Array.from(new Set(concatUniforms)); const parsedUniforms = uniforms.map((line)=>new UniformLine(line, shader)); const uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader); vertexExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(vertexExtracted.src, parsedUniforms); fragmentExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(fragmentExtracted.src, parsedUniforms); const parsedResources = WebgpuShaderProcessorWGSL.mergeResources(vertexExtracted.resources, fragmentExtracted.resources, shader); const resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, shaderDefinition.processingOptions, shader); const fOutput = WebgpuShaderProcessorWGSL.generateFragmentOutputStruct(fragmentExtracted.src, device.maxColorAttachments); vertexExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(vertexExtracted.src, shader); fragmentExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(fragmentExtracted.src, shader); const vBlock = `${attributesBlock}\n${vertexVaryingsBlock}\n${uniformsData.code}\n${resourcesData.code}\n`; const vshader = vertexExtracted.src.replace(MARKER, vBlock); const fBlock = `${fragmentVaryingsBlock}\n${fOutput}\n${uniformsData.code}\n${resourcesData.code}\n`; const fshader = fragmentExtracted.src.replace(MARKER, fBlock); return { vshader: vshader, fshader: fshader, attributes: attributesMap, meshUniformBufferFormat: uniformsData.meshUniformBufferFormat, meshBindGroupFormat: resourcesData.meshBindGroupFormat }; } static extract(src) { const attributes = []; const varyings = []; const uniforms = []; const resources = []; let replacement = `${MARKER}\n`; let match; while((match = KEYWORD$1.exec(src)) !== null){ const keyword = match[1]; KEYWORD_LINE.lastIndex = match.index; const lineMatch = KEYWORD_LINE.exec(src); if (keyword === 'attribute') { attributes.push(lineMatch[2]); } else if (keyword === 'varying') { varyings.push(lineMatch[2]); } else if (keyword === 'uniform') { uniforms.push(lineMatch[2]); } src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_LINE.lastIndex, replacement); KEYWORD$1.lastIndex = match.index + replacement.length; replacement = ''; } while((match = KEYWORD_RESOURCE.exec(src)) !== null){ resources.push(match[0]); src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_RESOURCE.lastIndex, replacement); KEYWORD_RESOURCE.lastIndex = match.index + replacement.length; replacement = ''; } return { src, attributes, varyings, uniforms, resources }; } static processUniforms(device, uniforms, processingOptions, shader) { const meshUniforms = []; uniforms.forEach((uniform)=>{ if (!processingOptions.hasUniform(uniform.name)) { uniform.ubName = 'ub_mesh_ub'; const uniformType = uniformTypeToNameMapWGSL.get(uniform.type); const uniformFormat = new UniformFormat(uniform.name, uniformType, uniform.arraySize); meshUniforms.push(uniformFormat); } else { uniform.ubName = 'ub_view'; } }); if (meshUniforms.length === 0) { meshUniforms.push(new UniformFormat(UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT)); } const meshUniformBufferFormat = new UniformBufferFormat(device, meshUniforms); let code = ''; processingOptions.uniformFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(format, bindGroupIndex, 0); } }); if (meshUniformBufferFormat) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(meshUniformBufferFormat, BINDGROUP_MESH_UB, 0); } return { code, meshUniformBufferFormat }; } static renameUniformAccess(source, uniforms) { uniforms.forEach((uniform)=>{ const srcName = `uniform.${uniform.name}`; const dstName = `${uniform.ubName}.${uniform.name}`; const regex = new RegExp(`\\b${srcName}\\b`, 'g'); source = source.replace(regex, dstName); }); return source; } static mergeResources(vertex, fragment, shader) { const resources = vertex.map((line)=>new ResourceLine(line, shader)); const fragmentResources = fragment.map((line)=>new ResourceLine(line, shader)); fragmentResources.forEach((fragmentResource)=>{ const existing = resources.find((resource)=>resource.name === fragmentResource.name); if (existing) { if (!existing.equals(fragmentResource)) { shader.failed = true; } } else { resources.push(fragmentResource); } }); return resources; } static processResources(device, resources, processingOptions, shader) { const textureFormats = []; for(let i = 0; i < resources.length; i++){ const resource = resources[i]; if (resource.isTexture) { const sampler = resources[i + 1]; const hasSampler = sampler?.isSampler; const sampleType = resource.sampleType; const dimension = resource.textureDimension; textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null)); if (hasSampler) i++; } if (resource.isStorageBuffer) { const readOnly = resource.accessMode !== 'read_write'; const bufferFormat = new BindStorageBufferFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, readOnly); bufferFormat.format = resource.type; textureFormats.push(bufferFormat); } } const meshBindGroupFormat = new BindGroupFormat(device, textureFormats); let code = ''; processingOptions.bindGroupFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(format, bindGroupIndex); } }); code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(meshBindGroupFormat, BINDGROUP_MESH); return { code, meshBindGroupFormat }; } static getUniformShaderDeclaration(ubFormat, bindGroup, bindIndex) { const name = bindGroupNames[bindGroup]; const structName = `struct_ub_${name}`; let code = `struct ${structName} {\n`; ubFormat.uniforms.forEach((uniform)=>{ let typeString = uniformTypeToNameWGSL[uniform.type][0]; if (uniform.count > 0) { if (wrappedArrayTypes.hasOwnProperty(typeString)) { typeString = wrappedArrayTypes[typeString]; } code += ` ${uniform.shortName}: array<${typeString}, ${uniform.count}>,\n`; } else { code += ` ${uniform.shortName}: ${typeString},\n`; } }); code += '};\n'; code += `@group(${bindGroup}) @binding(${bindIndex}) var ub_${name} : ${structName};\n\n`; return code; } static getTextureShaderDeclaration(format, bindGroup) { let code = ''; format.textureFormats.forEach((format)=>{ const textureTypeName = getTextureDeclarationType(format.textureDimension, format.sampleType); code += `@group(${bindGroup}) @binding(${format.slot}) var ${format.name}: ${textureTypeName};\n`; if (format.hasSampler) { const samplerName = format.sampleType === SAMPLETYPE_DEPTH ? 'sampler_comparison' : 'sampler'; code += `@group(${bindGroup}) @binding(${format.slot + 1}) var ${format.samplerName}: ${samplerName};\n`; } }); format.storageBufferFormats.forEach((format)=>{ const access = format.readOnly ? 'read' : 'read_write'; code += `@group(${bindGroup}) @binding(${format.slot}) var ${format.name} : ${format.format};\n`; }); return code; } static processVaryings(varyingLines, varyingMap, isVertex, device) { let block = ''; let blockPrivates = ''; let blockCopy = ''; varyingLines.forEach((line, index)=>{ const match = line.match(VARYING); if (match) { const name = match[1]; const type = match[2]; if (isVertex) { varyingMap.set(name, index); } else { index = varyingMap.get(name); } block += ` @location(${index}) ${line},\n`; if (!isVertex) { blockPrivates += ` var ${name}: ${type};\n`; blockCopy += ` ${name} = input.${name};\n`; } } }); if (isVertex) { block += ' @builtin(position) position : vec4f,\n'; } else { block += ' @builtin(position) position : vec4f,\n'; block += ' @builtin(front_facing) frontFacing : bool,\n'; block += ' @builtin(sample_index) sampleIndex : u32,\n'; if (device.supportsPrimitiveIndex) { block += ' @builtin(primitive_index) primitiveIndex : u32,\n'; } } const primitiveIndexGlobals = device.supportsPrimitiveIndex ? ` var pcPrimitiveIndex: u32; ` : ''; const primitiveIndexCopy = device.supportsPrimitiveIndex ? ` pcPrimitiveIndex = input.primitiveIndex; ` : ''; const fragmentGlobals = isVertex ? '' : ` var pcPosition: vec4f; var pcFrontFacing: bool; var pcSampleIndex: u32; ${primitiveIndexGlobals} ${blockPrivates} // function to copy inputs (varyings) to private global variables fn _pcCopyInputs(input: FragmentInput) { ${blockCopy} pcPosition = input.position; pcFrontFacing = input.frontFacing; pcSampleIndex = input.sampleIndex; ${primitiveIndexCopy} } `; const structName = isVertex ? 'VertexOutput' : 'FragmentInput'; return ` struct ${structName} { ${block} }; ${fragmentGlobals} `; } static generateFragmentOutputStruct(src, numRenderTargets) { let structCode = 'struct FragmentOutput {\n'; for(let i = 0; i < numRenderTargets; i++){ structCode += ` @location(${i}) color${i > 0 ? i : ''} : pcOutType${i},\n`; } const needsFragDepth = src.search(/\.fragDepth\s*=/) !== -1; if (needsFragDepth) { structCode += ' @builtin(frag_depth) fragDepth : f32\n'; } return `${structCode}};\n`; } static floatAttributeToInt(type, signed) { const longToShortMap = { 'f32': 'f32', 'vec2': 'vec2f', 'vec3': 'vec3f', 'vec4': 'vec4f' }; const shortType = longToShortMap[type] || type; const floatToIntShort = { 'f32': signed ? 'i32' : 'u32', 'vec2f': signed ? 'vec2i' : 'vec2u', 'vec3f': signed ? 'vec3i' : 'vec3u', 'vec4f': signed ? 'vec4i' : 'vec4u' }; return floatToIntShort[shortType] || null; } static processAttributes(attributeLines, shaderDefinitionAttributes = {}, attributesMap, processingOptions, shader) { let blockAttributes = ''; let blockPrivates = ''; let blockCopy = ''; attributeLines.forEach((line)=>{ const words = splitToWords(line); const name = words[0]; let type = words[1]; const originalType = type; if (shaderDefinitionAttributes.hasOwnProperty(name)) { const semantic = shaderDefinitionAttributes[name]; const location = semanticToLocation[semantic]; attributesMap.set(location, name); const element = processingOptions.getVertexElement(semantic); if (element) { const dataType = element.dataType; if (dataType !== TYPE_FLOAT32 && dataType !== TYPE_FLOAT16 && !element.normalize && !element.asInt) { const isSignedType = dataType === TYPE_INT8 || dataType === TYPE_INT16 || dataType === TYPE_INT32; type = WebgpuShaderProcessorWGSL.floatAttributeToInt(type, isSignedType); } } blockAttributes += ` @location(${location}) ${name}: ${type},\n`; blockPrivates += ` var ${line};\n`; blockCopy += ` ${name} = ${originalType}(input.${name});\n`; } }); return ` struct VertexInput { ${blockAttributes} @builtin(vertex_index) vertexIndex : u32, // built-in vertex index @builtin(instance_index) instanceIndex : u32 // built-in instance index }; ${blockPrivates} var pcVertexIndex: u32; var pcInstanceIndex: u32; fn _pcCopyInputs(input: VertexInput) { ${blockCopy} pcVertexIndex = input.vertexIndex; pcInstanceIndex = input.instanceIndex; } `; } static copyInputs(src, shader) { const match = src.match(ENTRY_FUNCTION); if (!match || !match[2]) { return src; } const inputName = match[2]; const braceIndex = match.index + match[0].length - 1; const beginning = src.slice(0, braceIndex + 1); const end = src.slice(braceIndex + 1); const lineToInject = `\n _pcCopyInputs(${inputName});`; return beginning + lineToInject + end; } static cutOut(src, start, end, replacement) { return src.substring(0, start) + replacement + src.substring(end); } } const computeShaderIds = new StringIds(); class WebgpuShader { destroy(shader) { this._vertexCode = null; this._fragmentCode = null; } createShaderModule(code, shaderType) { const device = this.shader.device; const wgpu = device.wgpu; const shaderModule = wgpu.createShaderModule({ code: code }); return shaderModule; } getVertexShaderModule() { return this.createShaderModule(this._vertexCode, 'Vertex'); } getFragmentShaderModule() { return this.createShaderModule(this._fragmentCode, 'Fragment'); } getComputeShaderModule() { return this.createShaderModule(this._computeCode, 'Compute'); } processGLSL() { const shader = this.shader; const processed = ShaderProcessorGLSL.run(shader.device, shader.definition, shader); this._vertexCode = this.transpile(processed.vshader, 'vertex', shader.definition.vshader); this._fragmentCode = this.transpile(processed.fshader, 'fragment', shader.definition.fshader); if (!(this._vertexCode && this._fragmentCode)) { shader.failed = true; } else { shader.ready = true; } shader.meshUniformBufferFormat = processed.meshUniformBufferFormat; shader.meshBindGroupFormat = processed.meshBindGroupFormat; shader.attributes = processed.attributes; } processWGSL() { const shader = this.shader; const processed = WebgpuShaderProcessorWGSL.run(shader.device, shader.definition, shader); this._vertexCode = processed.vshader; this._fragmentCode = processed.fshader; shader.meshUniformBufferFormat = processed.meshUniformBufferFormat; shader.meshBindGroupFormat = processed.meshBindGroupFormat; shader.attributes = processed.attributes; } transpile(src, shaderType, originalSrc) { const device = this.shader.device; if (!device.glslang || !device.twgsl) { console.error(`Cannot transpile shader [${this.shader.label}] - shader transpilers (glslang/twgsl) are not available. Make sure to provide glslangUrl and twgslUrl when creating the device.`, { shader: this.shader }); return null; } try { const spirv = device.glslang.compileGLSL(src, shaderType); const wgsl = device.twgsl.convertSpirV2WGSL(spirv); return wgsl; } catch (err) { console.error(`Failed to transpile webgl ${shaderType} shader [${this.shader.label}] to WebGPU while rendering ${ void 0}, error:\n [${err.stack}]`, { processed: src, original: originalSrc, shader: this.shader, error: err, stack: err.stack }); } } get vertexCode() { return this._vertexCode; } get fragmentCode() { return this._fragmentCode; } get computeKey() { if (this._computeKey === undefined) { const keyString = `${this._computeCode}|${this.computeEntryPoint}`; this._computeKey = computeShaderIds.get(keyString); } return this._computeKey; } loseContext() {} restoreContext(device, shader) {} constructor(shader){ this._vertexCode = null; this._fragmentCode = null; this._computeCode = null; this.vertexEntryPoint = 'main'; this.fragmentEntryPoint = 'main'; this.computeEntryPoint = 'main'; this.shader = shader; const definition = shader.definition; if (definition.shaderLanguage === SHADERLANGUAGE_WGSL) { if (definition.cshader) { this._computeCode = definition.cshader ?? null; this.computeUniformBufferFormats = definition.computeUniformBufferFormats; this.computeBindGroupFormat = definition.computeBindGroupFormat; if (definition.computeEntryPoint) { this.computeEntryPoint = definition.computeEntryPoint; } } else { this.vertexEntryPoint = 'vertexMain'; this.fragmentEntryPoint = 'fragmentMain'; if (definition.processingOptions) { this.processWGSL(); } else { this._vertexCode = definition.vshader ?? null; this._fragmentCode = definition.fshader ?? null; shader.meshUniformBufferFormat = definition.meshUniformBufferFormat; shader.meshBindGroupFormat = definition.meshBindGroupFormat; } } shader.ready = true; } else { if (definition.processingOptions) { this.processGLSL(); } } } } const gpuAddressModes = []; gpuAddressModes[ADDRESS_REPEAT] = 'repeat'; gpuAddressModes[ADDRESS_CLAMP_TO_EDGE] = 'clamp-to-edge'; gpuAddressModes[ADDRESS_MIRRORED_REPEAT] = 'mirror-repeat'; const gpuFilterModes = []; gpuFilterModes[FILTER_NEAREST] = { level: 'nearest', mip: 'nearest' }; gpuFilterModes[FILTER_LINEAR] = { level: 'linear', mip: 'nearest' }; gpuFilterModes[FILTER_NEAREST_MIPMAP_NEAREST] = { level: 'nearest', mip: 'nearest' }; gpuFilterModes[FILTER_NEAREST_MIPMAP_LINEAR] = { level: 'nearest', mip: 'linear' }; gpuFilterModes[FILTER_LINEAR_MIPMAP_NEAREST] = { level: 'linear', mip: 'nearest' }; gpuFilterModes[FILTER_LINEAR_MIPMAP_LINEAR] = { level: 'linear', mip: 'linear' }; const dummyUse = (thingOne)=>{}; class WebgpuTexture { create(device) { const texture = this.texture; const wgpu = device.wgpu; const numLevels = texture.numLevels; this.desc = { size: { width: texture.width, height: texture.height, depthOrArrayLayers: texture.cubemap ? 6 : texture.array ? texture.arrayLength : 1 }, format: this.format, mipLevelCount: numLevels, sampleCount: 1, dimension: texture.volume ? '3d' : '2d', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | (isCompressedPixelFormat(texture.format) ? 0 : GPUTextureUsage.RENDER_ATTACHMENT) | (texture.storage ? GPUTextureUsage.STORAGE_BINDING : 0) }; this.gpuTexture = wgpu.createTexture(this.desc); let viewDescr; if (this.texture.format === PIXELFORMAT_DEPTHSTENCIL) { viewDescr = { format: 'depth24plus', aspect: 'depth-only' }; } this.view = this.createView(viewDescr); this.viewCache.clear(); } destroy(device) {} propertyChanged(flag) { this.samplers.length = 0; } getView(device, textureView) { this.uploadImmediate(device, this.texture); if (textureView) { let view = this.viewCache.get(textureView.key); if (!view) { view = this.createView({ baseMipLevel: textureView.baseMipLevel, mipLevelCount: textureView.mipLevelCount, baseArrayLayer: textureView.baseArrayLayer, arrayLayerCount: textureView.arrayLayerCount }); this.viewCache.set(textureView.key, view); } return view; } return this.view; } createView(viewDescr) { const options = viewDescr ?? {}; const textureDescr = this.desc; const texture = this.texture; const defaultViewDimension = ()=>{ if (texture.cubemap) return 'cube'; if (texture.volume) return '3d'; if (texture.array) return '2d-array'; return '2d'; }; const desc = { format: options.format ?? textureDescr.format, dimension: options.dimension ?? defaultViewDimension(), aspect: options.aspect ?? 'all', baseMipLevel: options.baseMipLevel ?? 0, mipLevelCount: options.mipLevelCount ?? textureDescr.mipLevelCount, baseArrayLayer: options.baseArrayLayer ?? 0, arrayLayerCount: options.arrayLayerCount ?? textureDescr.depthOrArrayLayers }; const view = this.gpuTexture.createView(desc); return view; } getSampler(device, sampleType) { let sampler = this.samplers[sampleType]; if (!sampler) { const texture = this.texture; const desc = { addressModeU: gpuAddressModes[texture.addressU], addressModeV: gpuAddressModes[texture.addressV], addressModeW: gpuAddressModes[texture.addressW] }; if (!sampleType && texture.compareOnRead) { sampleType = SAMPLETYPE_DEPTH; } if (sampleType === SAMPLETYPE_DEPTH || sampleType === SAMPLETYPE_INT || sampleType === SAMPLETYPE_UINT) { desc.compare = 'less'; desc.magFilter = 'linear'; desc.minFilter = 'linear'; } else if (sampleType === SAMPLETYPE_UNFILTERABLE_FLOAT) { desc.magFilter = 'nearest'; desc.minFilter = 'nearest'; desc.mipmapFilter = 'nearest'; } else { const forceNearest = !device.textureFloatFilterable && (texture.format === PIXELFORMAT_RGBA32F || texture.format === PIXELFORMAT_RGBA16F); if (forceNearest || this.texture.format === PIXELFORMAT_DEPTHSTENCIL || isIntegerPixelFormat(this.texture.format)) { desc.magFilter = 'nearest'; desc.minFilter = 'nearest'; desc.mipmapFilter = 'nearest'; } else { desc.magFilter = gpuFilterModes[texture.magFilter].level; desc.minFilter = gpuFilterModes[texture.minFilter].level; desc.mipmapFilter = gpuFilterModes[texture.minFilter].mip; } } const allLinear = desc.minFilter === 'linear' && desc.magFilter === 'linear' && desc.mipmapFilter === 'linear'; desc.maxAnisotropy = allLinear ? math.clamp(Math.round(texture._anisotropy), 1, device.maxTextureAnisotropy) : 1; sampler = device.wgpu.createSampler(desc); this.samplers[sampleType] = sampler; } return sampler; } loseContext() {} uploadImmediate(device, texture) { if (texture._needsUpload || texture._needsMipmapsUpload) { this.uploadData(device); texture._needsUpload = false; texture._needsMipmapsUpload = false; } } uploadData(device) { const texture = this.texture; if (this.desc && (this.desc.size.width !== texture.width || this.desc.size.height !== texture.height)) { this.gpuTexture.destroy(); this.create(device); texture.renderVersionDirty = device.renderVersion; } if (texture._levels) { let anyUploads = false; let anyLevelMissing = false; const requiredMipLevels = texture.numLevels; for(let mipLevel = 0; mipLevel < requiredMipLevels; mipLevel++){ const mipObject = texture._levels[mipLevel]; if (mipObject) { if (texture.cubemap) { for(let face = 0; face < 6; face++){ const faceSource = mipObject[face]; if (faceSource) { if (this.isExternalImage(faceSource)) { this.uploadExternalImage(device, faceSource, mipLevel, face); anyUploads = true; } else if (ArrayBuffer.isView(faceSource)) { this.uploadTypedArrayData(device, faceSource, mipLevel, face); anyUploads = true; } else ; } else { anyLevelMissing = true; } } } else if (texture._volume) ; else if (texture.array) { if (texture.arrayLength === mipObject.length) { for(let index = 0; index < texture._arrayLength; index++){ const arraySource = mipObject[index]; if (this.isExternalImage(arraySource)) { this.uploadExternalImage(device, arraySource, mipLevel, index); anyUploads = true; } else if (ArrayBuffer.isView(arraySource)) { this.uploadTypedArrayData(device, arraySource, mipLevel, index); anyUploads = true; } else ; } } else { anyLevelMissing = true; } } else { if (this.isExternalImage(mipObject)) { this.uploadExternalImage(device, mipObject, mipLevel, 0); anyUploads = true; } else if (ArrayBuffer.isView(mipObject)) { this.uploadTypedArrayData(device, mipObject, mipLevel, 0); anyUploads = true; } else ; } } else { anyLevelMissing = true; } } if (anyUploads && anyLevelMissing && texture.mipmaps && !isCompressedPixelFormat(texture.format) && !isIntegerPixelFormat(texture.format)) { device.mipmapRenderer.generate(this); } if (texture._gpuSize) { texture.adjustVramSizeTracking(device._vram, -texture._gpuSize); } texture._gpuSize = texture.gpuSize; texture.adjustVramSizeTracking(device._vram, texture._gpuSize); } } isExternalImage(image) { return typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap || typeof HTMLVideoElement !== 'undefined' && image instanceof HTMLVideoElement || typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement || typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas; } uploadExternalImage(device, image, mipLevel, index) { const src = { source: image, origin: [ 0, 0 ], flipY: false }; const dst = { texture: this.gpuTexture, mipLevel: mipLevel, origin: [ 0, 0, index ], aspect: 'all', premultipliedAlpha: this.texture._premultiplyAlpha }; const copySize = { width: this.desc.size.width, height: this.desc.size.height, depthOrArrayLayers: 1 }; device.submit(); dummyUse(image instanceof HTMLCanvasElement && image.getContext('2d')); device.wgpu.queue.copyExternalImageToTexture(src, dst, copySize); } uploadTypedArrayData(device, data, mipLevel, index) { const texture = this.texture; const wgpu = device.wgpu; const dest = { texture: this.gpuTexture, origin: [ 0, 0, index ], mipLevel: mipLevel }; const width = TextureUtils.calcLevelDimension(texture.width, mipLevel); const height = TextureUtils.calcLevelDimension(texture.height, mipLevel); TextureUtils.calcLevelGpuSize(width, height, 1, texture.format); const formatInfo = pixelFormatInfo.get(texture.format); let dataLayout; let size; if (formatInfo.size) { dataLayout = { offset: 0, bytesPerRow: formatInfo.size * width, rowsPerImage: height }; size = { width: width, height: height }; } else if (formatInfo.blockSize) { const blockDim = (size)=>{ return Math.floor((size + 3) / 4); }; dataLayout = { offset: 0, bytesPerRow: formatInfo.blockSize * blockDim(width), rowsPerImage: blockDim(height) }; size = { width: Math.max(4, width), height: Math.max(4, height) }; } else ; device.submit(); wgpu.queue.writeTexture(dest, data, dataLayout, size); } read(x, y, width, height, options) { const mipLevel = options.mipLevel ?? 0; const face = options.face ?? 0; const data = options.data ?? null; const immediate = options.immediate ?? false; const texture = this.texture; const formatInfo = pixelFormatInfo.get(texture.format); const bytesPerRow = width * formatInfo.size; const paddedBytesPerRow = math.roundUp(bytesPerRow, 256); const size = paddedBytesPerRow * height; const device = texture.device; const stagingBuffer = device.createBufferImpl(BUFFERUSAGE_READ | BUFFERUSAGE_COPY_DST); stagingBuffer.allocate(device, size); const src = { texture: this.gpuTexture, mipLevel: mipLevel, origin: [ x, y, face ] }; const dst = { buffer: stagingBuffer.buffer, offset: 0, bytesPerRow: paddedBytesPerRow }; const copySize = { width, height, depthOrArrayLayers: 1 }; const commandEncoder = device.getCommandEncoder(); commandEncoder.copyTextureToBuffer(src, dst, copySize); return device.readBuffer(stagingBuffer, size, null, immediate).then((temp)=>{ const ArrayType = getPixelFormatArrayType(texture.format); const targetBuffer = data?.buffer ?? new ArrayBuffer(height * bytesPerRow); const target = new Uint8Array(targetBuffer, data?.byteOffset ?? 0, height * bytesPerRow); for(let i = 0; i < height; i++){ const srcOffset = i * paddedBytesPerRow; const dstOffset = i * bytesPerRow; target.set(temp.subarray(srcOffset, srcOffset + bytesPerRow), dstOffset); } return data ?? new ArrayType(targetBuffer); }); } constructor(texture){ this.samplers = []; this.viewCache = new Map(); this.texture = texture; this.format = gpuTextureFormats[texture.format]; this.create(texture.device); } } class WebgpuUniformBuffer extends WebgpuBuffer { unlock(uniformBuffer) { const device = uniformBuffer.device; super.unlock(device, uniformBuffer.storageInt32.buffer); } constructor(uniformBuffer){ super(BUFFERUSAGE_UNIFORM); } } class WebgpuVertexBuffer extends WebgpuBuffer { unlock(vertexBuffer) { const device = vertexBuffer.device; super.unlock(device, vertexBuffer.storage); } constructor(vertexBuffer, format, options){ super(BUFFERUSAGE_VERTEX | (options?.storage ? BUFFERUSAGE_STORAGE : 0)); } } const KEYWORD = /[ \t]*#(ifn?def|if|endif|else|elif|define|undef|extension|include)/g; const DEFINE = /define[ \t]+([^\n]+)\r?(?:\n|$)/g; const EXTENSION = /extension[ \t]+([\w-]+)[ \t]*:[ \t]*(enable|require)/g; const UNDEF = /undef[ \t]+([^\n]+)\r?(?:\n|$)/g; const IF = /(ifdef|ifndef|if)[ \t]*([^\r\n]+)\r?\n/g; const ENDIF = /(endif|else|elif)(?:[ \t]+([^\r\n]*))?\r?\n?/g; const IDENTIFIER$1 = /\{?[\w-]+\}?/; const DEFINED = /(!|\s)?defined\(([\w-]+)\)/; const DEFINED_PARENS = /!?defined\s*\([^)]*\)/g; const DEFINED_BEFORE_PAREN = /!?defined\s*$/; const COMPARISON = /([a-z_]\w*)\s*(==|!=|<|<=|>|>=)\s*([\w"']+)/i; const INVALID = /[+\-]/g; const INCLUDE = /include[ \t]+"([\w-]+)(?:\s*,\s*([\w-]+))?"/g; const LOOP_INDEX = /\{i\}/g; const FRAGCOLOR = /(pcFragColor[1-8])\b/g; class Preprocessor { static run(source, includes = new Map(), options = {}) { Preprocessor.sourceName = options.sourceName; source = this.stripComments(source); source = source.split(/\r?\n/).map((line)=>line.trimEnd()).join('\n'); const defines = new Map(); const injectDefines = new Map(); source = this._preprocess(source, defines, injectDefines, includes, options.stripDefines); if (source === null) return null; const intDefines = new Map(); defines.forEach((value, key)=>{ if (Number.isInteger(parseFloat(value)) && !value.includes('.')) { intDefines.set(key, value); } }); source = this.stripComments(source); source = this.stripUnusedColorAttachments(source, options); source = this.RemoveEmptyLines(source); source = this.processArraySize(source, intDefines); source = this.injectDefines(source, injectDefines); return source; } static stripUnusedColorAttachments(source, options) { if (options.stripUnusedColorAttachments) { const counts = new Map(); const matches = source.match(FRAGCOLOR); matches?.forEach((match)=>{ const index = parseInt(match.charAt(match.length - 1), 10); counts.set(index, (counts.get(index) ?? 0) + 1); }); const anySingleUse = Array.from(counts.values()).some((count)=>count === 1); if (anySingleUse) { const lines = source.split('\n'); const keepLines = []; for(let i = 0; i < lines.length; i++){ const match = lines[i].match(FRAGCOLOR); if (match) { const index = parseInt(match[0].charAt(match[0].length - 1), 10); if (index > 0 && counts.get(index) === 1) { continue; } } keepLines.push(lines[i]); } source = keepLines.join('\n'); } } return source; } static stripComments(source) { return source.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1'); } static processArraySize(source, intDefines) { if (source !== null) { intDefines.forEach((value, key)=>{ source = source.replace(new RegExp(`\\[${key}\\]`, 'g'), `[${value}]`); }); } return source; } static injectDefines(source, injectDefines) { if (source !== null && injectDefines.size > 0) { const lines = source.split('\n'); injectDefines.forEach((value, key)=>{ const regex = new RegExp(key, 'g'); for(let i = 0; i < lines.length; i++){ if (!lines[i].includes('#')) { lines[i] = lines[i].replace(regex, value); } } }); source = lines.join('\n'); } return source; } static RemoveEmptyLines(source) { if (source !== null) { source = source.split(/\r?\n/).map((line)=>line.trim() === '' ? '' : line).join('\n'); source = source.replace(/(\n\n){3,}/g, '\n\n'); } return source; } static _preprocess(source, defines = new Map(), injectDefines, includes, stripDefines) { const originalSource = source; const stack = []; let error = false; let match; while((match = KEYWORD.exec(source)) !== null && !error){ const keyword = match[1]; switch(keyword){ case 'define': { DEFINE.lastIndex = match.index; const define = DEFINE.exec(source); error || (error = define === null); const expression = define[1]; IDENTIFIER$1.lastIndex = define.index; const identifierValue = IDENTIFIER$1.exec(expression); const identifier = identifierValue[0]; let value = expression.substring(identifier.length).trim(); if (value === '') value = 'true'; const keep = Preprocessor._keep(stack); let stripThisDefine = stripDefines; if (keep) { const replacementDefine = identifier.startsWith('{') && identifier.endsWith('}'); if (replacementDefine) { stripThisDefine = true; } if (replacementDefine) { injectDefines.set(identifier, value); } else { defines.set(identifier, value); } if (stripThisDefine) { source = source.substring(0, define.index - 1) + source.substring(DEFINE.lastIndex); KEYWORD.lastIndex = define.index - 1; } } if (!stripThisDefine) { KEYWORD.lastIndex = define.index + define[0].length; } break; } case 'undef': { UNDEF.lastIndex = match.index; const undef = UNDEF.exec(source); const identifier = undef[1].trim(); const keep = Preprocessor._keep(stack); if (keep) { defines.delete(identifier); if (stripDefines) { source = source.substring(0, undef.index - 1) + source.substring(UNDEF.lastIndex); KEYWORD.lastIndex = undef.index - 1; } } if (!stripDefines) { KEYWORD.lastIndex = undef.index + undef[0].length; } break; } case 'extension': { EXTENSION.lastIndex = match.index; const extension = EXTENSION.exec(source); error || (error = extension === null); if (extension) { const identifier = extension[1]; const keep = Preprocessor._keep(stack); if (keep) { defines.set(identifier, 'true'); } } KEYWORD.lastIndex = extension.index + extension[0].length; break; } case 'ifdef': case 'ifndef': case 'if': { IF.lastIndex = match.index; const iff = IF.exec(source); const expression = iff[2]; const evaluated = Preprocessor.evaluate(expression, defines); error || (error = evaluated.error); let result = evaluated.result; if (keyword === 'ifndef') { result = !result; } stack.push({ anyKeep: result, keep: result, start: match.index, end: IF.lastIndex }); KEYWORD.lastIndex = iff.index + iff[0].length; break; } case 'endif': case 'else': case 'elif': { ENDIF.lastIndex = match.index; const endif = ENDIF.exec(source); const blockInfo = stack.pop(); if (!blockInfo) { console.error(`Shader preprocessing encountered "#${endif[1]}" without a preceding #if #ifdef #ifndef while preprocessing ${Preprocessor.sourceName} on line:\n ${source.substring(match.index, match.index + 100)}...`, { source: originalSource }); error = true; continue; } const blockCode = blockInfo.keep ? source.substring(blockInfo.end, match.index) : ''; source = source.substring(0, blockInfo.start) + blockCode + source.substring(ENDIF.lastIndex); KEYWORD.lastIndex = blockInfo.start + blockCode.length; const endifCommand = endif[1]; if (endifCommand === 'else' || endifCommand === 'elif') { let result = false; if (!blockInfo.anyKeep) { if (endifCommand === 'else') { result = !blockInfo.keep; } else { const evaluated = Preprocessor.evaluate(endif[2], defines); result = evaluated.result; error || (error = evaluated.error); } } stack.push({ anyKeep: blockInfo.anyKeep || result, keep: result, start: KEYWORD.lastIndex, end: KEYWORD.lastIndex }); } break; } case 'include': { INCLUDE.lastIndex = match.index; const include = INCLUDE.exec(source); error || (error = include === null); if (!include) { error = true; continue; } const identifier = include[1].trim(); const countIdentifier = include[2]?.trim(); const keep = Preprocessor._keep(stack); if (keep) { let includeSource = includes?.get(identifier); if (includeSource !== undefined) { includeSource = this.stripComments(includeSource); if (countIdentifier) { const countString = defines.get(countIdentifier); const count = parseFloat(countString); if (Number.isInteger(count)) { let result = ''; for(let i = 0; i < count; i++){ result += includeSource.replace(LOOP_INDEX, String(i)); } includeSource = result; } else { console.error(`Include Count identifier "${countIdentifier}" not resolved while preprocessing ${Preprocessor.sourceName} on line:\n ${source.substring(match.index, match.index + 100)}...`, { originalSource: originalSource, source: source }); error = true; } } source = source.substring(0, include.index - 1) + includeSource + source.substring(INCLUDE.lastIndex); KEYWORD.lastIndex = include.index - 1; } else { console.error(`Include "${identifier}" not resolved while preprocessing ${Preprocessor.sourceName}`, { originalSource: originalSource, source: source }); error = true; continue; } } break; } } } if (stack.length > 0) { console.error(`Shader preprocessing reached the end of the file without encountering the necessary #endif to close a preceding #if, #ifdef, or #ifndef block. ${Preprocessor.sourceName}`); error = true; } if (error) { console.error('Failed to preprocess shader: ', { source: originalSource }); return null; } return source; } static _keep(stack) { for(let i = 0; i < stack.length; i++){ if (!stack[i].keep) { return false; } } return true; } static evaluateAtomicExpression(expr, defines) { let error = false; expr = expr.trim(); let invert = false; if (expr === 'true') { return { result: true, error }; } if (expr === 'false') { return { result: false, error }; } const definedMatch = DEFINED.exec(expr); if (definedMatch) { invert = definedMatch[1] === '!'; expr = definedMatch[2].trim(); const exists = defines.has(expr); return { result: invert ? !exists : exists, error }; } const comparisonMatch = COMPARISON.exec(expr); if (comparisonMatch) { const left = defines.get(comparisonMatch[1].trim()) ?? comparisonMatch[1].trim(); const right = defines.get(comparisonMatch[3].trim()) ?? comparisonMatch[3].trim(); const operator = comparisonMatch[2].trim(); let result = false; switch(operator){ case '==': result = left === right; break; case '!=': result = left !== right; break; case '<': result = left < right; break; case '<=': result = left <= right; break; case '>': result = left > right; break; case '>=': result = left >= right; break; default: error = true; } return { result, error }; } const result = defines.has(expr); return { result, error }; } static processParentheses(expression, defines) { let error = false; let processed = expression.trim(); while(processed.startsWith('(') && processed.endsWith(')')){ let depth = 0; let wrapsEntire = true; for(let i = 0; i < processed.length - 1; i++){ if (processed[i] === '(') depth++; else if (processed[i] === ')') { depth--; if (depth === 0) { wrapsEntire = false; break; } } } if (wrapsEntire) { processed = processed.slice(1, -1).trim(); } else { break; } } while(true){ let foundParen = false; let depth = 0; let maxDepth = 0; let deepestStart = -1; let deepestEnd = -1; let inDefinedParen = 0; for(let i = 0; i < processed.length; i++){ if (processed[i] === '(') { const beforeParen = processed.substring(0, i); if (DEFINED_BEFORE_PAREN.test(beforeParen)) { inDefinedParen++; } else if (inDefinedParen === 0) { depth++; if (depth > maxDepth) { maxDepth = depth; deepestStart = i; } foundParen = true; } } else if (processed[i] === ')') { if (inDefinedParen > 0) { inDefinedParen--; } else if (depth > 0) { if (depth === maxDepth && deepestStart !== -1) { deepestEnd = i; } depth--; } } } if (!foundParen || deepestStart === -1 || deepestEnd === -1) { break; } const subExpr = processed.substring(deepestStart + 1, deepestEnd); const { result, error: subError } = Preprocessor.evaluate(subExpr, defines); error = error || subError; processed = processed.substring(0, deepestStart) + (result ? 'true' : 'false') + processed.substring(deepestEnd + 1); } return { expression: processed, error }; } static evaluate(expression, defines) { const correct = INVALID.exec(expression) === null; let processedExpr = expression; let parenError = false; const withoutDefined = expression.replace(DEFINED_PARENS, ''); if (withoutDefined.indexOf('(') !== -1) { const processed = Preprocessor.processParentheses(expression, defines); processedExpr = processed.expression; parenError = processed.error; } if (parenError) { return { result: false, error: true }; } const orSegments = processedExpr.split('||'); for (const orSegment of orSegments){ const andSegments = orSegment.split('&&'); let andResult = true; for (const andSegment of andSegments){ const { result, error } = Preprocessor.evaluateAtomicExpression(andSegment.trim(), defines); if (!result || error) { andResult = false; break; } } if (andResult) { return { result: true, error: !correct }; } } return { result: false, error: !correct }; } } var gles3PS = ` #ifndef outType_0 #define outType_0 vec4 #endif layout(location = 0) out highp outType_0 pcFragColor0; #if COLOR_ATTACHMENT_1 layout(location = 1) out highp outType_1 pcFragColor1; #endif #if COLOR_ATTACHMENT_2 layout(location = 2) out highp outType_2 pcFragColor2; #endif #if COLOR_ATTACHMENT_3 layout(location = 3) out highp outType_3 pcFragColor3; #endif #if COLOR_ATTACHMENT_4 layout(location = 4) out highp outType_4 pcFragColor4; #endif #if COLOR_ATTACHMENT_5 layout(location = 5) out highp outType_5 pcFragColor5; #endif #if COLOR_ATTACHMENT_6 layout(location = 6) out highp outType_6 pcFragColor6; #endif #if COLOR_ATTACHMENT_7 layout(location = 7) out highp outType_7 pcFragColor7; #endif #define gl_FragColor pcFragColor0 #define varying in #define texture2D texture #define texture2DBias texture #define textureCube texture #define texture2DProj textureProj #define texture2DLod textureLod #define texture2DProjLod textureProjLod #define textureCubeLod textureLod #define texture2DGrad textureGrad #define texture2DProjGrad textureProjGrad #define textureCubeGrad textureGrad #define utexture2D texture #define itexture2D texture #define texture2DLodEXT texture2DLodEXT_is_no_longer_supported_use_texture2DLod_instead #define texture2DProjLodEXT texture2DProjLodEXT_is_no_longer_supported_use_texture2DProjLod #define textureCubeLodEXT textureCubeLodEXT_is_no_longer_supported_use_textureCubeLod_instead #define texture2DGradEXT texture2DGradEXT_is_no_longer_supported_use_texture2DGrad_instead #define texture2DProjGradEXT texture2DProjGradEXT_is_no_longer_supported_use_texture2DProjGrad_instead #define textureCubeGradEXT textureCubeGradEXT_is_no_longer_supported_use_textureCubeGrad_instead #define textureShadow(res, uv) textureGrad(res, uv, vec2(1, 1), vec2(1, 1)) #define SHADOWMAP_PASS(name) name #define SHADOWMAP_ACCEPT(name) sampler2DShadow name #define TEXTURE_PASS(name) name #define TEXTURE_ACCEPT(name) sampler2D name #define TEXTURE_ACCEPT_HIGHP(name) highp sampler2D name #define GL2 `; var gles3VS = ` #extension GL_ANGLE_multi_draw : enable #define attribute in #define varying out #define texture2D texture #define utexture2D texture #define itexture2D texture #define GL2 #define VERTEXSHADER #define TEXTURE_PASS(name) name #define TEXTURE_ACCEPT(name) sampler2D name #define TEXTURE_ACCEPT_HIGHP(name) highp sampler2D name `; var webgpuPS$1 = ` #extension GL_EXT_samplerless_texture_functions : require #ifndef outType_0 #define outType_0 vec4 #endif #ifndef outType_1 #define outType_1 vec4 #endif #ifndef outType_2 #define outType_2 vec4 #endif #ifndef outType_3 #define outType_3 vec4 #endif #ifndef outType_4 #define outType_4 vec4 #endif #ifndef outType_5 #define outType_5 vec4 #endif #ifndef outType_6 #define outType_6 vec4 #endif #ifndef outType_7 #define outType_7 vec4 #endif layout(location = 0) out highp outType_0 pcFragColor0; layout(location = 1) out highp outType_1 pcFragColor1; layout(location = 2) out highp outType_2 pcFragColor2; layout(location = 3) out highp outType_3 pcFragColor3; layout(location = 4) out highp outType_4 pcFragColor4; layout(location = 5) out highp outType_5 pcFragColor5; layout(location = 6) out highp outType_6 pcFragColor6; layout(location = 7) out highp outType_7 pcFragColor7; #define gl_FragColor pcFragColor0 #define texture2D(res, uv) texture(sampler2D(res, res ## _sampler), uv) #define texture2DBias(res, uv, bias) texture(sampler2D(res, res ## _sampler), uv, bias) #define texture2DLod(res, uv, lod) textureLod(sampler2D(res, res ## _sampler), uv, lod) #define textureCube(res, uv) texture(samplerCube(res, res ## _sampler), uv) #define textureCubeLod(res, uv, lod) textureLod(samplerCube(res, res ## _sampler), uv, lod) #define textureShadow(res, uv) textureLod(sampler2DShadow(res, res ## _sampler), uv, 0.0) #define itexture2D(res, uv) texture(isampler2D(res, res ## _sampler), uv) #define utexture2D(res, uv) texture(usampler2D(res, res ## _sampler), uv) #define texture2DLodEXT texture2DLodEXT_is_no_longer_supported_use_texture2DLod_instead #define texture2DProjLodEXT texture2DProjLodEXT_is_no_longer_supported_use_texture2DProjLod #define textureCubeLodEXT textureCubeLodEXT_is_no_longer_supported_use_textureCubeLod_instead #define texture2DGradEXT texture2DGradEXT_is_no_longer_supported_use_texture2DGrad_instead #define texture2DProjGradEXT texture2DProjGradEXT_is_no_longer_supported_use_texture2DProjGrad_instead #define textureCubeGradEXT textureCubeGradEXT_is_no_longer_supported_use_textureCubeGrad_instead #define SHADOWMAP_PASS(name) name, name ## _sampler #define SHADOWMAP_ACCEPT(name) texture2D name, sampler name ## _sampler #define TEXTURE_PASS(name) name, name ## _sampler #define TEXTURE_ACCEPT(name) texture2D name, sampler name ## _sampler #define TEXTURE_ACCEPT_HIGHP TEXTURE_ACCEPT #define GL2 #define WEBGPU `; var webgpuVS$1 = ` #extension GL_EXT_samplerless_texture_functions : require #define texture2D(res, uv) texture(sampler2D(res, res ## _sampler), uv) #define itexture2D(res, uv) texture(isampler2D(res, res ## _sampler), uv) #define utexture2D(res, uv) texture(usampler2D(res, res ## _sampler), uv) #define TEXTURE_PASS(name) name, name ## _sampler #define TEXTURE_ACCEPT(name) texture2D name, sampler name ## _sampler #define TEXTURE_ACCEPT_HIGHP TEXTURE_ACCEPT #define GL2 #define WEBGPU #define VERTEXSHADER #define gl_VertexID gl_VertexIndex #define gl_InstanceID gl_InstanceIndex `; var webgpuPS = ` `; var webgpuVS = ` #define VERTEXSHADER `; var sharedGLSL = ` vec2 getGrabScreenPos(vec4 clipPos) { vec2 uv = (clipPos.xy / clipPos.w) * 0.5 + 0.5; #ifdef WEBGPU uv.y = 1.0 - uv.y; #endif return uv; } vec2 getImageEffectUV(vec2 uv) { #ifdef WEBGPU uv.y = 1.0 - uv.y; #endif return uv; } `; var sharedWGSL = ` #define WEBGPU fn getGrabScreenPos(clipPos: vec4) -> vec2 { var uv: vec2 = (clipPos.xy / clipPos.w) * 0.5 + vec2(0.5); uv.y = 1.0 - uv.y; return uv; } fn getImageEffectUV(uv: vec2) -> vec2 { var modifiedUV: vec2 = uv; modifiedUV.y = 1.0 - modifiedUV.y; return modifiedUV; } struct WrappedF32 { @size(16) element: f32 } struct WrappedI32 { @size(16) element: i32 } struct WrappedU32 { @size(16) element: u32 } struct WrappedVec2F { @size(16) element: vec2f } struct WrappedVec2I { @size(16) element: vec2i } struct WrappedVec2U { @size(16) element: vec2u } `; const _attrib2Semantic = { vertex_position: SEMANTIC_POSITION, vertex_normal: SEMANTIC_NORMAL, vertex_tangent: SEMANTIC_TANGENT, vertex_texCoord0: SEMANTIC_TEXCOORD0, vertex_texCoord1: SEMANTIC_TEXCOORD1, vertex_texCoord2: SEMANTIC_TEXCOORD2, vertex_texCoord3: SEMANTIC_TEXCOORD3, vertex_texCoord4: SEMANTIC_TEXCOORD4, vertex_texCoord5: SEMANTIC_TEXCOORD5, vertex_texCoord6: SEMANTIC_TEXCOORD6, vertex_texCoord7: SEMANTIC_TEXCOORD7, vertex_color: SEMANTIC_COLOR, vertex_boneIndices: SEMANTIC_BLENDINDICES, vertex_boneWeights: SEMANTIC_BLENDWEIGHT }; class ShaderDefinitionUtils { static createDefinition(device, options) { const normalizedOutputTypes = (options)=>{ let fragmentOutputTypes = options.fragmentOutputTypes ?? 'vec4'; if (!Array.isArray(fragmentOutputTypes)) { fragmentOutputTypes = [ fragmentOutputTypes ]; } return fragmentOutputTypes; }; const getDefines = (gpu, gl2, isVertex, options)=>{ const deviceIntro = device.isWebGPU ? gpu : gl2; let attachmentsDefine = ''; if (!isVertex) { const fragmentOutputTypes = normalizedOutputTypes(options); for(let i = 0; i < device.maxColorAttachments; i++){ attachmentsDefine += `#define COLOR_ATTACHMENT_${i}\n`; const outType = fragmentOutputTypes[i] ?? 'vec4'; attachmentsDefine += `#define outType_${i} ${outType}\n`; } } return attachmentsDefine + deviceIntro; }; const getDefinesWgsl = (isVertex, options)=>{ let code = ''; if (!isVertex && device.supportsPrimitiveIndex) { code += 'enable primitive_index;\n'; } if (!isVertex) { const fragmentOutputTypes = normalizedOutputTypes(options); for(let i = 0; i < device.maxColorAttachments; i++){ const glslOutType = fragmentOutputTypes[i] ?? 'vec4'; const wgslOutType = primitiveGlslToWgslTypeMap.get(glslOutType); code += `alias pcOutType${i} = ${wgslOutType};\n`; } } return code; }; const name = options.name ?? 'Untitled'; let vertCode; let fragCode; const vertexDefinesCode = ShaderDefinitionUtils.getDefinesCode(device, options.vertexDefines); const fragmentDefinesCode = ShaderDefinitionUtils.getDefinesCode(device, options.fragmentDefines); const wgsl = options.shaderLanguage === SHADERLANGUAGE_WGSL; if (wgsl) { vertCode = ` ${getDefinesWgsl(true, options)} ${vertexDefinesCode} ${webgpuVS} ${sharedWGSL} ${options.vertexCode} `; fragCode = ` ${getDefinesWgsl(false, options)} ${fragmentDefinesCode} ${webgpuPS} ${sharedWGSL} ${options.fragmentCode} `; } else { vertCode = `${ShaderDefinitionUtils.versionCode(device) + getDefines(webgpuVS$1, gles3VS, true, options) + vertexDefinesCode + ShaderDefinitionUtils.precisionCode(device)} ${sharedGLSL} ${ShaderDefinitionUtils.getShaderNameCode(name)} ${options.vertexCode}`; fragCode = `${(options.fragmentPreamble || '') + ShaderDefinitionUtils.versionCode(device) + getDefines(webgpuPS$1, gles3PS, false, options) + fragmentDefinesCode + ShaderDefinitionUtils.precisionCode(device)} ${sharedGLSL} ${ShaderDefinitionUtils.getShaderNameCode(name)} ${options.fragmentCode}`; } return { name: name, shaderLanguage: options.shaderLanguage ?? SHADERLANGUAGE_GLSL, attributes: options.attributes, vshader: vertCode, vincludes: options.vertexIncludes, fincludes: options.fragmentIncludes, fshader: fragCode, feedbackVaryings: options.feedbackVaryings, useTransformFeedback: options.useTransformFeedback, meshUniformBufferFormat: options.meshUniformBufferFormat, meshBindGroupFormat: options.meshBindGroupFormat }; } static getDefinesCode(device, defines) { let code = ''; device.capsDefines.forEach((value, key)=>{ code += `#define ${key} ${value}\n`; }); code += '\n'; defines?.forEach((value, key)=>{ code += `#define ${key} ${value}\n`; }); code += '\n'; return code; } static getShaderNameCode(name) { return `#define SHADER_NAME ${name}\n`; } static versionCode(device) { return device.isWebGPU ? '#version 450\n' : '#version 300 es\n'; } static precisionCode(device, forcePrecision) { if (forcePrecision && forcePrecision !== 'highp' && forcePrecision !== 'mediump' && forcePrecision !== 'lowp') { forcePrecision = null; } if (forcePrecision) { if (forcePrecision === 'highp' && device.maxPrecision !== 'highp') { forcePrecision = 'mediump'; } if (forcePrecision === 'mediump' && device.maxPrecision === 'lowp') { forcePrecision = 'lowp'; } } const precision = forcePrecision ? forcePrecision : device.precision; const code = ` precision ${precision} float; precision ${precision} int; precision ${precision} usampler2D; precision ${precision} isampler2D; precision ${precision} sampler2DShadow; precision ${precision} samplerCubeShadow; precision ${precision} sampler2DArray; `; return code; } static collectAttributes(vsCode) { const attribs = {}; let attrs = 0; let found = vsCode.indexOf('attribute'); while(found >= 0){ if (found > 0 && vsCode[found - 1] === '/') break; let ignore = false; if (found > 0) { let startOfLine = vsCode.lastIndexOf('\n', found); startOfLine = startOfLine !== -1 ? startOfLine + 1 : 0; const lineStartString = vsCode.substring(startOfLine, found); if (lineStartString.includes('#')) { ignore = true; } } if (!ignore) { const endOfLine = vsCode.indexOf(';', found); const startOfAttribName = vsCode.lastIndexOf(' ', endOfLine); const attribName = vsCode.substring(startOfAttribName + 1, endOfLine); if (attribs[attribName]) ; else { const semantic = _attrib2Semantic[attribName]; if (semantic !== undefined) { attribs[attribName] = semantic; } else { attribs[attribName] = `ATTR${attrs}`; attrs++; } } } found = vsCode.indexOf('attribute', found + 1); } return attribs; } } let id$8 = 0; class Shader { init() { this.ready = false; this.failed = false; } get label() { return `Shader Id ${this.id} (${this.definition.shaderLanguage === SHADERLANGUAGE_WGSL ? 'WGSL' : 'GLSL'}) ${this.name}`; } destroy() { this.device.onDestroyShader(this); this.impl.destroy(this); } loseContext() { this.init(); this.impl.loseContext(); } restoreContext() { this.impl.restoreContext(this.device, this); } constructor(graphicsDevice, definition){ this.attributes = new Map(); this.id = id$8++; this.device = graphicsDevice; this.definition = definition; this.name = definition.name || 'Untitled'; this.init(); if (definition.cshader) { const definesCode = ShaderDefinitionUtils.getDefinesCode(graphicsDevice, definition.cdefines); const cshader = definesCode + definition.cshader; definition.cshader = Preprocessor.run(cshader, definition.cincludes, { sourceName: `compute shader for ${this.label}`, stripDefines: true }); } else { const wgsl = definition.shaderLanguage === SHADERLANGUAGE_WGSL; definition.vshader = Preprocessor.run(definition.vshader, definition.vincludes, { sourceName: `vertex shader for ${this.label}`, stripDefines: wgsl }); if (definition.shaderLanguage === SHADERLANGUAGE_GLSL) { var _definition; (_definition = definition).attributes ?? (_definition.attributes = ShaderDefinitionUtils.collectAttributes(definition.vshader)); } const stripUnusedColorAttachments = graphicsDevice.isWebGL2 && (platform.name === 'osx' || platform.name === 'ios'); definition.fshader = Preprocessor.run(definition.fshader, definition.fincludes, { stripUnusedColorAttachments, stripDefines: wgsl, sourceName: `fragment shader for ${this.label}` }); if (!definition.vshader || !definition.fshader) { this.failed = true; return; } } this.impl = graphicsDevice.createShaderImpl(this); } } class UsedBuffer { } class DynamicBufferAllocation { } class DynamicBuffers { destroy() { this.gpuBuffers.forEach((gpuBuffer)=>{ gpuBuffer.destroy(this.device); }); this.gpuBuffers = null; this.stagingBuffers.forEach((stagingBuffer)=>{ stagingBuffer.destroy(this.device); }); this.stagingBuffers = null; this.usedBuffers = null; this.activeBuffer = null; } alloc(allocation, size) { if (this.activeBuffer) { const alignedStart = math.roundUp(this.activeBuffer.size, this.bufferAlignment); const space = this.bufferSize - alignedStart; if (space < size) { this.scheduleSubmit(); } } if (!this.activeBuffer) { let gpuBuffer = this.gpuBuffers.pop(); if (!gpuBuffer) { gpuBuffer = this.createBuffer(this.device, this.bufferSize, false); } let stagingBuffer = this.stagingBuffers.pop(); if (!stagingBuffer) { stagingBuffer = this.createBuffer(this.device, this.bufferSize, true); } this.activeBuffer = new UsedBuffer(); this.activeBuffer.stagingBuffer = stagingBuffer; this.activeBuffer.gpuBuffer = gpuBuffer; this.activeBuffer.offset = 0; this.activeBuffer.size = 0; } const activeBuffer = this.activeBuffer; const alignedStart = math.roundUp(activeBuffer.size, this.bufferAlignment); allocation.gpuBuffer = activeBuffer.gpuBuffer; allocation.offset = alignedStart; allocation.storage = activeBuffer.stagingBuffer.alloc(alignedStart, size); activeBuffer.size = alignedStart + size; } scheduleSubmit() { if (this.activeBuffer) { this.usedBuffers.push(this.activeBuffer); this.activeBuffer = null; } } submit() { this.scheduleSubmit(); } constructor(device, bufferSize, bufferAlignment){ this.gpuBuffers = []; this.stagingBuffers = []; this.usedBuffers = []; this.activeBuffer = null; this.device = device; this.bufferSize = bufferSize; this.bufferAlignment = bufferAlignment; } } const _updateFunctions = []; _updateFunctions[UNIFORMTYPE_FLOAT] = function(uniformBuffer, value, offset) { const dst = uniformBuffer.storageFloat32; dst[offset] = value; }; _updateFunctions[UNIFORMTYPE_VEC2] = (uniformBuffer, value, offset)=>{ const dst = uniformBuffer.storageFloat32; dst[offset] = value[0]; dst[offset + 1] = value[1]; }; _updateFunctions[UNIFORMTYPE_VEC3] = (uniformBuffer, value, offset)=>{ const dst = uniformBuffer.storageFloat32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; }; _updateFunctions[UNIFORMTYPE_VEC4] = (uniformBuffer, value, offset)=>{ const dst = uniformBuffer.storageFloat32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; dst[offset + 3] = value[3]; }; _updateFunctions[UNIFORMTYPE_INT] = function(uniformBuffer, value, offset) { const dst = uniformBuffer.storageInt32; dst[offset] = value; }; _updateFunctions[UNIFORMTYPE_IVEC2] = function(uniformBuffer, value, offset) { const dst = uniformBuffer.storageInt32; dst[offset] = value[0]; dst[offset + 1] = value[1]; }; _updateFunctions[UNIFORMTYPE_IVEC3] = function(uniformBuffer, value, offset) { const dst = uniformBuffer.storageInt32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; }; _updateFunctions[UNIFORMTYPE_IVEC4] = function(uniformBuffer, value, offset) { const dst = uniformBuffer.storageInt32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; dst[offset + 3] = value[3]; }; _updateFunctions[UNIFORMTYPE_MAT2] = (uniformBuffer, value, offset)=>{ const dst = uniformBuffer.storageFloat32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 4] = value[2]; dst[offset + 5] = value[3]; dst[offset + 8] = value[4]; dst[offset + 9] = value[5]; }; _updateFunctions[UNIFORMTYPE_MAT3] = (uniformBuffer, value, offset)=>{ const dst = uniformBuffer.storageFloat32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; dst[offset + 4] = value[3]; dst[offset + 5] = value[4]; dst[offset + 6] = value[5]; dst[offset + 8] = value[6]; dst[offset + 9] = value[7]; dst[offset + 10] = value[8]; }; _updateFunctions[UNIFORMTYPE_FLOATARRAY] = function(uniformBuffer, value, offset, count) { const dst = uniformBuffer.storageFloat32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i]; } }; _updateFunctions[UNIFORMTYPE_VEC2ARRAY] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageFloat32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i * 2]; dst[offset + i * 4 + 1] = value[i * 2 + 1]; } }; _updateFunctions[UNIFORMTYPE_VEC3ARRAY] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageFloat32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i * 3]; dst[offset + i * 4 + 1] = value[i * 3 + 1]; dst[offset + i * 4 + 2] = value[i * 3 + 2]; } }; _updateFunctions[UNIFORMTYPE_UINT] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageUint32; dst[offset] = value; }; _updateFunctions[UNIFORMTYPE_UVEC2] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageUint32; dst[offset] = value[0]; dst[offset + 1] = value[1]; }; _updateFunctions[UNIFORMTYPE_UVEC3] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageUint32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; }; _updateFunctions[UNIFORMTYPE_UVEC4] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageUint32; dst[offset] = value[0]; dst[offset + 1] = value[1]; dst[offset + 2] = value[2]; dst[offset + 3] = value[3]; }; _updateFunctions[UNIFORMTYPE_INTARRAY] = function(uniformBuffer, value, offset, count) { const dst = uniformBuffer.storageInt32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i]; } }; _updateFunctions[UNIFORMTYPE_BOOLARRAY] = _updateFunctions[UNIFORMTYPE_INTARRAY]; _updateFunctions[UNIFORMTYPE_UINTARRAY] = function(uniformBuffer, value, offset, count) { const dst = uniformBuffer.storageUint32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i]; } }; _updateFunctions[UNIFORMTYPE_IVEC2ARRAY] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageInt32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i * 2]; dst[offset + i * 4 + 1] = value[i * 2 + 1]; } }; _updateFunctions[UNIFORMTYPE_BVEC2ARRAY] = _updateFunctions[UNIFORMTYPE_IVEC2ARRAY]; _updateFunctions[UNIFORMTYPE_UVEC2ARRAY] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageUint32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i * 2]; dst[offset + i * 4 + 1] = value[i * 2 + 1]; } }; _updateFunctions[UNIFORMTYPE_IVEC3ARRAY] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageInt32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i * 3]; dst[offset + i * 4 + 1] = value[i * 3 + 1]; dst[offset + i * 4 + 2] = value[i * 3 + 2]; } }; _updateFunctions[UNIFORMTYPE_BVEC3ARRAY] = _updateFunctions[UNIFORMTYPE_IVEC3ARRAY]; _updateFunctions[UNIFORMTYPE_UVEC3ARRAY] = (uniformBuffer, value, offset, count)=>{ const dst = uniformBuffer.storageUint32; for(let i = 0; i < count; i++){ dst[offset + i * 4] = value[i * 3]; dst[offset + i * 4 + 1] = value[i * 3 + 1]; dst[offset + i * 4 + 2] = value[i * 3 + 2]; } }; class UniformBuffer { destroy() { if (this.persistent) { const device = this.device; this.impl.destroy(device); device._vram.ub -= this.format.byteSize; } } get offset() { return this.persistent ? 0 : this.allocation.offset; } assignStorage(storage) { this.storageInt32 = storage; this.storageUint32 = new Uint32Array(storage.buffer, storage.byteOffset, storage.byteLength / 4); this.storageFloat32 = new Float32Array(storage.buffer, storage.byteOffset, storage.byteLength / 4); } loseContext() { this.impl?.loseContext(); } setUniform(uniformFormat, value) { const offset = uniformFormat.offset; if (value !== null && value !== undefined) { const updateFunction = _updateFunctions[uniformFormat.updateType]; if (updateFunction) { updateFunction(this, value, offset, uniformFormat.count); } else { this.storageFloat32.set(value, offset); } } } set(name, value) { const uniformFormat = this.format.map.get(name); if (uniformFormat) { this.setUniform(uniformFormat, value); } } startUpdate(dynamicBindGroup) { if (!this.persistent) { const allocation = this.allocation; const oldGpuBuffer = allocation.gpuBuffer; this.device.dynamicBuffers.alloc(allocation, this.format.byteSize); this.assignStorage(allocation.storage); if (dynamicBindGroup) { dynamicBindGroup.bindGroup = allocation.gpuBuffer.getBindGroup(this); dynamicBindGroup.offsets[0] = allocation.offset; } if (oldGpuBuffer !== allocation.gpuBuffer) { this.renderVersionDirty = this.device.renderVersion; } } } endUpdate() { if (this.persistent) { this.impl.unlock(this); } else { this.storageFloat32 = null; this.storageInt32 = null; } } update(dynamicBindGroup) { this.startUpdate(dynamicBindGroup); const uniforms = this.format.uniforms; for(let i = 0; i < uniforms.length; i++){ const value = uniforms[i].scopeId.value; this.setUniform(uniforms[i], value); } this.endUpdate(); } constructor(graphicsDevice, format, persistent = true){ this.renderVersionDirty = 0; this.device = graphicsDevice; this.format = format; this.persistent = persistent; if (persistent) { this.impl = graphicsDevice.createUniformBufferImpl(this); const storage = new ArrayBuffer(format.byteSize); this.assignStorage(new Int32Array(storage)); graphicsDevice._vram.ub += this.format.byteSize; } else { this.allocation = new DynamicBufferAllocation(); } } } const primitive = { type: PRIMITIVE_TRISTRIP, base: 0, baseVertex: 0, count: 4, indexed: false }; class WebgpuClearRenderer { destroy() { this.shader.destroy(); this.shader = null; this.uniformBuffer.destroy(); this.uniformBuffer = null; } clear(device, renderTarget, options, defaultOptions) { options = options || defaultOptions; const flags = options.flags ?? defaultOptions.flags; if (flags !== 0) { const { uniformBuffer, dynamicBindGroup } = this; uniformBuffer.startUpdate(dynamicBindGroup); device.setBindGroup(BINDGROUP_MESH_UB, dynamicBindGroup.bindGroup, dynamicBindGroup.offsets); device.setBindGroup(BINDGROUP_MESH, device.emptyBindGroup); if (flags & CLEARFLAG_COLOR && (renderTarget.colorBuffer || renderTarget.impl.assignedColorTexture)) { const color = options.color ?? defaultOptions.color; this.colorData.set(color); device.setBlendState(BlendState.NOBLEND); } else { device.setBlendState(BlendState.NOWRITE); } uniformBuffer.set('color', this.colorData); if (flags & CLEARFLAG_DEPTH && renderTarget.depth) { const depth = options.depth ?? defaultOptions.depth; uniformBuffer.set('depth', depth); device.setDepthState(DepthState.WRITEDEPTH); } else { uniformBuffer.set('depth', 1); device.setDepthState(DepthState.NODEPTH); } if (flags & CLEARFLAG_STENCIL && renderTarget.stencil) ; uniformBuffer.endUpdate(); device.setCullMode(CULLFACE_NONE); device.setShader(this.shader); device.draw(primitive); } } constructor(device){ const code = ` struct ub_mesh { color : vec4f, depth: f32 } @group(2) @binding(0) var ubMesh : ub_mesh; var pos : array = array( vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(-1.0, -1.0), vec2(1.0, -1.0) ); struct VertexOutput { @builtin(position) position : vec4f } @vertex fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { var output : VertexOutput; output.position = vec4(pos[vertexIndex], ubMesh.depth, 1.0); return output; } @fragment fn fragmentMain() -> @location(0) vec4f { return ubMesh.color; } `; this.shader = new Shader(device, { name: 'WebGPUClearRendererShader', shaderLanguage: SHADERLANGUAGE_WGSL, vshader: code, fshader: code }); this.uniformBuffer = new UniformBuffer(device, new UniformBufferFormat(device, [ new UniformFormat('color', UNIFORMTYPE_VEC4), new UniformFormat('depth', UNIFORMTYPE_FLOAT) ]), false); this.dynamicBindGroup = new DynamicBindGroup(); this.colorData = new Float32Array(4); } } class WebgpuMipmapRenderer { destroy() { this.shader.destroy(); this.shader = null; this.pipelineCache.clear(); } generate(webgpuTexture) { const textureDescr = webgpuTexture.desc; if (textureDescr.mipLevelCount <= 1) { return; } if (webgpuTexture.texture.volume) { return; } const device = this.device; const wgpu = device.wgpu; const format = textureDescr.format; let pipeline = this.pipelineCache.get(format); if (!pipeline) { const webgpuShader = this.shader.impl; pipeline = wgpu.createRenderPipeline({ layout: 'auto', vertex: { module: webgpuShader.getVertexShaderModule(), entryPoint: webgpuShader.vertexEntryPoint }, fragment: { module: webgpuShader.getFragmentShaderModule(), entryPoint: webgpuShader.fragmentEntryPoint, targets: [ { format: format } ] }, primitive: { topology: 'triangle-strip' } }); this.pipelineCache.set(format, pipeline); } const texture = webgpuTexture.texture; const numFaces = texture.cubemap ? 6 : texture.array ? texture.arrayLength : 1; const srcViews = []; for(let face = 0; face < numFaces; face++){ srcViews.push(webgpuTexture.createView({ dimension: '2d', baseMipLevel: 0, mipLevelCount: 1, baseArrayLayer: face })); } const commandEncoder = device.getCommandEncoder(); for(let i = 1; i < textureDescr.mipLevelCount; i++){ for(let face = 0; face < numFaces; face++){ const dstView = webgpuTexture.createView({ dimension: '2d', baseMipLevel: i, mipLevelCount: 1, baseArrayLayer: face }); const passEncoder = commandEncoder.beginRenderPass({ colorAttachments: [ { view: dstView, loadOp: 'clear', storeOp: 'store' } ] }); const bindGroup = wgpu.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: this.minSampler }, { binding: 1, resource: srcViews[face] } ] }); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(4); passEncoder.end(); srcViews[face] = dstView; } } device.pipeline = null; } constructor(device){ this.pipelineCache = new Map(); this.device = device; const code = ` var pos : array = array( vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(-1.0, -1.0), vec2(1.0, -1.0) ); struct VertexOutput { @builtin(position) position : vec4f, @location(0) texCoord : vec2f }; @vertex fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { var output : VertexOutput; output.texCoord = pos[vertexIndex] * vec2f(0.5, -0.5) + vec2f(0.5); output.position = vec4f(pos[vertexIndex], 0, 1); return output; } @group(0) @binding(0) var imgSampler : sampler; @group(0) @binding(1) var img : texture_2d; @fragment fn fragmentMain(@location(0) texCoord : vec2f) -> @location(0) vec4f { return textureSample(img, imgSampler, texCoord); } `; this.shader = new Shader(device, { name: 'WebGPUMipmapRendererShader', shaderLanguage: SHADERLANGUAGE_WGSL, vshader: code, fshader: code }); this.minSampler = device.wgpu.createSampler({ minFilter: 'linear' }); } } class DynamicBuffer { getBindGroup(ub) { const ubSize = ub.format.byteSize; let bindGroup = this.bindGroupCache.get(ubSize); if (!bindGroup) { bindGroup = new BindGroup(this.device, this.bindGroupFormat, ub); bindGroup.update(); this.bindGroupCache.set(ubSize, bindGroup); } return bindGroup; } constructor(device){ this.bindGroupCache = new Map(); this.device = device; this.bindGroupFormat = new BindGroupFormat(this.device, [ new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) ]); } } class WebgpuDynamicBuffer extends DynamicBuffer { destroy(device) { device._vram.ub -= this.buffer.size; this.buffer.destroy(); this.buffer = null; } onAvailable() { this.mappedRange = this.buffer.getMappedRange(); } alloc(offset, size) { return new Int32Array(this.mappedRange, offset, size / 4); } constructor(device, size, isStaging){ super(device), this.buffer = null, this.mappedRange = null; this.buffer = device.wgpu.createBuffer({ size: size, usage: isStaging ? GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC : GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, mappedAtCreation: isStaging }); if (isStaging) { this.onAvailable(); } device._vram.ub += size; } } class WebgpuDynamicBuffers extends DynamicBuffers { createBuffer(device, size, isStaging) { return new WebgpuDynamicBuffer(device, size, isStaging); } submit() { super.submit(); const count = this.usedBuffers.length; if (count) { const device = this.device; const gpuBuffers = this.gpuBuffers; const commandEncoder = device.wgpu.createCommandEncoder(); for(let i = count - 1; i >= 0; i--){ const usedBuffer = this.usedBuffers[i]; const { stagingBuffer, gpuBuffer, offset, size } = usedBuffer; const src = stagingBuffer.buffer; src.unmap(); commandEncoder.copyBufferToBuffer(src, offset, gpuBuffer.buffer, offset, size); gpuBuffers.push(gpuBuffer); } const cb = commandEncoder.finish(); device.addCommandBuffer(cb, true); for(let i = 0; i < count; i++){ const stagingBuffer = this.usedBuffers[i].stagingBuffer; this.pendingStagingBuffers.push(stagingBuffer); } this.usedBuffers.length = 0; } } onCommandBuffersSubmitted() { const count = this.pendingStagingBuffers.length; if (count) { for(let i = 0; i < count; i++){ const stagingBuffer = this.pendingStagingBuffers[i]; stagingBuffer.buffer.mapAsync(GPUMapMode.WRITE).then(()=>{ if (this.stagingBuffers) { stagingBuffer.onAvailable(); this.stagingBuffers.push(stagingBuffer); } }); } this.pendingStagingBuffers.length = 0; } } constructor(...args){ super(...args), this.pendingStagingBuffers = []; } } class GpuProfiler { loseContext() { this.pastFrameAllocations.clear(); } set enabled(value) { this._enableRequest = value; } get enabled() { return this._enableRequest; } get passTimings() { return this._passTimings; } processEnableRequest() { if (this._enableRequest !== this._enabled) { this._enabled = this._enableRequest; if (!this._enabled) { this._frameTime = 0; } } } request(renderVersion) { this.pastFrameAllocations.set(renderVersion, this.frameAllocations); this.frameAllocations = []; } _parsePassName(name) { let parsedName = this._nameCache.get(name); if (parsedName === undefined) { if (name.startsWith('RenderPass')) { parsedName = name.substring(10); } else { parsedName = name; } this._nameCache.set(name, parsedName); } return parsedName; } report(renderVersion, timings) { if (timings) { const allocations = this.pastFrameAllocations.get(renderVersion); if (!allocations) { return; } if (timings.length > 0) { this._frameTime = timings.reduce((sum, t)=>sum + t, 0); } this._passTimings.clear(); for(let i = 0; i < allocations.length; ++i){ const name = allocations[i]; const timing = timings[i]; const parsedName = this._parsePassName(name); this._passTimings.set(parsedName, (this._passTimings.get(parsedName) || 0) + timing); } if (Tracing.get(TRACEID_GPU_TIMINGS)) { let total = 0; for(let i = 0; i < allocations.length; ++i){ allocations[i]; total += timings[i]; } } } this.pastFrameAllocations.delete(renderVersion); } getSlot(name) { if (this.frameAllocations.length >= this.maxCount) { return -1; } const slot = this.frameAllocations.length; this.frameAllocations.push(name); return slot; } get slotCount() { return this.frameAllocations.length; } constructor(){ this.frameAllocations = []; this.pastFrameAllocations = new Map(); this._enabled = false; this._enableRequest = false; this._frameTime = 0; this._passTimings = new Map(); this._nameCache = new Map(); this.maxCount = 9999; } } class WebgpuQuerySet { destroy() { this.querySet?.destroy(); this.querySet = null; this.queryBuffer?.destroy(); this.queryBuffer = null; this.activeStagingBuffer = null; this.stagingBuffers.forEach((stagingBuffer)=>{ stagingBuffer.destroy(); }); this.stagingBuffers = null; } getStagingBuffer() { let stagingBuffer = this.stagingBuffers.pop(); if (!stagingBuffer) { stagingBuffer = this.device.wgpu.createBuffer({ size: this.queryBuffer.size, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ }); } return stagingBuffer; } resolve(count) { const device = this.device; const commandEncoder = device.getCommandEncoder(); commandEncoder.resolveQuerySet(this.querySet, 0, count, this.queryBuffer, 0); const activeStagingBuffer = this.getStagingBuffer(); this.activeStagingBuffer = activeStagingBuffer; commandEncoder.copyBufferToBuffer(this.queryBuffer, 0, activeStagingBuffer, 0, this.bytesPerSlot * count); } request(count, renderVersion) { const stagingBuffer = this.activeStagingBuffer; this.activeStagingBuffer = null; return stagingBuffer.mapAsync(GPUMapMode.READ).then(()=>{ const srcTimings = new BigInt64Array(stagingBuffer.getMappedRange()); const timings = []; for(let i = 0; i < count; i++){ timings.push(Number(srcTimings[i * 2 + 1] - srcTimings[i * 2]) * 0.000001); } stagingBuffer.unmap(); this.stagingBuffers?.push(stagingBuffer); return { renderVersion, timings }; }); } constructor(device, isTimestamp, capacity){ this.stagingBuffers = []; this.activeStagingBuffer = null; this.device = device; this.capacity = capacity; this.bytesPerSlot = isTimestamp ? 8 : 4; const wgpu = device.wgpu; this.querySet = wgpu.createQuerySet({ type: isTimestamp ? 'timestamp' : 'occlusion', count: capacity }); this.queryBuffer = wgpu.createBuffer({ size: this.bytesPerSlot * capacity, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST }); } } class WebgpuGpuProfiler extends GpuProfiler { destroy() { this.timestampQueriesSet?.destroy(); this.timestampQueriesSet = null; } frameStart() { this.processEnableRequest(); } frameEnd() { if (this._enabled) { this.timestampQueriesSet?.resolve(this.slotCount * 2); } } request() { if (this._enabled) { const renderVersion = this.device.renderVersion; this.timestampQueriesSet?.request(this.slotCount, renderVersion).then((results)=>{ this.report(results.renderVersion, results.timings); }); super.request(renderVersion); } } constructor(device){ super(); this.device = device; this.maxCount = 1024; this.timestampQueriesSet = device.supportsTimestampQuery ? new WebgpuQuerySet(device, true, 2 * this.maxCount) : null; } } class WebgpuResolver { destroy() { this.shader.destroy(); this.shader = null; this.pipelineCache = null; } getPipeline(format) { let pipeline = this.pipelineCache.get(format); if (!pipeline) { pipeline = this.createPipeline(format); this.pipelineCache.set(format, pipeline); } return pipeline; } createPipeline(format) { const webgpuShader = this.shader.impl; const pipeline = this.device.wgpu.createRenderPipeline({ layout: 'auto', vertex: { module: webgpuShader.getVertexShaderModule(), entryPoint: webgpuShader.vertexEntryPoint }, fragment: { module: webgpuShader.getFragmentShaderModule(), entryPoint: webgpuShader.fragmentEntryPoint, targets: [ { format: format } ] }, primitive: { topology: 'triangle-strip' } }); return pipeline; } resolveDepth(commandEncoder, sourceTexture, destinationTexture) { const device = this.device; const wgpu = device.wgpu; const pipeline = this.getPipeline(destinationTexture.format); const numFaces = sourceTexture.depthOrArrayLayers; for(let face = 0; face < numFaces; face++){ const srcView = sourceTexture.createView({ dimension: '2d', aspect: 'depth-only', baseMipLevel: 0, mipLevelCount: 1, baseArrayLayer: face }); const dstView = destinationTexture.createView({ dimension: '2d', baseMipLevel: 0, mipLevelCount: 1, baseArrayLayer: face }); const passEncoder = commandEncoder.beginRenderPass({ colorAttachments: [ { view: dstView, loadOp: 'clear', storeOp: 'store' } ] }); const bindGroup = wgpu.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: srcView } ] }); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(4); passEncoder.end(); } device.pipeline = null; } constructor(device){ this.pipelineCache = new Map(); this.device = device; const code = ` var pos : array = array( vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(-1.0, -1.0), vec2(1.0, -1.0) ); struct VertexOutput { @builtin(position) position : vec4f, }; @vertex fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { var output : VertexOutput; output.position = vec4f(pos[vertexIndex], 0, 1); return output; } @group(0) @binding(0) var img : texture_depth_multisampled_2d; @fragment fn fragmentMain(@builtin(position) fragColor: vec4f) -> @location(0) vec4f { // load th depth value from sample index 0 var depth = textureLoad(img, vec2i(fragColor.xy), 0u); return vec4f(depth, 0.0, 0.0, 0.0); } `; this.shader = new Shader(device, { name: 'WebGPUResolverDepthShader', shaderLanguage: SHADERLANGUAGE_WGSL, vshader: code, fshader: code }); } } const _indirectDispatchEntryByteSize$1 = 3 * 4; class WebgpuCompute { destroy() { this.uniformBuffers.forEach((ub)=>ub.destroy()); this.uniformBuffers.length = 0; this.bindGroup.destroy(); this.bindGroup = null; } updateBindGroup() { const { bindGroup } = this; bindGroup.updateUniformBuffers(); bindGroup.update(); } dispatch(x, y, z) { const device = this.compute.device; device.setBindGroup(0, this.bindGroup); const passEncoder = device.passEncoder; passEncoder.setPipeline(this.pipeline); const { indirectSlotIndex, indirectBuffer, indirectFrameStamp } = this.compute; if (indirectSlotIndex >= 0) { let gpuBuffer; if (indirectBuffer) { gpuBuffer = indirectBuffer.impl.buffer; } else { gpuBuffer = device.indirectDispatchBuffer.impl.buffer; } const offset = indirectSlotIndex * _indirectDispatchEntryByteSize$1; passEncoder.dispatchWorkgroupsIndirect(gpuBuffer, offset); } else { passEncoder.dispatchWorkgroups(x, y, z); } } constructor(compute){ this.uniformBuffers = []; this.bindGroup = null; this.compute = compute; const { device, shader } = compute; const { computeBindGroupFormat, computeUniformBufferFormats } = shader.impl; this.bindGroup = new BindGroup(device, computeBindGroupFormat); if (computeUniformBufferFormats) { for(const name in computeUniformBufferFormats){ if (computeUniformBufferFormats.hasOwnProperty(name)) { const ub = new UniformBuffer(device, computeUniformBufferFormats[name], true); this.uniformBuffers.push(ub); this.bindGroup.setUniformBuffer(name, ub); } } } this.pipeline = device.computePipeline.get(shader, computeBindGroupFormat); } } let id$7 = 0; class StorageBuffer { destroy() { const device = this.device; device.buffers.delete(this); this.adjustVramSizeTracking(device._vram, -this.byteSize); this.impl.destroy(device); } adjustVramSizeTracking(vram, size) { vram.sb += size; } read(offset = 0, size = this.byteSize, data = null, immediate = false) { return this.impl.read(this.device, offset, size, data, immediate); } write(bufferOffset = 0, data, dataOffset = 0, size) { this.impl.write(this.device, bufferOffset, data, dataOffset, size); } clear(offset = 0, size = this.byteSize) { this.impl.clear(this.device, offset, size); } copy(srcBuffer, srcOffset = 0, dstOffset = 0, size = srcBuffer.byteSize - srcOffset) { const commandEncoder = this.device.getCommandEncoder(); commandEncoder.copyBufferToBuffer(srcBuffer.impl.buffer, srcOffset, this.impl.buffer, dstOffset, size); } constructor(graphicsDevice, byteSize, bufferUsage = 0, addStorageUsage = true){ this.id = id$7++; this.device = graphicsDevice; this.byteSize = byteSize; this.bufferUsage = bufferUsage; const usage = addStorageUsage ? BUFFERUSAGE_STORAGE | bufferUsage : bufferUsage; this.impl = graphicsDevice.createBufferImpl(usage); this.impl.allocate(graphicsDevice, byteSize); this.device.buffers.add(this); this.adjustVramSizeTracking(graphicsDevice._vram, this.byteSize); } } class WebgpuDrawCommands { allocate(maxCount) { if (this.gpuIndirect && this.gpuIndirect.length === 5 * maxCount) { return; } this.storage?.destroy(); this.gpuIndirect = new Uint32Array(5 * maxCount); this.gpuIndirectSigned = new Int32Array(this.gpuIndirect.buffer); this.storage = new StorageBuffer(this.device, this.gpuIndirect.byteLength, BUFFERUSAGE_INDIRECT | BUFFERUSAGE_COPY_DST); } add(i, indexOrVertexCount, instanceCount, firstIndexOrVertex, baseVertex = 0, firstInstance = 0) { const o = i * 5; this.gpuIndirect[o + 0] = indexOrVertexCount; this.gpuIndirect[o + 1] = instanceCount; this.gpuIndirect[o + 2] = firstIndexOrVertex; this.gpuIndirectSigned[o + 3] = baseVertex; this.gpuIndirect[o + 4] = firstInstance; } update(count) { if (this.storage && count > 0) { const used = count * 5; this.storage.write(0, this.gpuIndirect, 0, used); } let totalPrimitives = 0; return totalPrimitives; } destroy() { this.storage?.destroy(); this.storage = null; } constructor(device){ this.gpuIndirect = null; this.gpuIndirectSigned = null; this.storage = null; this.device = device; } } class WebgpuUploadStream { _onDeviceLost() {} destroy() { this._destroyed = true; this.availableStagingBuffers.forEach((buffer)=>buffer.destroy()); this.pendingStagingBuffers.forEach((buffer)=>buffer.destroy()); } update(minByteSize) { const pending = this.pendingStagingBuffers; for(let i = 0; i < pending.length; i++){ const buffer = pending[i]; buffer.mapAsync(GPUMapMode.WRITE).then(()=>{ if (!this._destroyed) { this.availableStagingBuffers.push(buffer); } else { buffer.destroy(); } }); } pending.length = 0; const available = this.availableStagingBuffers; for(let i = available.length - 1; i >= 0; i--){ if (available[i].size < minByteSize) { available[i].destroy(); available.splice(i, 1); } } } upload(data, target, offset, size) { if (this.useSingleBuffer) { this.uploadDirect(data, target, offset, size); } else { this.uploadStaging(data, target, offset, size); } } uploadDirect(data, target, offset, size) { const byteOffset = offset * data.BYTES_PER_ELEMENT; size * data.BYTES_PER_ELEMENT; target.write(byteOffset, data, 0, size); } uploadStaging(data, target, offset, size) { const device = this.uploadStream.device; const byteOffset = offset * data.BYTES_PER_ELEMENT; const byteSize = size * data.BYTES_PER_ELEMENT; this.update(byteSize); const buffer = this.availableStagingBuffers.pop() ?? (()=>{ const newBuffer = this.uploadStream.device.wgpu.createBuffer({ size: byteSize, usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, mappedAtCreation: true }); return newBuffer; })(); const mappedRange = buffer.getMappedRange(); new Uint8Array(mappedRange).set(new Uint8Array(data.buffer, data.byteOffset, byteSize)); buffer.unmap(); device.getCommandEncoder().copyBufferToBuffer(buffer, 0, target.impl.buffer, byteOffset, byteSize); this.pendingStagingBuffers.push(buffer); } constructor(uploadStream){ this.availableStagingBuffers = []; this.pendingStagingBuffers = []; this._destroyed = false; this.uploadStream = uploadStream; this.useSingleBuffer = uploadStream.useSingleBuffer; } } const _uniqueLocations = new Map(); const _indirectEntryByteSize = 5 * 4; const _indirectDispatchEntryByteSize = 3 * 4; class WebgpuGraphicsDevice extends GraphicsDevice { destroy() { this.clearRenderer.destroy(); this.clearRenderer = null; this.mipmapRenderer.destroy(); this.mipmapRenderer = null; this.resolver.destroy(); this.resolver = null; super.destroy(); } initDeviceCaps() { const limits = this.wgpu?.limits; this.limits = limits; this.precision = 'highp'; this.maxPrecision = 'highp'; this.maxSamples = 4; this.maxTextures = 16; this.maxTextureSize = limits.maxTextureDimension2D; this.maxCubeMapSize = limits.maxTextureDimension2D; this.maxVolumeSize = limits.maxTextureDimension3D; this.maxColorAttachments = limits.maxColorAttachments; this.maxPixelRatio = 1; this.maxAnisotropy = 16; this.fragmentUniformsCount = limits.maxUniformBufferBindingSize / 16; this.vertexUniformsCount = limits.maxUniformBufferBindingSize / 16; this.supportsUniformBuffers = true; this.supportsAreaLights = true; this.supportsGpuParticles = true; this.supportsCompute = true; this.textureFloatRenderable = true; this.textureHalfFloatRenderable = true; this.supportsImageBitmap = true; this.samples = this.backBufferAntialias ? 4 : 1; const wgslFeatures = window.navigator.gpu.wgslLanguageFeatures; this.supportsStorageTextureRead = wgslFeatures?.has('readonly_and_readwrite_storage_textures'); this.initCapsDefines(); } async initWebGpu(glslangUrl, twgslUrl) { if (!window.navigator.gpu) { throw new Error('Unable to retrieve GPU. Ensure you are using a browser that supports WebGPU rendering.'); } if (glslangUrl && twgslUrl) { const buildUrl = (srcPath)=>{ return new URL(srcPath, window.location.href).toString(); }; const results = await Promise.all([ import(/* @vite-ignore */ /* webpackIgnore: true */ `${buildUrl(twgslUrl)}`).then((module)=>twgsl(twgslUrl.replace('.js', '.wasm'))), import(/* @vite-ignore */ /* webpackIgnore: true */ `${buildUrl(glslangUrl)}`).then((module)=>module.default()) ]); this.twgsl = results[0]; this.glslang = results[1]; } return this.createDevice(); } async createDevice() { const adapterOptions = { powerPreference: this.initOptions.powerPreference !== 'default' ? this.initOptions.powerPreference : undefined, xrCompatible: this.initOptions.xrCompatible }; this.gpuAdapter = await window.navigator.gpu.requestAdapter(adapterOptions); const requiredFeatures = []; const requireFeature = (feature)=>{ const supported = this.gpuAdapter.features.has(feature); if (supported) { requiredFeatures.push(feature); } return supported; }; this.textureFloatFilterable = requireFeature('float32-filterable'); this.textureFloatBlendable = requireFeature('float32-blendable'); this.extCompressedTextureS3TC = requireFeature('texture-compression-bc'); this.extCompressedTextureS3TCSliced3D = requireFeature('texture-compression-bc-sliced-3d'); this.extCompressedTextureETC = requireFeature('texture-compression-etc2'); this.extCompressedTextureASTC = requireFeature('texture-compression-astc'); this.extCompressedTextureASTCSliced3D = requireFeature('texture-compression-astc-sliced-3d'); this.supportsTimestampQuery = requireFeature('timestamp-query'); this.supportsDepthClip = requireFeature('depth-clip-control'); this.supportsDepth32Stencil = requireFeature('depth32float-stencil8'); this.supportsIndirectFirstInstance = requireFeature('indirect-first-instance'); this.supportsShaderF16 = requireFeature('shader-f16'); this.supportsStorageRGBA8 = requireFeature('bgra8unorm-storage'); this.textureRG11B10Renderable = requireFeature('rg11b10ufloat-renderable'); this.supportsClipDistances = requireFeature('clip-distances'); this.supportsTextureFormatTier1 = requireFeature('texture-format-tier1'); this.supportsTextureFormatTier2 = requireFeature('texture-format-tier2'); this.supportsPrimitiveIndex = requireFeature('primitive-index'); const adapterLimits = this.gpuAdapter?.limits; const requiredLimits = {}; if (adapterLimits) { for(const limitName in adapterLimits){ if (limitName === 'minSubgroupSize' || limitName === 'maxSubgroupSize') { continue; } requiredLimits[limitName] = adapterLimits[limitName]; } } const deviceDescr = { requiredFeatures, requiredLimits, defaultQueue: { label: 'Default Queue' } }; this.wgpu = await this.gpuAdapter.requestDevice(deviceDescr); this.wgpu.lost?.then(this.handleDeviceLost.bind(this)); this.initDeviceCaps(); this.gpuContext = this.canvas.getContext('webgpu'); let canvasToneMapping = 'standard'; let preferredCanvasFormat = window.navigator.gpu.getPreferredCanvasFormat(); const displayFormat = this.initOptions.displayFormat; this.backBufferFormat = preferredCanvasFormat === 'rgba8unorm' ? displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8 : displayFormat === DISPLAYFORMAT_LDR_SRGB ? PIXELFORMAT_SBGRA8 : PIXELFORMAT_BGRA8; this.backBufferViewFormat = displayFormat === DISPLAYFORMAT_LDR_SRGB ? `${preferredCanvasFormat}-srgb` : preferredCanvasFormat; if (displayFormat === DISPLAYFORMAT_HDR && this.textureFloatFilterable) { const hdrMediaQuery = window.matchMedia('(dynamic-range: high)'); if (hdrMediaQuery?.matches) { this.backBufferFormat = PIXELFORMAT_RGBA16F; this.backBufferViewFormat = 'rgba16float'; preferredCanvasFormat = 'rgba16float'; this.isHdr = true; canvasToneMapping = 'extended'; } } this.canvasConfig = { device: this.wgpu, colorSpace: 'srgb', alphaMode: this.initOptions.alpha ? 'premultiplied' : 'opaque', format: preferredCanvasFormat, toneMapping: { mode: canvasToneMapping }, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, viewFormats: displayFormat === DISPLAYFORMAT_LDR_SRGB ? [ this.backBufferViewFormat ] : [] }; this.gpuContext?.configure(this.canvasConfig); this.createBackbuffer(); this.clearRenderer = new WebgpuClearRenderer(this); this.mipmapRenderer = new WebgpuMipmapRenderer(this); this.resolver = new WebgpuResolver(this); this.postInit(); return this; } async handleDeviceLost(info) { if (info.reason !== 'destroyed') { super.loseContext(); await this.createDevice(); super.restoreContext(); } } postInit() { super.postInit(); this.initializeRenderState(); this.setupPassEncoderDefaults(); this.gpuProfiler = new WebgpuGpuProfiler(this); this.dynamicBuffers = new WebgpuDynamicBuffers(this, 100 * 1024, this.limits.minUniformBufferOffsetAlignment); this.emptyBindGroup = new BindGroup(this, new BindGroupFormat(this, [])); this.emptyBindGroup.update(); } createBackbuffer() { this.supportsStencil = this.initOptions.stencil; this.backBuffer = new RenderTarget({ name: 'WebgpuFramebuffer', graphicsDevice: this, depth: this.initOptions.depth, stencil: this.supportsStencil, samples: this.samples }); this.backBuffer.impl.isBackbuffer = true; } frameStart() { super.frameStart(); this.gpuProfiler.frameStart(); this.submit(); const outColorBuffer = this.gpuContext?.getCurrentTexture?.() ?? this.externalBackbuffer?.impl.gpuTexture; if (this.backBufferSize.x !== outColorBuffer.width || this.backBufferSize.y !== outColorBuffer.height) { this.backBufferSize.set(outColorBuffer.width, outColorBuffer.height); this.backBuffer.destroy(); this.backBuffer = null; this.createBackbuffer(); } const rt = this.backBuffer; const wrt = rt.impl; wrt.setColorAttachment(0, undefined, this.backBufferViewFormat); this.initRenderTarget(rt); wrt.assignColorTexture(this, outColorBuffer); } frameEnd() { super.frameEnd(); this.gpuProfiler.frameEnd(); this.submit(); if (!this.contextLost) { this.gpuProfiler.request(); } this._indirectDrawNextIndex = 0; this._indirectDispatchNextIndex = 0; } createBufferImpl(usageFlags) { return new WebgpuBuffer(usageFlags); } createUniformBufferImpl(uniformBuffer) { return new WebgpuUniformBuffer(uniformBuffer); } createVertexBufferImpl(vertexBuffer, format, options) { return new WebgpuVertexBuffer(vertexBuffer, format, options); } createIndexBufferImpl(indexBuffer, options) { return new WebgpuIndexBuffer(indexBuffer, options); } createShaderImpl(shader) { return new WebgpuShader(shader); } createDrawCommandImpl(drawCommands) { return new WebgpuDrawCommands(this); } createTextureImpl(texture) { this.textures.add(texture); return new WebgpuTexture(texture); } createRenderTargetImpl(renderTarget) { return new WebgpuRenderTarget(renderTarget); } createUploadStreamImpl(uploadStream) { return new WebgpuUploadStream(uploadStream); } createBindGroupFormatImpl(bindGroupFormat) { return new WebgpuBindGroupFormat(bindGroupFormat); } createBindGroupImpl(bindGroup) { return new WebgpuBindGroup(); } createComputeImpl(compute) { return new WebgpuCompute(compute); } get indirectDrawBuffer() { this.allocateIndirectDrawBuffer(); return this._indirectDrawBuffer; } allocateIndirectDrawBuffer() { if (this._indirectDrawNextIndex === 0 && this._indirectDrawBufferCount < this.maxIndirectDrawCount) { this._indirectDrawBuffer?.destroy(); this._indirectDrawBuffer = null; } if (this._indirectDrawBuffer === null) { this._indirectDrawBuffer = new StorageBuffer(this, this.maxIndirectDrawCount * _indirectEntryByteSize, BUFFERUSAGE_INDIRECT | BUFFERUSAGE_COPY_DST); this._indirectDrawBufferCount = this.maxIndirectDrawCount; } } getIndirectDrawSlot(count = 1) { this.allocateIndirectDrawBuffer(); const slot = this._indirectDrawNextIndex; const nextIndex = this._indirectDrawNextIndex + count; this._indirectDrawNextIndex = nextIndex; return slot; } get indirectDispatchBuffer() { this.allocateIndirectDispatchBuffer(); return this._indirectDispatchBuffer; } allocateIndirectDispatchBuffer() { if (this._indirectDispatchNextIndex === 0 && this._indirectDispatchBufferCount < this.maxIndirectDispatchCount) { this._indirectDispatchBuffer?.destroy(); this._indirectDispatchBuffer = null; } if (this._indirectDispatchBuffer === null) { this._indirectDispatchBuffer = new StorageBuffer(this, this.maxIndirectDispatchCount * _indirectDispatchEntryByteSize, BUFFERUSAGE_INDIRECT | BUFFERUSAGE_COPY_DST); this._indirectDispatchBufferCount = this.maxIndirectDispatchCount; } } getIndirectDispatchSlot(count = 1) { this.allocateIndirectDispatchBuffer(); const slot = this._indirectDispatchNextIndex; const nextIndex = this._indirectDispatchNextIndex + count; this._indirectDispatchNextIndex = nextIndex; return slot; } setBindGroup(index, bindGroup, offsets) { if (this.passEncoder) { this.passEncoder.setBindGroup(index, bindGroup.impl.bindGroup, offsets ?? bindGroup.uniformBufferOffsets); this.bindGroupFormats[index] = bindGroup.format.impl; } } submitVertexBuffer(vertexBuffer, slot) { const format = vertexBuffer.format; const { interleaved, elements } = format; const elementCount = elements.length; const vbBuffer = vertexBuffer.impl.buffer; if (interleaved) { this.passEncoder.setVertexBuffer(slot, vbBuffer); return 1; } for(let i = 0; i < elementCount; i++){ this.passEncoder.setVertexBuffer(slot + i, vbBuffer, elements[i].offset); } return elementCount; } validateVBLocations(vb0, vb1) { const validateVB = (vb)=>{ const { elements } = vb.format; for(let i = 0; i < elements.length; i++){ const name = elements[i].name; const location = semanticToLocation[name]; if (_uniqueLocations.has(location)) ; _uniqueLocations.set(location, name); } }; validateVB(vb0); validateVB(vb1); _uniqueLocations.clear(); } draw(primitive, indexBuffer, numInstances = 1, drawCommands, first = true, last = true) { if (this.shader.ready && !this.shader.failed) { const passEncoder = this.passEncoder; let pipeline = this.pipeline; const vb0 = this.vertexBuffers[0]; const vb1 = this.vertexBuffers[1]; if (first) { if (vb0) { const vbSlot = this.submitVertexBuffer(vb0, 0); if (vb1) { this.submitVertexBuffer(vb1, vbSlot); } } pipeline = this.renderPipeline.get(primitive, vb0?.format, vb1?.format, indexBuffer?.format, this.shader, this.renderTarget, this.bindGroupFormats, this.blendState, this.depthState, this.cullMode, this.stencilEnabled, this.stencilFront, this.stencilBack); if (this.pipeline !== pipeline) { this.pipeline = pipeline; passEncoder.setPipeline(pipeline); } } if (indexBuffer) { passEncoder.setIndexBuffer(indexBuffer.impl.buffer, indexBuffer.impl.format); } if (drawCommands) { const storage = drawCommands.impl?.storage ?? this.indirectDrawBuffer; const indirectBuffer = storage.impl.buffer; const drawsCount = drawCommands.count; for(let d = 0; d < drawsCount; d++){ const indirectOffset = (drawCommands.slotIndex + d) * _indirectEntryByteSize; if (indexBuffer) { passEncoder.drawIndexedIndirect(indirectBuffer, indirectOffset); } else { passEncoder.drawIndirect(indirectBuffer, indirectOffset); } } } else { if (indexBuffer) { passEncoder.drawIndexed(primitive.count, numInstances, primitive.base, primitive.baseVertex ?? 0, 0); } else { passEncoder.draw(primitive.count, numInstances, primitive.base, 0); } } this._drawCallsPerFrame++; } if (last) { this.clearVertexBuffer(); this.pipeline = null; } } setShader(shader, asyncCompile = false) { if (shader !== this.shader) { this.shader = shader; } } setBlendState(blendState) { this.blendState.copy(blendState); } setDepthState(depthState) { this.depthState.copy(depthState); } setStencilState(stencilFront, stencilBack) { if (stencilFront || stencilBack) { this.stencilEnabled = true; this.stencilFront.copy(stencilFront ?? StencilParameters.DEFAULT); this.stencilBack.copy(stencilBack ?? StencilParameters.DEFAULT); const ref = this.stencilFront.ref; if (this.stencilRef !== ref) { this.stencilRef = ref; this.passEncoder.setStencilReference(ref); } } else { this.stencilEnabled = false; } } setBlendColor(r, g, b, a) { const c = this.blendColor; if (r !== c.r || g !== c.g || b !== c.b || a !== c.a) { c.set(r, g, b, a); this.passEncoder.setBlendConstant(c); } } setCullMode(cullMode) { this.cullMode = cullMode; } setAlphaToCoverage(state) {} initializeContextCaches() { super.initializeContextCaches(); } setupPassEncoderDefaults() { this.pipeline = null; this.stencilRef = 0; this.blendColor.set(0, 0, 0, 0); } _uploadDirtyTextures() { this.texturesToUpload.forEach((texture)=>{ if (texture._needsUpload || texture._needsMipmapsUpload) { texture.upload(); } }); this.texturesToUpload.clear(); } setupTimeStampWrites(passDesc, name) { if (this.gpuProfiler._enabled) { if (this.gpuProfiler.timestampQueriesSet) { const slot = this.gpuProfiler.getSlot(name); if (slot === -1) ; else { passDesc = passDesc ?? {}; passDesc.timestampWrites = { querySet: this.gpuProfiler.timestampQueriesSet.querySet, beginningOfPassWriteIndex: slot * 2, endOfPassWriteIndex: slot * 2 + 1 }; } } } return passDesc; } startRenderPass(renderPass) { this._uploadDirtyTextures(); const rt = renderPass.renderTarget || this.backBuffer; this.renderTarget = rt; const wrt = rt.impl; if (rt !== this.backBuffer) { this.initRenderTarget(rt); } wrt.setupForRenderPass(renderPass, rt); const renderPassDesc = wrt.renderPassDescriptor; this.setupTimeStampWrites(renderPassDesc, renderPass.name); const commandEncoder = this.getCommandEncoder(); this.passEncoder = commandEncoder.beginRenderPass(renderPassDesc); this.passEncoder.label = `${renderPass.name}-PassEncoder RT:${rt.name}`; this.setupPassEncoderDefaults(); const { width, height } = rt; this.setViewport(0, 0, width, height); this.setScissor(0, 0, width, height); this.insideRenderPass = true; } endRenderPass(renderPass) { this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; this.bindGroupFormats.length = 0; const target = this.renderTarget; if (target) { if (target.depthBuffer && renderPass.depthStencilOps.resolveDepth) { if (renderPass.samples > 1 && target.autoResolve) { const depthAttachment = target.impl.depthAttachment; const destTexture = target.depthBuffer.impl.gpuTexture; if (depthAttachment && destTexture) { this.resolver.resolveDepth(this.commandEncoder, depthAttachment.multisampledDepthBuffer, destTexture); } } } } for(let i = 0; i < renderPass.colorArrayOps.length; i++){ const colorOps = renderPass.colorArrayOps[i]; if (colorOps.genMipmaps) { this.mipmapRenderer.generate(renderPass.renderTarget._colorBuffers[i].impl); } } } startComputePass(name) { this._uploadDirtyTextures(); this.pipeline = null; const computePassDesc = this.setupTimeStampWrites(undefined, name); const commandEncoder = this.getCommandEncoder(); this.passEncoder = commandEncoder.beginComputePass(computePassDesc); this.insideRenderPass = true; } endComputePass() { this.passEncoder.end(); this.passEncoder = null; this.insideRenderPass = false; this.bindGroupFormats.length = 0; } computeDispatch(computes, name = 'Unnamed') { this.startComputePass(name); for(let i = 0; i < computes.length; i++){ const compute = computes[i]; compute.applyParameters(); compute.impl.updateBindGroup(); } for(let i = 0; i < computes.length; i++){ const compute = computes[i]; compute.impl.dispatch(compute.countX, compute.countY, compute.countZ); } this.endComputePass(); } getCommandEncoder() { let commandEncoder = this.commandEncoder; if (!commandEncoder) { commandEncoder = this.wgpu.createCommandEncoder(); this.commandEncoder = commandEncoder; } return commandEncoder; } endCommandEncoder() { const { commandEncoder } = this; if (commandEncoder) { const cb = commandEncoder.finish(); this.addCommandBuffer(cb); this.commandEncoder = null; } } addCommandBuffer(commandBuffer, front = false) { if (front) { this.commandBuffers.unshift(commandBuffer); } else { this.commandBuffers.push(commandBuffer); } } submit() { this.endCommandEncoder(); if (this.commandBuffers.length > 0) { this.dynamicBuffers.submit(); this.wgpu.queue.submit(this.commandBuffers); this.commandBuffers.length = 0; this.dynamicBuffers.onCommandBuffersSubmitted(); } } clear(options) { if (options.flags) { this.clearRenderer.clear(this, this.renderTarget, options, this.defaultClearOptions); } } setViewport(x, y, w, h) { if (this.passEncoder) { if (!this.renderTarget.flipY) { y = this.renderTarget.height - y - h; } this.vx = x; this.vy = y; this.vw = w; this.vh = h; this.passEncoder.setViewport(x, y, w, h, 0, 1); } } setScissor(x, y, w, h) { if (this.passEncoder) { if (!this.renderTarget.flipY) { y = this.renderTarget.height - y - h; } this.sx = x; this.sy = y; this.sw = w; this.sh = h; this.passEncoder.setScissorRect(x, y, w, h); } } clearStorageBuffer(storageBuffer, offset = 0, size = storageBuffer.byteSize) { const commandEncoder = this.getCommandEncoder(); commandEncoder.clearBuffer(storageBuffer.buffer, offset, size); } readStorageBuffer(storageBuffer, offset = 0, size = storageBuffer.byteSize - offset, data = null, immediate = false) { const stagingBuffer = this.createBufferImpl(BUFFERUSAGE_READ | BUFFERUSAGE_COPY_DST); stagingBuffer.allocate(this, size); const destBuffer = stagingBuffer.buffer; const commandEncoder = this.getCommandEncoder(); commandEncoder.copyBufferToBuffer(storageBuffer.buffer, offset, destBuffer, 0, size); return this.readBuffer(stagingBuffer, size, data, immediate); } readBuffer(stagingBuffer, size, data = null, immediate = false) { const destBuffer = stagingBuffer.buffer; return new Promise((resolve, reject)=>{ const read = ()=>{ destBuffer?.mapAsync(GPUMapMode.READ).then(()=>{ data ?? (data = new Uint8Array(size)); const copySrc = destBuffer.getMappedRange(0, size); const srcType = data.constructor; data.set(new srcType(copySrc)); destBuffer.unmap(); stagingBuffer.destroy(this); resolve(data); }); }; if (immediate) { this.submit(); read(); } else { setTimeout(()=>{ read(); }); } }); } writeStorageBuffer(storageBuffer, bufferOffset = 0, data, dataOffset = 0, size) { this.wgpu.queue.writeBuffer(storageBuffer.buffer, bufferOffset, data, dataOffset, size); } copyRenderTarget(source, dest, color, depth) { const copySize = { width: source ? source.width : dest.width, height: source ? source.height : dest.height, depthOrArrayLayers: 1 }; const commandEncoder = this.getCommandEncoder(); if (color) { const copySrc = { texture: source ? source.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: source ? source.mipLevel : 0 }; const copyDst = { texture: dest ? dest.colorBuffer.impl.gpuTexture : this.backBuffer.impl.assignedColorTexture, mipLevel: dest ? dest.mipLevel : 0 }; commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } if (depth) { const sourceRT = source ? source : this.renderTarget; const sourceTexture = sourceRT.impl.depthAttachment.depthTexture; const sourceMipLevel = sourceRT.mipLevel; if (source.samples > 1) { const destTexture = dest.colorBuffer.impl.gpuTexture; this.resolver.resolveDepth(commandEncoder, sourceTexture, destTexture); } else { const destTexture = dest ? dest.depthBuffer.impl.gpuTexture : this.renderTarget.impl.depthAttachment.depthTexture; const destMipLevel = dest ? dest.mipLevel : this.renderTarget.mipLevel; const copySrc = { texture: sourceTexture, mipLevel: sourceMipLevel }; const copyDst = { texture: destTexture, mipLevel: destMipLevel }; commandEncoder.copyTextureToTexture(copySrc, copyDst, copySize); } } return true; } get hasTranspilers() { return this.glslang && this.twgsl; } constructor(canvas, options = {}){ super(canvas, options), this.renderPipeline = new WebgpuRenderPipeline(this), this.computePipeline = new WebgpuComputePipeline(this), this._indirectDrawBuffer = null, this._indirectDrawBufferCount = 0, this._indirectDrawNextIndex = 0, this._indirectDispatchBuffer = null, this._indirectDispatchBufferCount = 0, this._indirectDispatchNextIndex = 0, this.pipeline = null, this.bindGroupFormats = [], this.commandEncoder = null, this.commandBuffers = [], this.glslang = null, this.twgsl = null; options = this.initOptions; options.alpha = options.alpha ?? true; this.backBufferAntialias = options.antialias ?? false; this.isWebGPU = true; this._deviceType = DEVICETYPE_WEBGPU; this.scope.resolve(UNUSED_UNIFORM_NAME).setValue(0); } } class WebglBuffer { destroy(device) { if (this.bufferId) { device.gl.deleteBuffer(this.bufferId); this.bufferId = null; } } get initialized() { return !!this.bufferId; } loseContext() { this.bufferId = null; } unlock(device, usage, target, storage) { const gl = device.gl; if (!this.bufferId) { let glUsage; switch(usage){ case BUFFER_STATIC: glUsage = gl.STATIC_DRAW; break; case BUFFER_DYNAMIC: glUsage = gl.DYNAMIC_DRAW; break; case BUFFER_STREAM: glUsage = gl.STREAM_DRAW; break; case BUFFER_GPUDYNAMIC: glUsage = gl.DYNAMIC_COPY; break; } this.bufferId = gl.createBuffer(); gl.bindBuffer(target, this.bufferId); gl.bufferData(target, storage, glUsage); } else { gl.bindBuffer(target, this.bufferId); gl.bufferSubData(target, 0, storage); } } constructor(){ this.bufferId = null; } } class WebglVertexBuffer extends WebglBuffer { destroy(device) { super.destroy(device); device.unbindVertexArray(); } loseContext() { super.loseContext(); this.vao = null; } unlock(vertexBuffer) { const device = vertexBuffer.device; super.unlock(device, vertexBuffer.usage, device.gl.ARRAY_BUFFER, vertexBuffer.storage); } constructor(...args){ super(...args), this.vao = null; } } class WebglIndexBuffer extends WebglBuffer { unlock(indexBuffer) { const device = indexBuffer.device; super.unlock(device, indexBuffer.usage, device.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.storage); } constructor(indexBuffer){ super(); const gl = indexBuffer.device.gl; const format = indexBuffer.format; if (format === INDEXFORMAT_UINT8) { this.glFormat = gl.UNSIGNED_BYTE; } else if (format === INDEXFORMAT_UINT16) { this.glFormat = gl.UNSIGNED_SHORT; } else if (format === INDEXFORMAT_UINT32) { this.glFormat = gl.UNSIGNED_INT; } } } class WebglShaderInput { constructor(graphicsDevice, name, type, locationId){ this.locationId = locationId; this.scopeId = graphicsDevice.scope.resolve(name); this.version = new Version(); if (name.substring(name.length - 3) === '[0]') { switch(type){ case UNIFORMTYPE_FLOAT: type = UNIFORMTYPE_FLOATARRAY; break; case UNIFORMTYPE_INT: type = UNIFORMTYPE_INTARRAY; break; case UNIFORMTYPE_UINT: type = UNIFORMTYPE_UINTARRAY; break; case UNIFORMTYPE_BOOL: type = UNIFORMTYPE_BOOLARRAY; break; case UNIFORMTYPE_VEC2: type = UNIFORMTYPE_VEC2ARRAY; break; case UNIFORMTYPE_IVEC2: type = UNIFORMTYPE_IVEC2ARRAY; break; case UNIFORMTYPE_UVEC2: type = UNIFORMTYPE_UVEC2ARRAY; break; case UNIFORMTYPE_BVEC2: type = UNIFORMTYPE_BVEC2ARRAY; break; case UNIFORMTYPE_VEC3: type = UNIFORMTYPE_VEC3ARRAY; break; case UNIFORMTYPE_IVEC3: type = UNIFORMTYPE_IVEC3ARRAY; break; case UNIFORMTYPE_UVEC3: type = UNIFORMTYPE_UVEC3ARRAY; break; case UNIFORMTYPE_BVEC3: type = UNIFORMTYPE_BVEC3ARRAY; break; case UNIFORMTYPE_VEC4: type = UNIFORMTYPE_VEC4ARRAY; break; case UNIFORMTYPE_IVEC4: type = UNIFORMTYPE_IVEC4ARRAY; break; case UNIFORMTYPE_UVEC4: type = UNIFORMTYPE_UVEC4ARRAY; break; case UNIFORMTYPE_BVEC4: type = UNIFORMTYPE_BVEC4ARRAY; break; } } this.dataType = type; this.value = [ null, null, null, null ]; this.array = []; } } const _vertexShaderBuiltins = new Set([ 'gl_VertexID', 'gl_InstanceID', 'gl_DrawID', 'gl_BaseVertex', 'gl_BaseInstance' ]); class CompiledShaderCache { destroy(device) { this.map.forEach((shader)=>{ device.gl.deleteShader(shader); }); } loseContext(device) { this.map.clear(); } constructor(){ this.map = new Map(); } } const _vertexShaderCache = new DeviceCache(); const _fragmentShaderCache = new DeviceCache(); class WebglShader { destroy(shader) { if (this.glProgram) { shader.device.gl.deleteProgram(this.glProgram); this.glProgram = null; } } init() { this.uniforms = []; this.samplers = []; this.attributes = []; this.glProgram = null; this.glVertexShader = null; this.glFragmentShader = null; } loseContext() { this.init(); } restoreContext(device, shader) { this.compile(device, shader); this.link(device, shader); } compile(device, shader) { const definition = shader.definition; this.glVertexShader = this._compileShaderSource(device, definition.vshader, true); this.glFragmentShader = this._compileShaderSource(device, definition.fshader, false); } link(device, shader) { if (this.glProgram) { return; } const gl = device.gl; if (gl.isContextLost()) { return; } const glProgram = gl.createProgram(); this.glProgram = glProgram; gl.attachShader(glProgram, this.glVertexShader); gl.attachShader(glProgram, this.glFragmentShader); const definition = shader.definition; const attrs = definition.attributes; if (definition.useTransformFeedback) { let outNames = definition.feedbackVaryings; if (!outNames) { outNames = []; for(const attr in attrs){ if (attrs.hasOwnProperty(attr)) { outNames.push(`out_${attr}`); } } } gl.transformFeedbackVaryings(glProgram, outNames, gl.INTERLEAVED_ATTRIBS); } for(const attr in attrs){ if (attrs.hasOwnProperty(attr)) { const semantic = attrs[attr]; const loc = semanticToLocation[semantic]; gl.bindAttribLocation(glProgram, loc, attr); } } gl.linkProgram(glProgram); } _compileShaderSource(device, src, isVertexShader) { const gl = device.gl; if (gl.isContextLost()) { return null; } const shaderDeviceCache = isVertexShader ? _vertexShaderCache : _fragmentShaderCache; const shaderCache = shaderDeviceCache.get(device, ()=>{ return new CompiledShaderCache(); }); let glShader = shaderCache.map.get(src); if (!glShader) { glShader = gl.createShader(isVertexShader ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER); gl.shaderSource(glShader, src); gl.compileShader(glShader); shaderCache.map.set(src, glShader); } return glShader; } finalize(device, shader) { const gl = device.gl; if (gl.isContextLost()) { return true; } const glProgram = this.glProgram; const definition = shader.definition; const linkStatus = gl.getProgramParameter(glProgram, gl.LINK_STATUS); if (!linkStatus) { if (!this._isCompiled(device, shader, this.glVertexShader, definition.vshader, 'vertex')) { return false; } if (!this._isCompiled(device, shader, this.glFragmentShader, definition.fshader, 'fragment')) { return false; } const message = `Failed to link shader program. Error: ${gl.getProgramInfoLog(glProgram)}`; console.error(message); return false; } const numAttributes = gl.getProgramParameter(glProgram, gl.ACTIVE_ATTRIBUTES); shader.attributes.clear(); for(let i = 0; i < numAttributes; i++){ const info = gl.getActiveAttrib(glProgram, i); const location = gl.getAttribLocation(glProgram, info.name); if (_vertexShaderBuiltins.has(info.name)) { continue; } if (definition.attributes[info.name] === undefined) { console.error(`Vertex shader attribute "${info.name}" is not mapped to a semantic in shader definition, shader [${shader.label}]`, shader); shader.failed = true; } else { shader.attributes.set(location, info.name); } } const samplerTypes = device._samplerTypes; const numUniforms = gl.getProgramParameter(glProgram, gl.ACTIVE_UNIFORMS); for(let i = 0; i < numUniforms; i++){ const info = gl.getActiveUniform(glProgram, i); const location = gl.getUniformLocation(glProgram, info.name); if (_vertexShaderBuiltins.has(info.name)) { continue; } const shaderInput = new WebglShaderInput(device, info.name, device.pcUniformType[info.type], location); if (samplerTypes.has(info.type)) { this.samplers.push(shaderInput); } else { this.uniforms.push(shaderInput); } } shader.ready = true; return true; } _isCompiled(device, shader, glShader, source, shaderType) { const gl = device.gl; if (!gl.getShaderParameter(glShader, gl.COMPILE_STATUS)) { const infoLog = gl.getShaderInfoLog(glShader); const [code, error] = this._processError(source, infoLog); const message = `Failed to compile ${shaderType} shader:\n\n${infoLog}\n${code} while rendering ${ void 0}`; console.error(message); return false; } return true; } isLinked(device) { const { extParallelShaderCompile } = device; if (extParallelShaderCompile) { return device.gl.getProgramParameter(this.glProgram, extParallelShaderCompile.COMPLETION_STATUS_KHR); } return true; } _processError(src, infoLog) { const error = {}; let code = ''; if (src) { const lines = src.split('\n'); let from = 0; let to = lines.length; if (infoLog && infoLog.startsWith('ERROR:')) { const match = infoLog.match(/^ERROR:\s(\d+):(\d+):\s*(.+)/); if (match) { error.message = match[3]; error.line = parseInt(match[2], 10); from = Math.max(0, error.line - 6); to = Math.min(lines.length, error.line + 5); } } for(let i = from; i < to; i++){ const linePrefix = i + 1 === error.line ? '> ' : ' '; code += `${linePrefix}${i + 1}:\t${lines[i]}\n`; } error.source = src; } return [ code, error ]; } constructor(shader){ this.compileDuration = 0; this.init(); this.compile(shader.device, shader); this.link(shader.device, shader); shader.device.shaders.push(shader); } } class WebglDrawCommands { allocate(maxCount) { if (this.glCounts && this.glCounts.length === maxCount) { return; } this.glCounts = new Int32Array(maxCount); this.glOffsetsBytes = new Int32Array(maxCount); this.glInstanceCounts = new Int32Array(maxCount); } add(i, indexOrVertexCount, instanceCount, firstIndexOrVertex) { this.glCounts[i] = indexOrVertexCount; this.glOffsetsBytes[i] = firstIndexOrVertex * this.indexSizeBytes; this.glInstanceCounts[i] = instanceCount; } update(count) { let totalPrimitives = 0; return totalPrimitives; } constructor(indexSizeBytes){ this.glCounts = null; this.glOffsetsBytes = null; this.glInstanceCounts = null; this.indexSizeBytes = indexSizeBytes; } } function downsampleImage(image, size) { const srcW = image.width; const srcH = image.height; if (srcW > size || srcH > size) { const scale = size / Math.max(srcW, srcH); const dstW = Math.floor(srcW * scale); const dstH = Math.floor(srcH * scale); const canvas = document.createElement('canvas'); canvas.width = dstW; canvas.height = dstH; const context = canvas.getContext('2d'); context.drawImage(image, 0, 0, srcW, srcH, 0, 0, dstW, dstH); return canvas; } return image; } class WebglTexture { destroy(device) { if (this._glTexture) { for(let i = 0; i < device.textureUnits.length; i++){ const textureUnit = device.textureUnits[i]; for(let j = 0; j < textureUnit.length; j++){ if (textureUnit[j] === this._glTexture) { textureUnit[j] = null; } } } device.gl.deleteTexture(this._glTexture); this._glTexture = null; } } loseContext() { this._glTexture = null; } propertyChanged(flag) { this.dirtyParameterFlags |= flag; } initialize(device, texture) { const gl = device.gl; this._glTexture = gl.createTexture(); this._glTarget = texture._cubemap ? gl.TEXTURE_CUBE_MAP : texture._volume ? gl.TEXTURE_3D : texture.array ? gl.TEXTURE_2D_ARRAY : gl.TEXTURE_2D; switch(texture._format){ case PIXELFORMAT_A8: this._glFormat = gl.ALPHA; this._glInternalFormat = gl.ALPHA; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_L8: this._glFormat = gl.LUMINANCE; this._glInternalFormat = gl.LUMINANCE; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_LA8: this._glFormat = gl.LUMINANCE_ALPHA; this._glInternalFormat = gl.LUMINANCE_ALPHA; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_R8: this._glFormat = gl.RED; this._glInternalFormat = gl.R8; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_RG8: this._glFormat = gl.RG; this._glInternalFormat = gl.RG8; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_RGB565: this._glFormat = gl.RGB; this._glInternalFormat = gl.RGB565; this._glPixelType = gl.UNSIGNED_SHORT_5_6_5; break; case PIXELFORMAT_RGBA5551: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGB5_A1; this._glPixelType = gl.UNSIGNED_SHORT_5_5_5_1; break; case PIXELFORMAT_RGBA4: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGBA4; this._glPixelType = gl.UNSIGNED_SHORT_4_4_4_4; break; case PIXELFORMAT_RGB8: this._glFormat = gl.RGB; this._glInternalFormat = gl.RGB8; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_RGBA8: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGBA8; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_BGRA8: case PIXELFORMAT_SBGRA8: break; case PIXELFORMAT_RG32F: break; case PIXELFORMAT_RGB9E5: this._glFormat = gl.RGB; this._glInternalFormat = gl.RGB9_E5; this._glPixelType = gl.UNSIGNED_INT_5_9_9_9_REV; break; case PIXELFORMAT_RG8S: this._glFormat = gl.RG; this._glInternalFormat = gl.RG8_SNORM; this._glPixelType = gl.BYTE; break; case PIXELFORMAT_RGBA8S: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGBA8_SNORM; this._glPixelType = gl.BYTE; break; case PIXELFORMAT_RGB10A2: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGB10_A2; this._glPixelType = gl.UNSIGNED_INT_2_10_10_10_REV; break; case PIXELFORMAT_RGB10A2U: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGB10_A2UI; this._glPixelType = gl.UNSIGNED_INT_2_10_10_10_REV; break; case PIXELFORMAT_DXT1: this._glFormat = gl.RGB; this._glInternalFormat = device.extCompressedTextureS3TC.COMPRESSED_RGB_S3TC_DXT1_EXT; break; case PIXELFORMAT_DXT3: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTextureS3TC.COMPRESSED_RGBA_S3TC_DXT3_EXT; break; case PIXELFORMAT_DXT5: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTextureS3TC.COMPRESSED_RGBA_S3TC_DXT5_EXT; break; case PIXELFORMAT_ETC1: this._glFormat = gl.RGB; this._glInternalFormat = device.extCompressedTextureETC1.COMPRESSED_RGB_ETC1_WEBGL; break; case PIXELFORMAT_PVRTC_2BPP_RGB_1: this._glFormat = gl.RGB; this._glInternalFormat = device.extCompressedTexturePVRTC.COMPRESSED_RGB_PVRTC_2BPPV1_IMG; break; case PIXELFORMAT_PVRTC_2BPP_RGBA_1: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTexturePVRTC.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG; break; case PIXELFORMAT_PVRTC_4BPP_RGB_1: this._glFormat = gl.RGB; this._glInternalFormat = device.extCompressedTexturePVRTC.COMPRESSED_RGB_PVRTC_4BPPV1_IMG; break; case PIXELFORMAT_PVRTC_4BPP_RGBA_1: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTexturePVRTC.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG; break; case PIXELFORMAT_ETC2_RGB: this._glFormat = gl.RGB; this._glInternalFormat = device.extCompressedTextureETC.COMPRESSED_RGB8_ETC2; break; case PIXELFORMAT_ETC2_RGBA: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTextureETC.COMPRESSED_RGBA8_ETC2_EAC; break; case PIXELFORMAT_ASTC_4x4: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTextureASTC.COMPRESSED_RGBA_ASTC_4x4_KHR; break; case PIXELFORMAT_ATC_RGB: this._glFormat = gl.RGB; this._glInternalFormat = device.extCompressedTextureATC.COMPRESSED_RGB_ATC_WEBGL; break; case PIXELFORMAT_ATC_RGBA: this._glFormat = gl.RGBA; this._glInternalFormat = device.extCompressedTextureATC.COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL; break; case PIXELFORMAT_BC6F: this._glFormat = gl.RGB; this._glInternalFormat = device.extTextureCompressionBPTC.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT; break; case PIXELFORMAT_BC6UF: this._glFormat = gl.RGB; this._glInternalFormat = device.extTextureCompressionBPTC.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT; break; case PIXELFORMAT_BC7: this._glFormat = gl.RGBA; this._glInternalFormat = device.extTextureCompressionBPTC.COMPRESSED_RGBA_BPTC_UNORM_EXT; break; case PIXELFORMAT_DXT1_SRGB: this._glFormat = gl.SRGB; this._glInternalFormat = device.extCompressedTextureS3TC_SRGB.COMPRESSED_SRGB_S3TC_DXT1_EXT; break; case PIXELFORMAT_DXT3_SRGBA: this._glFormat = gl.SRGB_ALPHA; this._glInternalFormat = device.extCompressedTextureS3TC_SRGB.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT; break; case PIXELFORMAT_DXT5_SRGBA: this._glFormat = gl.SRGB_ALPHA; this._glInternalFormat = device.extCompressedTextureS3TC_SRGB.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; break; case PIXELFORMAT_ETC2_SRGB: this._glFormat = gl.SRGB; this._glInternalFormat = device.extCompressedTextureETC.COMPRESSED_SRGB8_ETC2; break; case PIXELFORMAT_ETC2_SRGBA: this._glFormat = gl.SRGB_ALPHA; this._glInternalFormat = device.extCompressedTextureETC.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC; break; case PIXELFORMAT_ASTC_4x4_SRGB: this._glFormat = gl.SRGB_ALPHA; this._glInternalFormat = device.extCompressedTextureASTC.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR; break; case PIXELFORMAT_BC7_SRGBA: this._glFormat = gl.RGBA; this._glInternalFormat = device.extTextureCompressionBPTC.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT; break; case PIXELFORMAT_R16F: this._glFormat = gl.RED; this._glInternalFormat = gl.R16F; this._glPixelType = gl.HALF_FLOAT; break; case PIXELFORMAT_RG16F: this._glFormat = gl.RG; this._glInternalFormat = gl.RG16F; this._glPixelType = gl.HALF_FLOAT; break; case PIXELFORMAT_RGB16F: this._glFormat = gl.RGB; this._glInternalFormat = gl.RGB16F; this._glPixelType = gl.HALF_FLOAT; break; case PIXELFORMAT_RGBA16F: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGBA16F; this._glPixelType = gl.HALF_FLOAT; break; case PIXELFORMAT_RGB32F: this._glFormat = gl.RGB; this._glInternalFormat = gl.RGB32F; this._glPixelType = gl.FLOAT; break; case PIXELFORMAT_RGBA32F: this._glFormat = gl.RGBA; this._glInternalFormat = gl.RGBA32F; this._glPixelType = gl.FLOAT; break; case PIXELFORMAT_R32F: this._glFormat = gl.RED; this._glInternalFormat = gl.R32F; this._glPixelType = gl.FLOAT; break; case PIXELFORMAT_DEPTH: this._glFormat = gl.DEPTH_COMPONENT; this._glInternalFormat = gl.DEPTH_COMPONENT32F; this._glPixelType = gl.FLOAT; break; case PIXELFORMAT_DEPTH16: this._glFormat = gl.DEPTH_COMPONENT; this._glInternalFormat = gl.DEPTH_COMPONENT16; this._glPixelType = gl.UNSIGNED_SHORT; break; case PIXELFORMAT_DEPTHSTENCIL: this._glFormat = gl.DEPTH_STENCIL; this._glInternalFormat = gl.DEPTH24_STENCIL8; this._glPixelType = gl.UNSIGNED_INT_24_8; break; case PIXELFORMAT_111110F: this._glFormat = gl.RGB; this._glInternalFormat = gl.R11F_G11F_B10F; this._glPixelType = gl.UNSIGNED_INT_10F_11F_11F_REV; break; case PIXELFORMAT_SRGB8: this._glFormat = gl.RGB; this._glInternalFormat = gl.SRGB8; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_SRGBA8: this._glFormat = gl.RGBA; this._glInternalFormat = gl.SRGB8_ALPHA8; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_R8I: this._glFormat = gl.RED_INTEGER; this._glInternalFormat = gl.R8I; this._glPixelType = gl.BYTE; break; case PIXELFORMAT_R8U: this._glFormat = gl.RED_INTEGER; this._glInternalFormat = gl.R8UI; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_R16I: this._glFormat = gl.RED_INTEGER; this._glInternalFormat = gl.R16I; this._glPixelType = gl.SHORT; break; case PIXELFORMAT_R16U: this._glFormat = gl.RED_INTEGER; this._glInternalFormat = gl.R16UI; this._glPixelType = gl.UNSIGNED_SHORT; break; case PIXELFORMAT_R32I: this._glFormat = gl.RED_INTEGER; this._glInternalFormat = gl.R32I; this._glPixelType = gl.INT; break; case PIXELFORMAT_R32U: this._glFormat = gl.RED_INTEGER; this._glInternalFormat = gl.R32UI; this._glPixelType = gl.UNSIGNED_INT; break; case PIXELFORMAT_RG8I: this._glFormat = gl.RG_INTEGER; this._glInternalFormat = gl.RG8I; this._glPixelType = gl.BYTE; break; case PIXELFORMAT_RG8U: this._glFormat = gl.RG_INTEGER; this._glInternalFormat = gl.RG8UI; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_RG16I: this._glFormat = gl.RG_INTEGER; this._glInternalFormat = gl.RG16I; this._glPixelType = gl.SHORT; break; case PIXELFORMAT_RG16U: this._glFormat = gl.RG_INTEGER; this._glInternalFormat = gl.RG16UI; this._glPixelType = gl.UNSIGNED_SHORT; break; case PIXELFORMAT_RG32I: this._glFormat = gl.RG_INTEGER; this._glInternalFormat = gl.RG32I; this._glPixelType = gl.INT; break; case PIXELFORMAT_RG32U: this._glFormat = gl.RG_INTEGER; this._glInternalFormat = gl.RG32UI; this._glPixelType = gl.UNSIGNED_INT; break; case PIXELFORMAT_RGBA8I: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGBA8I; this._glPixelType = gl.BYTE; break; case PIXELFORMAT_RGBA8U: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGBA8UI; this._glPixelType = gl.UNSIGNED_BYTE; break; case PIXELFORMAT_RGBA16I: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGBA16I; this._glPixelType = gl.SHORT; break; case PIXELFORMAT_RGBA16U: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGBA16UI; this._glPixelType = gl.UNSIGNED_SHORT; break; case PIXELFORMAT_RGBA32I: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGBA32I; this._glPixelType = gl.INT; break; case PIXELFORMAT_RGBA32U: this._glFormat = gl.RGBA_INTEGER; this._glInternalFormat = gl.RGBA32UI; this._glPixelType = gl.UNSIGNED_INT; break; } this._glCreated = false; } upload(device, texture) { const gl = device.gl; if (!texture._needsUpload && (texture._needsMipmapsUpload && texture._mipmapsUploaded || !texture.pot)) { return; } let mipLevel = 0; let mipObject; let resMult; const requiredMipLevels = texture.numLevels; if (texture.array && !this._glCreated) { gl.texStorage3D(gl.TEXTURE_2D_ARRAY, requiredMipLevels, this._glInternalFormat, texture._width, texture._height, texture._arrayLength); } while(texture._levels[mipLevel] || mipLevel === 0){ if (!texture._needsUpload && mipLevel === 0) { mipLevel++; continue; } else if (mipLevel && (!texture._needsMipmapsUpload || !texture._mipmaps)) { break; } mipObject = texture._levels[mipLevel]; resMult = 1 / Math.pow(2, mipLevel); if (mipLevel === 1 && !texture._compressed && !texture._integerFormat && texture._levels.length < requiredMipLevels) { gl.generateMipmap(this._glTarget); texture._mipmapsUploaded = true; } if (texture._cubemap) { let face; if (device._isBrowserInterface(mipObject[0])) { for(face = 0; face < 6; face++){ if (!texture._levelsUpdated[0][face]) { continue; } let src = mipObject[face]; if (device._isImageBrowserInterface(src)) { if (src.width > device.maxCubeMapSize || src.height > device.maxCubeMapSize) { src = downsampleImage(src, device.maxCubeMapSize); if (mipLevel === 0) { texture._width = src.width; texture._height = src.height; } } } device.setUnpackFlipY(false); device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha); if (this._glCreated) { gl.texSubImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, mipLevel, 0, 0, this._glFormat, this._glPixelType, src); } else { gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, mipLevel, this._glInternalFormat, this._glFormat, this._glPixelType, src); } } } else { resMult = 1 / Math.pow(2, mipLevel); for(face = 0; face < 6; face++){ if (!texture._levelsUpdated[0][face]) { continue; } const texData = mipObject[face]; if (texture._compressed) { if (this._glCreated && texData) { gl.compressedTexSubImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, mipLevel, 0, 0, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), this._glInternalFormat, texData); } else { gl.compressedTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, mipLevel, this._glInternalFormat, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), 0, texData); } } else { device.setUnpackFlipY(false); device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha); if (this._glCreated && texData) { gl.texSubImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, mipLevel, 0, 0, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), this._glFormat, this._glPixelType, texData); } else { gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + face, mipLevel, this._glInternalFormat, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), 0, this._glFormat, this._glPixelType, texData); } } } } } else if (texture._volume) { if (texture._compressed) { gl.compressedTexImage3D(gl.TEXTURE_3D, mipLevel, this._glInternalFormat, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), Math.max(texture._depth * resMult, 1), 0, mipObject); } else { device.setUnpackFlipY(false); device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha); gl.texImage3D(gl.TEXTURE_3D, mipLevel, this._glInternalFormat, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), Math.max(texture._depth * resMult, 1), 0, this._glFormat, this._glPixelType, mipObject); } } else if (texture.array) { if (Array.isArray(mipObject) && texture._arrayLength === mipObject.length) { if (texture._compressed) { for(let index = 0; index < texture._arrayLength; index++){ gl.compressedTexSubImage3D(gl.TEXTURE_2D_ARRAY, mipLevel, 0, 0, index, Math.max(Math.floor(texture._width * resMult), 1), Math.max(Math.floor(texture._height * resMult), 1), 1, this._glInternalFormat, mipObject[index]); } } else { for(let index = 0; index < texture._arrayLength; index++){ gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, mipLevel, 0, 0, index, Math.max(Math.floor(texture._width * resMult), 1), Math.max(Math.floor(texture._height * resMult), 1), 1, this._glFormat, this._glPixelType, mipObject[index]); } } } } else { if (device._isBrowserInterface(mipObject)) { if (device._isImageBrowserInterface(mipObject)) { if (mipObject.width > device.maxTextureSize || mipObject.height > device.maxTextureSize) { mipObject = downsampleImage(mipObject, device.maxTextureSize); if (mipLevel === 0) { texture._width = mipObject.width; texture._height = mipObject.height; } } } const w = mipObject.width || mipObject.videoWidth; const h = mipObject.height || mipObject.videoHeight; device.setUnpackFlipY(texture._flipY); device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha); if (this._glCreated && texture._width === w && texture._height === h && !device._isImageVideoInterface(mipObject)) { gl.texSubImage2D(gl.TEXTURE_2D, mipLevel, 0, 0, this._glFormat, this._glPixelType, mipObject); } else { gl.texImage2D(gl.TEXTURE_2D, mipLevel, this._glInternalFormat, this._glFormat, this._glPixelType, mipObject); if (mipLevel === 0) { texture._width = w; texture._height = h; } } } else { resMult = 1 / Math.pow(2, mipLevel); if (texture._compressed) { if (this._glCreated && mipObject) { gl.compressedTexSubImage2D(gl.TEXTURE_2D, mipLevel, 0, 0, Math.max(Math.floor(texture._width * resMult), 1), Math.max(Math.floor(texture._height * resMult), 1), this._glInternalFormat, mipObject); } else { gl.compressedTexImage2D(gl.TEXTURE_2D, mipLevel, this._glInternalFormat, Math.max(Math.floor(texture._width * resMult), 1), Math.max(Math.floor(texture._height * resMult), 1), 0, mipObject); } } else { device.setUnpackFlipY(false); device.setUnpackPremultiplyAlpha(texture._premultiplyAlpha); if (this._glCreated && mipObject) { gl.texSubImage2D(gl.TEXTURE_2D, mipLevel, 0, 0, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), this._glFormat, this._glPixelType, mipObject); } else { gl.texImage2D(gl.TEXTURE_2D, mipLevel, this._glInternalFormat, Math.max(texture._width * resMult, 1), Math.max(texture._height * resMult, 1), 0, this._glFormat, this._glPixelType, mipObject); } } } if (mipLevel === 0) { texture._mipmapsUploaded = false; } else { texture._mipmapsUploaded = true; } } mipLevel++; } if (texture._needsUpload) { if (texture._cubemap) { for(let i = 0; i < 6; i++){ texture._levelsUpdated[0][i] = false; } } else { texture._levelsUpdated[0] = false; } } if (!texture._compressed && !texture._integerFormat && texture._mipmaps && texture._needsMipmapsUpload && texture._levels.length === 1) { gl.generateMipmap(this._glTarget); texture._mipmapsUploaded = true; } if (texture._gpuSize) { texture.adjustVramSizeTracking(device._vram, -texture._gpuSize); } texture._gpuSize = texture.gpuSize; texture.adjustVramSizeTracking(device._vram, texture._gpuSize); this._glCreated = true; } uploadImmediate(device, texture) { if (texture._needsUpload || texture._needsMipmapsUpload) { device.setTexture(texture, 0); texture._needsUpload = false; texture._needsMipmapsUpload = false; } } read(x, y, width, height, options) { const texture = this.texture; const device = texture.device; return device.readTextureAsync(texture, x, y, width, height, options); } write(x, y, width, height, data) { const { texture } = this; const { device } = texture; device.setTexture(texture, 0); return device.writeTextureAsync(texture, x, y, width, height, data); } constructor(texture){ this._glTexture = null; this.dirtyParameterFlags = 0; this.texture = texture; } } class FramebufferPair { destroy(gl) { if (this.msaaFB) { gl.deleteRenderbuffer(this.msaaFB); this.msaaFB = null; } if (this.resolveFB) { gl.deleteRenderbuffer(this.resolveFB); this.resolveFB = null; } } constructor(msaaFB, resolveFB){ this.msaaFB = msaaFB; this.resolveFB = resolveFB; } } class WebglRenderTarget { destroy(device) { const gl = device.gl; this._isInitialized = false; if (this._glFrameBuffer) { if (this._glFrameBuffer !== this.suppliedColorFramebuffer) { gl.deleteFramebuffer(this._glFrameBuffer); } this._glFrameBuffer = null; } if (this._glDepthBuffer) { gl.deleteRenderbuffer(this._glDepthBuffer); this._glDepthBuffer = null; } if (this._glResolveFrameBuffer) { if (this._glResolveFrameBuffer !== this.suppliedColorFramebuffer) { gl.deleteFramebuffer(this._glResolveFrameBuffer); } this._glResolveFrameBuffer = null; } this._glMsaaColorBuffers.forEach((buffer)=>{ gl.deleteRenderbuffer(buffer); }); this._glMsaaColorBuffers.length = 0; this.colorMrtFramebuffers?.forEach((framebuffer)=>{ framebuffer.destroy(gl); }); this.colorMrtFramebuffers = null; if (this._glMsaaDepthBuffer) { this._glMsaaDepthBuffer = null; if (this.msaaDepthBufferKey) { getMultisampledTextureCache(device).release(this.msaaDepthBufferKey); } } this.suppliedColorFramebuffer = undefined; } get initialized() { return this._isInitialized; } init(device, target) { const gl = device.gl; this._isInitialized = true; const buffers = []; if (this.suppliedColorFramebuffer !== undefined) { this._glFrameBuffer = this.suppliedColorFramebuffer; } else { this._glFrameBuffer = gl.createFramebuffer(); device.setFramebuffer(this._glFrameBuffer); const colorBufferCount = target._colorBuffers?.length ?? 0; const attachmentBaseConstant = gl.COLOR_ATTACHMENT0; for(let i = 0; i < colorBufferCount; ++i){ const colorBuffer = target.getColorBuffer(i); if (colorBuffer) { if (!colorBuffer.impl._glTexture) { colorBuffer._width = Math.min(colorBuffer.width, device.maxRenderBufferSize); colorBuffer._height = Math.min(colorBuffer.height, device.maxRenderBufferSize); device.setTexture(colorBuffer, 0); } gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentBaseConstant + i, colorBuffer._cubemap ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + target._face : gl.TEXTURE_2D, colorBuffer.impl._glTexture, target.mipLevel); buffers.push(attachmentBaseConstant + i); } } gl.drawBuffers(buffers); const depthBuffer = target._depthBuffer; if (depthBuffer || target._depth) { const attachmentPoint = target._stencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT; if (depthBuffer) { if (!depthBuffer.impl._glTexture) { depthBuffer._width = Math.min(depthBuffer.width, device.maxRenderBufferSize); depthBuffer._height = Math.min(depthBuffer.height, device.maxRenderBufferSize); device.setTexture(depthBuffer, 0); } gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, depthBuffer._cubemap ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + target._face : gl.TEXTURE_2D, target._depthBuffer.impl._glTexture, target.mipLevel); } else { const willRenderMsaa = target._samples > 1; if (!willRenderMsaa) { if (!this._glDepthBuffer) { this._glDepthBuffer = gl.createRenderbuffer(); } const internalFormat = target._stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT32F; gl.bindRenderbuffer(gl.RENDERBUFFER, this._glDepthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, target.width, target.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachmentPoint, gl.RENDERBUFFER, this._glDepthBuffer); gl.bindRenderbuffer(gl.RENDERBUFFER, null); } } } } if (target._samples > 1) { this._glResolveFrameBuffer = this._glFrameBuffer; this._glFrameBuffer = gl.createFramebuffer(); device.setFramebuffer(this._glFrameBuffer); const colorBufferCount = target._colorBuffers?.length ?? 0; if (this.suppliedColorFramebuffer !== undefined) { const buffer = gl.createRenderbuffer(); this._glMsaaColorBuffers.push(buffer); const internalFormat = device.backBufferFormat === PIXELFORMAT_RGBA8 ? gl.RGBA8 : gl.RGB8; gl.bindRenderbuffer(gl.RENDERBUFFER, buffer); gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, internalFormat, target.width, target.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, buffer); } else { for(let i = 0; i < colorBufferCount; ++i){ const colorBuffer = target.getColorBuffer(i); if (colorBuffer) { const buffer = gl.createRenderbuffer(); this._glMsaaColorBuffers.push(buffer); gl.bindRenderbuffer(gl.RENDERBUFFER, buffer); gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, colorBuffer.impl._glInternalFormat, target.width, target.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, buffer); } } } if (target._depth) { const internalFormat = target._stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT32F; const attachmentPoint = target._stencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT; let key; const depthBuffer = target._depthBuffer; if (depthBuffer) { key = `${depthBuffer.id}:${target.width}:${target.height}:${target._samples}:${internalFormat}:${attachmentPoint}`; this._glMsaaDepthBuffer = getMultisampledTextureCache(device).get(key); } if (!this._glMsaaDepthBuffer) { this._glMsaaDepthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, this._glMsaaDepthBuffer); gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, internalFormat, target.width, target.height); this._glMsaaDepthBuffer.destroy = function() { gl.deleteRenderbuffer(this); }; if (depthBuffer) { getMultisampledTextureCache(device).set(key, this._glMsaaDepthBuffer); } } this.msaaDepthBufferKey = key; gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachmentPoint, gl.RENDERBUFFER, this._glMsaaDepthBuffer); } if (colorBufferCount > 1) { this._createMsaaMrtFramebuffers(device, target, colorBufferCount); device.setFramebuffer(this._glFrameBuffer); gl.drawBuffers(buffers); } } } _createMsaaMrtFramebuffers(device, target, colorBufferCount) { const gl = device.gl; this.colorMrtFramebuffers = []; for(let i = 0; i < colorBufferCount; ++i){ const colorBuffer = target.getColorBuffer(i); const srcFramebuffer = gl.createFramebuffer(); device.setFramebuffer(srcFramebuffer); const buffer = this._glMsaaColorBuffers[i]; gl.bindRenderbuffer(gl.RENDERBUFFER, buffer); gl.renderbufferStorageMultisample(gl.RENDERBUFFER, target._samples, colorBuffer.impl._glInternalFormat, target.width, target.height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, buffer); gl.drawBuffers([ gl.COLOR_ATTACHMENT0 ]); const dstFramebuffer = gl.createFramebuffer(); device.setFramebuffer(dstFramebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, colorBuffer._cubemap ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + target._face : gl.TEXTURE_2D, colorBuffer.impl._glTexture, 0); this.colorMrtFramebuffers[i] = new FramebufferPair(srcFramebuffer, dstFramebuffer); } } _checkFbo(device, target, type = '') { const gl = device.gl; const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); switch(status){ case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: break; case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: break; case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: break; case gl.FRAMEBUFFER_UNSUPPORTED: break; } } loseContext() { this._glFrameBuffer = null; this._glDepthBuffer = null; this._glResolveFrameBuffer = null; this._glMsaaColorBuffers.length = 0; this._glMsaaDepthBuffer = null; this.msaaDepthBufferKey = undefined; this.colorMrtFramebuffers = null; this.suppliedColorFramebuffer = undefined; this._isInitialized = false; } internalResolve(device, src, dst, target, mask) { device.setScissor(0, 0, target.width, target.height); const gl = device.gl; gl.bindFramebuffer(gl.READ_FRAMEBUFFER, src); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst); gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, target.width, target.height, mask, gl.NEAREST); } resolve(device, target, color, depth) { const gl = device.gl; if (this.colorMrtFramebuffers) { if (color) { for(let i = 0; i < this.colorMrtFramebuffers.length; i++){ const fbPair = this.colorMrtFramebuffers[i]; this.internalResolve(device, fbPair.msaaFB, fbPair.resolveFB, target, gl.COLOR_BUFFER_BIT); } } if (depth) { this.internalResolve(device, this._glFrameBuffer, this._glResolveFrameBuffer, target, gl.DEPTH_BUFFER_BIT); } } else { this.internalResolve(device, this._glFrameBuffer, this._glResolveFrameBuffer, target, (color ? gl.COLOR_BUFFER_BIT : 0) | (depth ? gl.DEPTH_BUFFER_BIT : 0)); } gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); } constructor(){ this._glFrameBuffer = null; this._glDepthBuffer = null; this._glResolveFrameBuffer = null; this.colorMrtFramebuffers = null; this._glMsaaColorBuffers = []; this._glMsaaDepthBuffer = null; this._isInitialized = false; } } class WebglUploadStream { destroy() { const gl = this.uploadStream.device.gl; this.availablePBOs.forEach((info)=>gl.deleteBuffer(info.pbo)); this.pendingPBOs.forEach((item)=>{ if (item.sync) gl.deleteSync(item.sync); gl.deleteBuffer(item.pbo); }); } _onDeviceLost() { this.availablePBOs.length = 0; this.pendingPBOs.length = 0; } update(minByteSize) { const gl = this.uploadStream.device.gl; const pending = this.pendingPBOs; for(let i = pending.length - 1; i >= 0; i--){ const item = pending[i]; const result = gl.clientWaitSync(item.sync, 0, 0); if (result === gl.CONDITION_SATISFIED || result === gl.ALREADY_SIGNALED) { gl.deleteSync(item.sync); this.availablePBOs.push({ pbo: item.pbo, size: item.size }); pending.splice(i, 1); } } const available = this.availablePBOs; for(let i = available.length - 1; i >= 0; i--){ if (available[i].size < minByteSize) { gl.deleteBuffer(available[i].pbo); available.splice(i, 1); } } } upload(data, target, offset, size) { if (this.useSingleBuffer) { this.uploadDirect(data, target, offset, size); } else { this.uploadPBO(data, target, offset, size); } } uploadDirect(data, target, offset, size) { target._levels[0] = data; target.upload(); } uploadPBO(data, target, offset, size) { const device = this.uploadStream.device; const gl = device.gl; const width = target.width; const byteSize = size * data.BYTES_PER_ELEMENT; this.update(byteSize); const startY = offset / width; const height = size / width; const pboInfo = this.availablePBOs.pop() ?? (()=>{ const pbo = gl.createBuffer(); return { pbo, size: byteSize }; })(); gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pboInfo.pbo); gl.bufferData(gl.PIXEL_UNPACK_BUFFER, byteSize, gl.STREAM_DRAW); gl.bufferSubData(gl.PIXEL_UNPACK_BUFFER, 0, new Uint8Array(data.buffer, data.byteOffset, byteSize)); gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null); device.setTexture(target, 0); device.activeTexture(0); device.bindTexture(target); gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pboInfo.pbo); device.setUnpackFlipY(false); device.setUnpackPremultiplyAlpha(false); gl.pixelStorei(gl.UNPACK_ALIGNMENT, data.BYTES_PER_ELEMENT); gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0); gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0); gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0); const impl = target.impl; gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, startY, width, height, impl._glFormat, impl._glPixelType, 0); gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null); const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); this.pendingPBOs.push({ pbo: pboInfo.pbo, size: byteSize, sync }); gl.flush(); } constructor(uploadStream){ this.availablePBOs = []; this.pendingPBOs = []; this.uploadStream = uploadStream; this.useSingleBuffer = uploadStream.useSingleBuffer; } } class FrameQueriesInfo { destroy(gl) { this.queries.forEach((query)=>gl.deleteQuery(query)); this.queries = null; } constructor(){ this.queries = []; } } class WebglGpuProfiler extends GpuProfiler { destroy() { this.freeQueries.forEach((query)=>this.device.gl.deleteQuery(query)); this.frameQueries.forEach((query)=>this.device.gl.deleteQuery(query)); this.previousFrameQueries.forEach((frameQueriesInfo)=>frameQueriesInfo.destroy(this.device.gl)); this.freeQueries = null; this.frameQueries = null; this.previousFrameQueries = null; } loseContext() { super.loseContext(); this.freeQueries = []; this.frameQueries = []; this.previousFrameQueries = []; } restoreContext() { this.ext = this.device.extDisjointTimerQuery; } getQuery() { return this.freeQueries.pop() ?? this.device.gl.createQuery(); } start(name) { if (this.ext) { const slot = this.getSlot(name); const query = this.getQuery(); this.frameQueries[slot] = query; this.device.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query); return slot; } return undefined; } end(slot) { if (slot !== undefined) { this.device.gl.endQuery(this.ext.TIME_ELAPSED_EXT); } } frameStart() { this.processEnableRequest(); if (this._enabled) { this.frameGPUMarkerSlot = this.start('GpuFrame'); } } frameEnd() { if (this._enabled) { this.end(this.frameGPUMarkerSlot); } } request() { if (this._enabled) { const ext = this.ext; const gl = this.device.gl; const renderVersion = this.device.renderVersion; const frameQueries = this.frameQueries; if (frameQueries.length > 0) { this.frameQueries = []; const frameQueriesInfo = new FrameQueriesInfo(); frameQueriesInfo.queries = frameQueries; frameQueriesInfo.renderVersion = renderVersion; this.previousFrameQueries.push(frameQueriesInfo); } if (this.previousFrameQueries.length > 0) { const previousQueriesInfo = this.previousFrameQueries[0]; const previousQueries = previousQueriesInfo.queries; const lastQuery = previousQueries[previousQueries.length - 1]; const available = gl.getQueryParameter(lastQuery, gl.QUERY_RESULT_AVAILABLE); const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT); if (available && !disjoint) { this.previousFrameQueries.shift(); const timings = this.timings; timings.length = 0; for(let i = 0; i < previousQueries.length; i++){ const query = previousQueries[i]; const duration = gl.getQueryParameter(query, gl.QUERY_RESULT); timings[i] = duration * 0.000001; this.freeQueries.push(query); } this.report(previousQueriesInfo.renderVersion, timings); } if (disjoint) { this.previousFrameQueries.forEach((frameQueriesInfo)=>{ this.report(frameQueriesInfo.renderVersion, null); frameQueriesInfo.destroy(gl); }); this.previousFrameQueries.length = 0; } } super.request(renderVersion); } } constructor(device){ super(), this.freeQueries = [], this.frameQueries = [], this.previousFrameQueries = [], this.timings = []; this.device = device; this.ext = device.extDisjointTimerQuery; } } const invalidateAttachments = []; class WebglGraphicsDevice extends GraphicsDevice { postInit() { super.postInit(); this.gpuProfiler = new WebglGpuProfiler(this); } destroy() { super.destroy(); const gl = this.gl; if (this.feedback) { gl.deleteTransformFeedback(this.feedback); } this.clearVertexArrayObjectCache(); this.canvas.removeEventListener('webglcontextlost', this._contextLostHandler, false); this.canvas.removeEventListener('webglcontextrestored', this._contextRestoredHandler, false); this._contextLostHandler = null; this._contextRestoredHandler = null; this.gl = null; super.postDestroy(); } createBackbuffer(frameBuffer) { this.supportsStencil = this.initOptions.stencil; this.backBuffer = new RenderTarget({ name: 'WebglFramebuffer', graphicsDevice: this, depth: this.initOptions.depth, stencil: this.supportsStencil, samples: this.samples }); this.backBuffer.impl.suppliedColorFramebuffer = frameBuffer; } updateBackbufferFormat(framebuffer) { const gl = this.gl; gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); const alphaBits = this.gl.getParameter(this.gl.ALPHA_BITS); this.backBufferFormat = alphaBits ? PIXELFORMAT_RGBA8 : PIXELFORMAT_RGB8; } updateBackbuffer() { const resolutionChanged = this.canvas.width !== this.backBufferSize.x || this.canvas.height !== this.backBufferSize.y; if (this._defaultFramebufferChanged || resolutionChanged) { if (this._defaultFramebufferChanged) { this.updateBackbufferFormat(this._defaultFramebuffer); } this._defaultFramebufferChanged = false; this.backBufferSize.set(this.canvas.width, this.canvas.height); this.backBuffer.destroy(); this.createBackbuffer(this._defaultFramebuffer); } } createVertexBufferImpl(vertexBuffer, format) { return new WebglVertexBuffer(); } createIndexBufferImpl(indexBuffer) { return new WebglIndexBuffer(indexBuffer); } createShaderImpl(shader) { return new WebglShader(shader); } createDrawCommandImpl(drawCommands) { return new WebglDrawCommands(drawCommands.indexSizeBytes); } createTextureImpl(texture) { this.textures.add(texture); return new WebglTexture(texture); } createRenderTargetImpl(renderTarget) { return new WebglRenderTarget(); } createUploadStreamImpl(uploadStream) { return new WebglUploadStream(uploadStream); } getPrecision() { const gl = this.gl; let precision = 'highp'; if (gl.getShaderPrecisionFormat) { const vertexShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT); const vertexShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT); const fragmentShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); const fragmentShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT); if (vertexShaderPrecisionHighpFloat && vertexShaderPrecisionMediumpFloat && fragmentShaderPrecisionHighpFloat && fragmentShaderPrecisionMediumpFloat) { const highpAvailable = vertexShaderPrecisionHighpFloat.precision > 0 && fragmentShaderPrecisionHighpFloat.precision > 0; const mediumpAvailable = vertexShaderPrecisionMediumpFloat.precision > 0 && fragmentShaderPrecisionMediumpFloat.precision > 0; if (!highpAvailable) { if (mediumpAvailable) { precision = 'mediump'; } else { precision = 'lowp'; } } } } return precision; } getExtension() { for(let i = 0; i < arguments.length; i++){ if (this.supportedExtensions.indexOf(arguments[i]) !== -1) { return this.gl.getExtension(arguments[i]); } } return null; } get extDisjointTimerQuery() { if (!this._extDisjointTimerQuery) { this._extDisjointTimerQuery = this.getExtension('EXT_disjoint_timer_query_webgl2', 'EXT_disjoint_timer_query'); } return this._extDisjointTimerQuery; } initializeExtensions() { const gl = this.gl; this.supportedExtensions = gl.getSupportedExtensions() ?? []; this._extDisjointTimerQuery = null; this.textureRG11B10Renderable = true; this.extColorBufferFloat = this.getExtension('EXT_color_buffer_float'); this.textureFloatRenderable = !!this.extColorBufferFloat; this.extColorBufferHalfFloat = this.getExtension('EXT_color_buffer_half_float'); this.textureHalfFloatRenderable = !!this.extColorBufferHalfFloat || !!this.extColorBufferFloat; this.extDebugRendererInfo = this.getExtension('WEBGL_debug_renderer_info'); this.extTextureFloatLinear = this.getExtension('OES_texture_float_linear'); this.textureFloatFilterable = !!this.extTextureFloatLinear; this.extFloatBlend = this.getExtension('EXT_float_blend'); this.extTextureFilterAnisotropic = this.getExtension('EXT_texture_filter_anisotropic', 'WEBKIT_EXT_texture_filter_anisotropic'); this.extParallelShaderCompile = this.getExtension('KHR_parallel_shader_compile'); this.extMultiDraw = this.getExtension('WEBGL_multi_draw'); this.supportsMultiDraw = !!this.extMultiDraw; this.extCompressedTextureETC1 = this.getExtension('WEBGL_compressed_texture_etc1'); this.extCompressedTextureETC = this.getExtension('WEBGL_compressed_texture_etc'); this.extCompressedTexturePVRTC = this.getExtension('WEBGL_compressed_texture_pvrtc', 'WEBKIT_WEBGL_compressed_texture_pvrtc'); this.extCompressedTextureS3TC = this.getExtension('WEBGL_compressed_texture_s3tc', 'WEBKIT_WEBGL_compressed_texture_s3tc'); this.extCompressedTextureS3TC_SRGB = this.getExtension('WEBGL_compressed_texture_s3tc_srgb'); this.extCompressedTextureATC = this.getExtension('WEBGL_compressed_texture_atc'); this.extCompressedTextureASTC = this.getExtension('WEBGL_compressed_texture_astc'); this.extTextureCompressionBPTC = this.getExtension('EXT_texture_compression_bptc'); } initializeCapabilities() { const gl = this.gl; let ext; const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : ''; this.maxPrecision = this.precision = this.getPrecision(); const contextAttribs = gl.getContextAttributes(); this.supportsMsaa = contextAttribs?.antialias ?? false; this.supportsStencil = contextAttribs?.stencil ?? false; this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); this.maxCubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE); this.maxRenderBufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); this.maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); this.maxCombinedTextures = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); this.maxVertexTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); this.vertexUniformsCount = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS); this.fragmentUniformsCount = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS); this.maxColorAttachments = gl.getParameter(gl.MAX_COLOR_ATTACHMENTS); this.maxVolumeSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE); ext = this.extDebugRendererInfo; this.unmaskedRenderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : ''; this.unmaskedVendor = ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : ''; const maliRendererRegex = /\bMali-G52+/; const samsungModelRegex = /SM-[a-zA-Z0-9]+/; this.supportsGpuParticles = !(this.unmaskedVendor === 'ARM' && userAgent.match(samsungModelRegex)) && !this.unmaskedRenderer.match(maliRendererRegex); ext = this.extTextureFilterAnisotropic; this.maxAnisotropy = ext ? gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) : 1; const antialiasSupported = !this.forceDisableMultisampling; this.maxSamples = antialiasSupported ? gl.getParameter(gl.MAX_SAMPLES) : 1; this.maxSamples = Math.min(this.maxSamples, 4); this.samples = antialiasSupported && this.backBufferAntialias ? this.maxSamples : 1; this.supportsAreaLights = !platform.android; if (this.maxTextures <= 8) { this.supportsAreaLights = false; } this.initCapsDefines(); } initializeRenderState() { super.initializeRenderState(); const gl = this.gl; gl.disable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ZERO); gl.blendEquation(gl.FUNC_ADD); gl.colorMask(true, true, true, true); gl.blendColor(0, 0, 0, 0); gl.enable(gl.CULL_FACE); this.cullFace = gl.BACK; gl.cullFace(gl.BACK); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.depthMask(true); this.stencil = false; gl.disable(gl.STENCIL_TEST); this.stencilFuncFront = this.stencilFuncBack = FUNC_ALWAYS; this.stencilRefFront = this.stencilRefBack = 0; this.stencilMaskFront = this.stencilMaskBack = 0xFF; gl.stencilFunc(gl.ALWAYS, 0, 0xFF); this.stencilFailFront = this.stencilFailBack = STENCILOP_KEEP; this.stencilZfailFront = this.stencilZfailBack = STENCILOP_KEEP; this.stencilZpassFront = this.stencilZpassBack = STENCILOP_KEEP; this.stencilWriteMaskFront = 0xFF; this.stencilWriteMaskBack = 0xFF; gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); gl.stencilMask(0xFF); this.alphaToCoverage = false; this.raster = true; gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE); gl.disable(gl.RASTERIZER_DISCARD); this.depthBiasEnabled = false; gl.disable(gl.POLYGON_OFFSET_FILL); this.clearDepth = 1; gl.clearDepth(1); this.clearColor = new Color(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0); this.clearStencil = 0; gl.clearStencil(0); gl.hint(gl.FRAGMENT_SHADER_DERIVATIVE_HINT, gl.NICEST); gl.enable(gl.SCISSOR_TEST); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); this.unpackFlipY = false; gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); this.unpackPremultiplyAlpha = false; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); } initTextureUnits(count = 16) { this.textureUnits = []; for(let i = 0; i < count; i++){ this.textureUnits.push([ null, null, null ]); } } initializeContextCaches() { super.initializeContextCaches(); this._vaoMap = new Map(); this.boundVao = null; this.activeFramebuffer = null; this.feedback = null; this.transformFeedbackBuffer = null; this.textureUnit = 0; this.initTextureUnits(this.maxCombinedTextures); } loseContext() { super.loseContext(); for (const shader of this.shaders){ shader.loseContext(); } } restoreContext() { this.initializeExtensions(); this.initializeCapabilities(); super.restoreContext(); for (const shader of this.shaders){ shader.restoreContext(); } } setViewport(x, y, w, h) { if (this.vx !== x || this.vy !== y || this.vw !== w || this.vh !== h) { this.gl.viewport(x, y, w, h); this.vx = x; this.vy = y; this.vw = w; this.vh = h; } } setScissor(x, y, w, h) { if (this.sx !== x || this.sy !== y || this.sw !== w || this.sh !== h) { this.gl.scissor(x, y, w, h); this.sx = x; this.sy = y; this.sw = w; this.sh = h; } } setFramebuffer(fb) { if (this.activeFramebuffer !== fb) { const gl = this.gl; gl.bindFramebuffer(gl.FRAMEBUFFER, fb); this.activeFramebuffer = fb; } } copyRenderTarget(source, dest, color, depth) { const gl = this.gl; if (source === this.backBuffer) { source = null; } if (color) { if (!dest) { if (!source._colorBuffer) { return false; } } else if (source) { if (!source._colorBuffer || !dest._colorBuffer) { return false; } if (source._colorBuffer._format !== dest._colorBuffer._format) { return false; } } } if (depth && source) { if (!source._depth) { if (!source._depthBuffer || !dest._depthBuffer) { return false; } if (source._depthBuffer._format !== dest._depthBuffer._format) { return false; } } } const prevRt = this.renderTarget; this.renderTarget = dest; this.updateBegin(); const src = source ? source.impl._glFrameBuffer : this.backBuffer?.impl._glFrameBuffer; const dst = dest ? dest.impl._glFrameBuffer : this.backBuffer?.impl._glFrameBuffer; gl.bindFramebuffer(gl.READ_FRAMEBUFFER, src); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst); const w = source ? source.width : dest ? dest.width : this.width; const h = source ? source.height : dest ? dest.height : this.height; gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, (color ? gl.COLOR_BUFFER_BIT : 0) | (depth ? gl.DEPTH_BUFFER_BIT : 0), gl.NEAREST); this.renderTarget = prevRt; gl.bindFramebuffer(gl.FRAMEBUFFER, prevRt ? prevRt.impl._glFrameBuffer : null); return true; } frameStart() { super.frameStart(); this.updateBackbuffer(); this.gpuProfiler.frameStart(); } frameEnd() { super.frameEnd(); this.gpuProfiler.frameEnd(); this.gpuProfiler.request(); } startRenderPass(renderPass) { const rt = renderPass.renderTarget ?? this.backBuffer; this.renderTarget = rt; this.updateBegin(); const { width, height } = rt; this.setViewport(0, 0, width, height); this.setScissor(0, 0, width, height); const colorOps = renderPass.colorOps; const depthStencilOps = renderPass.depthStencilOps; if (colorOps?.clear || depthStencilOps.clearDepth || depthStencilOps.clearStencil) { let clearFlags = 0; const clearOptions = {}; if (colorOps?.clear) { clearFlags |= CLEARFLAG_COLOR; clearOptions.color = [ colorOps.clearValue.r, colorOps.clearValue.g, colorOps.clearValue.b, colorOps.clearValue.a ]; } if (depthStencilOps.clearDepth) { clearFlags |= CLEARFLAG_DEPTH; clearOptions.depth = depthStencilOps.clearDepthValue; } if (depthStencilOps.clearStencil) { clearFlags |= CLEARFLAG_STENCIL; clearOptions.stencil = depthStencilOps.clearStencilValue; } clearOptions.flags = clearFlags; this.clear(clearOptions); } this.insideRenderPass = true; } endRenderPass(renderPass) { this.unbindVertexArray(); const target = this.renderTarget; const colorBufferCount = renderPass.colorArrayOps.length; if (target) { invalidateAttachments.length = 0; const gl = this.gl; for(let i = 0; i < colorBufferCount; i++){ const colorOps = renderPass.colorArrayOps[i]; if (!(colorOps.store || colorOps.resolve)) { invalidateAttachments.push(gl.COLOR_ATTACHMENT0 + i); } } if (target !== this.backBuffer) { if (!renderPass.depthStencilOps.storeDepth) { invalidateAttachments.push(gl.DEPTH_ATTACHMENT); } if (!renderPass.depthStencilOps.storeStencil) { invalidateAttachments.push(gl.STENCIL_ATTACHMENT); } } if (invalidateAttachments.length > 0) { if (renderPass.fullSizeClearRect) { gl.invalidateFramebuffer(gl.DRAW_FRAMEBUFFER, invalidateAttachments); } } if (colorBufferCount && renderPass.colorOps?.resolve) { if (renderPass.samples > 1 && target.autoResolve) { target.resolve(true, false); } } if (target.depthBuffer && renderPass.depthStencilOps.resolveDepth) { if (renderPass.samples > 1 && target.autoResolve) { target.resolve(false, true); } } for(let i = 0; i < colorBufferCount; i++){ const colorOps = renderPass.colorArrayOps[i]; if (colorOps.genMipmaps) { const colorBuffer = target._colorBuffers[i]; if (colorBuffer && colorBuffer.impl._glTexture && colorBuffer.mipmaps) { this.activeTexture(this.maxCombinedTextures - 1); this.bindTexture(colorBuffer); this.gl.generateMipmap(colorBuffer.impl._glTarget); } } } } this.insideRenderPass = false; } set defaultFramebuffer(value) { if (this._defaultFramebuffer !== value) { this._defaultFramebuffer = value; this._defaultFramebufferChanged = true; } } get defaultFramebuffer() { return this._defaultFramebuffer; } updateBegin() { this.boundVao = null; if (this._tempEnableSafariTextureUnitWorkaround) { for(let unit = 0; unit < this.textureUnits.length; ++unit){ for(let slot = 0; slot < 3; ++slot){ this.textureUnits[unit][slot] = null; } } } const target = this.renderTarget ?? this.backBuffer; const targetImpl = target.impl; if (!targetImpl.initialized) { this.initRenderTarget(target); } this.setFramebuffer(targetImpl._glFrameBuffer); } updateEnd() { this.unbindVertexArray(); const target = this.renderTarget; if (target && target !== this.backBuffer) { if (target._samples > 1 && target.autoResolve) { target.resolve(); } const colorBuffer = target._colorBuffer; if (colorBuffer && colorBuffer.impl._glTexture && colorBuffer.mipmaps) { this.activeTexture(this.maxCombinedTextures - 1); this.bindTexture(colorBuffer); this.gl.generateMipmap(colorBuffer.impl._glTarget); } } } setUnpackFlipY(flipY) { if (this.unpackFlipY !== flipY) { this.unpackFlipY = flipY; const gl = this.gl; gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY); } } setUnpackPremultiplyAlpha(premultiplyAlpha) { if (this.unpackPremultiplyAlpha !== premultiplyAlpha) { this.unpackPremultiplyAlpha = premultiplyAlpha; const gl = this.gl; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiplyAlpha); } } activeTexture(textureUnit) { if (this.textureUnit !== textureUnit) { this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit); this.textureUnit = textureUnit; } } bindTexture(texture) { const impl = texture.impl; const textureTarget = impl._glTarget; const textureObject = impl._glTexture; const textureUnit = this.textureUnit; const slot = this.targetToSlot[textureTarget]; if (this.textureUnits[textureUnit][slot] !== textureObject) { this.gl.bindTexture(textureTarget, textureObject); this.textureUnits[textureUnit][slot] = textureObject; } } bindTextureOnUnit(texture, textureUnit) { const impl = texture.impl; const textureTarget = impl._glTarget; const textureObject = impl._glTexture; const slot = this.targetToSlot[textureTarget]; if (this.textureUnits[textureUnit][slot] !== textureObject) { this.activeTexture(textureUnit); this.gl.bindTexture(textureTarget, textureObject); this.textureUnits[textureUnit][slot] = textureObject; } } setTextureParameters(texture) { const gl = this.gl; const flags = texture.impl.dirtyParameterFlags; const target = texture.impl._glTarget; if (flags & TEXPROPERTY_MIN_FILTER) { let filter = texture._minFilter; if (!texture._mipmaps || texture._compressed && texture._levels.length === 1) { if (filter === FILTER_NEAREST_MIPMAP_NEAREST || filter === FILTER_NEAREST_MIPMAP_LINEAR) { filter = FILTER_NEAREST; } else if (filter === FILTER_LINEAR_MIPMAP_NEAREST || filter === FILTER_LINEAR_MIPMAP_LINEAR) { filter = FILTER_LINEAR; } } gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, this.glFilter[filter]); } if (flags & TEXPROPERTY_MAG_FILTER) { gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, this.glFilter[texture._magFilter]); } if (flags & TEXPROPERTY_ADDRESS_U) { gl.texParameteri(target, gl.TEXTURE_WRAP_S, this.glAddress[texture._addressU]); } if (flags & TEXPROPERTY_ADDRESS_V) { gl.texParameteri(target, gl.TEXTURE_WRAP_T, this.glAddress[texture._addressV]); } if (flags & TEXPROPERTY_ADDRESS_W) { gl.texParameteri(target, gl.TEXTURE_WRAP_R, this.glAddress[texture._addressW]); } if (flags & TEXPROPERTY_COMPARE_ON_READ) { gl.texParameteri(target, gl.TEXTURE_COMPARE_MODE, texture._compareOnRead ? gl.COMPARE_REF_TO_TEXTURE : gl.NONE); } if (flags & TEXPROPERTY_COMPARE_FUNC) { gl.texParameteri(target, gl.TEXTURE_COMPARE_FUNC, this.glComparison[texture._compareFunc]); } if (flags & TEXPROPERTY_ANISOTROPY) { const ext = this.extTextureFilterAnisotropic; if (ext) { gl.texParameterf(target, ext.TEXTURE_MAX_ANISOTROPY_EXT, math.clamp(Math.round(texture._anisotropy), 1, this.maxAnisotropy)); } } } setTexture(texture, textureUnit) { const impl = texture.impl; if (!impl._glTexture) { impl.initialize(this, texture); } if (impl.dirtyParameterFlags > 0 || texture._needsUpload || texture._needsMipmapsUpload) { this.activeTexture(textureUnit); this.bindTexture(texture); if (impl.dirtyParameterFlags) { this.setTextureParameters(texture); impl.dirtyParameterFlags = 0; } if (texture._needsUpload || texture._needsMipmapsUpload) { impl.upload(this, texture); texture._needsUpload = false; texture._needsMipmapsUpload = false; } } else { this.bindTextureOnUnit(texture, textureUnit); } } createVertexArray(vertexBuffers) { let key, vao; const useCache = vertexBuffers.length > 1; if (useCache) { key = ''; for(let i = 0; i < vertexBuffers.length; i++){ const vertexBuffer = vertexBuffers[i]; key += vertexBuffer.id + vertexBuffer.format.renderingHash; } vao = this._vaoMap.get(key); } if (!vao) { const gl = this.gl; vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); for(let i = 0; i < vertexBuffers.length; i++){ const vertexBuffer = vertexBuffers[i]; gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer.impl.bufferId); const elements = vertexBuffer.format.elements; for(let j = 0; j < elements.length; j++){ const e = elements[j]; const loc = semanticToLocation[e.name]; if (e.asInt) { gl.vertexAttribIPointer(loc, e.numComponents, this.glType[e.dataType], e.stride, e.offset); } else { gl.vertexAttribPointer(loc, e.numComponents, this.glType[e.dataType], e.normalize, e.stride, e.offset); } gl.enableVertexAttribArray(loc); if (vertexBuffer.format.instancing) { gl.vertexAttribDivisor(loc, 1); } } } gl.bindVertexArray(null); gl.bindBuffer(gl.ARRAY_BUFFER, null); if (useCache) { this._vaoMap.set(key, vao); } } return vao; } unbindVertexArray() { if (this.boundVao) { this.boundVao = null; this.gl.bindVertexArray(null); } } setBuffers(indexBuffer) { const gl = this.gl; let vao; if (this.vertexBuffers.length === 1) { const vertexBuffer = this.vertexBuffers[0]; if (!vertexBuffer.impl.vao) { vertexBuffer.impl.vao = this.createVertexArray(this.vertexBuffers); } vao = vertexBuffer.impl.vao; } else { vao = this.createVertexArray(this.vertexBuffers); } if (this.boundVao !== vao) { this.boundVao = vao; gl.bindVertexArray(vao); } const bufferId = indexBuffer ? indexBuffer.impl.bufferId : null; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferId); } _multiDrawLoopFallback(mode, primitive, indexBuffer, numInstances, drawCommands) { const gl = this.gl; if (primitive.indexed) { const format = indexBuffer.impl.glFormat; const { glCounts, glOffsetsBytes, glInstanceCounts, count } = drawCommands.impl; if (numInstances > 0) { for(let i = 0; i < count; i++){ gl.drawElementsInstanced(mode, glCounts[i], format, glOffsetsBytes[i], glInstanceCounts[i]); } } else { for(let i = 0; i < count; i++){ gl.drawElements(mode, glCounts[i], format, glOffsetsBytes[i]); } } } else { const { glCounts, glOffsetsBytes, glInstanceCounts, count } = drawCommands.impl; if (numInstances > 0) { for(let i = 0; i < count; i++){ gl.drawArraysInstanced(mode, glOffsetsBytes[i], glCounts[i], glInstanceCounts[i]); } } else { for(let i = 0; i < count; i++){ gl.drawArrays(mode, glOffsetsBytes[i], glCounts[i]); } } } } draw(primitive, indexBuffer, numInstances, drawCommands, first = true, last = true) { const shader = this.shader; if (shader) { this.activateShader(); if (this.shaderValid) { const gl = this.gl; if (first) { this.setBuffers(indexBuffer); } let textureUnit = 0; const samplers = shader.impl.samplers; for(let i = 0, len = samplers.length; i < len; i++){ const sampler = samplers[i]; let samplerValue = sampler.scopeId.value; if (!samplerValue) { const samplerName = sampler.scopeId.name; if (samplerName === 'uSceneDepthMap') { samplerValue = getBuiltInTexture(this, 'white'); } if (samplerName === 'uSceneColorMap') { samplerValue = getBuiltInTexture(this, 'pink'); } if (!samplerValue) { samplerValue = getBuiltInTexture(this, 'pink'); } } if (samplerValue instanceof Texture) { const texture = samplerValue; this.setTexture(texture, textureUnit); if (sampler.slot !== textureUnit) { gl.uniform1i(sampler.locationId, textureUnit); sampler.slot = textureUnit; } textureUnit++; } else { sampler.array.length = 0; const numTextures = samplerValue.length; for(let j = 0; j < numTextures; j++){ const texture = samplerValue[j]; this.setTexture(texture, textureUnit); sampler.array[j] = textureUnit; textureUnit++; } gl.uniform1iv(sampler.locationId, sampler.array); } } const uniforms = shader.impl.uniforms; for(let i = 0, len = uniforms.length; i < len; i++){ const uniform = uniforms[i]; const scopeId = uniform.scopeId; const uniformVersion = uniform.version; const programVersion = scopeId.versionObject.version; if (uniformVersion.globalId !== programVersion.globalId || uniformVersion.revision !== programVersion.revision) { uniformVersion.globalId = programVersion.globalId; uniformVersion.revision = programVersion.revision; const value = scopeId.value; if (value !== null && value !== undefined) { this.commitFunction[uniform.dataType](uniform, value); } } } if (this.transformFeedbackBuffer) { gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.transformFeedbackBuffer.impl.bufferId); gl.beginTransformFeedback(gl.POINTS); } const mode = this.glPrimitive[primitive.type]; const count = primitive.count; if (drawCommands) { if (this.extMultiDraw) { const impl = drawCommands.impl; if (primitive.indexed) { const format = indexBuffer.impl.glFormat; if (numInstances > 0) { this.extMultiDraw.multiDrawElementsInstancedWEBGL(mode, impl.glCounts, 0, format, impl.glOffsetsBytes, 0, impl.glInstanceCounts, 0, drawCommands.count); } else { this.extMultiDraw.multiDrawElementsWEBGL(mode, impl.glCounts, 0, format, impl.glOffsetsBytes, 0, drawCommands.count); } } else { if (numInstances > 0) { this.extMultiDraw.multiDrawArraysInstancedWEBGL(mode, impl.glOffsetsBytes, 0, impl.glCounts, 0, impl.glInstanceCounts, 0, drawCommands.count); } else { this.extMultiDraw.multiDrawArraysWEBGL(mode, impl.glOffsetsBytes, 0, impl.glCounts, 0, drawCommands.count); } } } else { this._multiDrawLoopFallback(mode, primitive, indexBuffer, numInstances, drawCommands); } } else { if (primitive.indexed) { const format = indexBuffer.impl.glFormat; const offset = primitive.base * indexBuffer.bytesPerIndex; if (numInstances > 0) { gl.drawElementsInstanced(mode, count, format, offset, numInstances); } else { gl.drawElements(mode, count, format, offset); } } else { const first = primitive.base; if (numInstances > 0) { gl.drawArraysInstanced(mode, first, count, numInstances); } else { gl.drawArrays(mode, first, count); } } } if (this.transformFeedbackBuffer) { gl.endTransformFeedback(); gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null); } this._drawCallsPerFrame++; } } if (last) { this.clearVertexBuffer(); } } clear(options) { const defaultOptions = this.defaultClearOptions; options = options || defaultOptions; const flags = options.flags ?? defaultOptions.flags; if (flags !== 0) { const gl = this.gl; if (flags & CLEARFLAG_COLOR) { const color = options.color ?? defaultOptions.color; const r = color[0]; const g = color[1]; const b = color[2]; const a = color[3]; const c = this.clearColor; if (r !== c.r || g !== c.g || b !== c.b || a !== c.a) { this.gl.clearColor(r, g, b, a); this.clearColor.set(r, g, b, a); } this.setBlendState(BlendState.NOBLEND); } if (flags & CLEARFLAG_DEPTH) { const depth = options.depth ?? defaultOptions.depth; if (depth !== this.clearDepth) { this.gl.clearDepth(depth); this.clearDepth = depth; } this.setDepthState(DepthState.WRITEDEPTH); } if (flags & CLEARFLAG_STENCIL) { const stencil = options.stencil ?? defaultOptions.stencil; if (stencil !== this.clearStencil) { this.gl.clearStencil(stencil); this.clearStencil = stencil; } gl.stencilMask(0xFF); this.stencilWriteMaskFront = 0xFF; this.stencilWriteMaskBack = 0xFF; } gl.clear(this.glClearFlag[flags]); } } submit() { this.gl.flush(); } readPixels(x, y, w, h, pixels) { const gl = this.gl; gl.readPixels(x, y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); } clientWaitAsync(flags, interval_ms) { const gl = this.gl; const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); this.submit(); return new Promise((resolve, reject)=>{ function test() { const res = gl.clientWaitSync(sync, flags, 0); if (res === gl.TIMEOUT_EXPIRED) { setTimeout(test, interval_ms); } else { gl.deleteSync(sync); if (res === gl.WAIT_FAILED) { reject(new Error('webgl clientWaitSync sync failed')); } else { resolve(); } } } test(); }); } async readPixelsAsync(x, y, w, h, pixels) { const gl = this.gl; const impl = this.renderTarget.colorBuffer?.impl; const format = impl?._glFormat ?? gl.RGBA; const pixelType = impl?._glPixelType ?? gl.UNSIGNED_BYTE; const buf = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf); gl.bufferData(gl.PIXEL_PACK_BUFFER, pixels.byteLength, gl.STREAM_READ); gl.readPixels(x, y, w, h, format, pixelType, 0); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); await this.clientWaitAsync(0, 16); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf); gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, pixels); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); gl.deleteBuffer(buf); return pixels; } readTextureAsync(texture, x, y, width, height, options) { const face = options.face ?? 0; const mipLevel = options.mipLevel ?? 0; const renderTarget = options.renderTarget ?? new RenderTarget({ colorBuffer: texture, depth: false, face: face, mipLevel: mipLevel }); const buffer = new ArrayBuffer(TextureUtils.calcLevelGpuSize(width, height, 1, texture._format)); const data = options.data ?? new (getPixelFormatArrayType(texture._format))(buffer); this.setRenderTarget(renderTarget); this.initRenderTarget(renderTarget); this.setFramebuffer(renderTarget.impl._glFrameBuffer); if (options.immediate) { this.gl.flush(); } return new Promise((resolve, reject)=>{ this.readPixelsAsync(x, y, width, height, data).then((data)=>{ if (this._destroyed) return; if (!options.renderTarget) { renderTarget.destroy(); } resolve(data); }).catch(reject); }); } async writeTextureAsync(texture, x, y, width, height, data) { const gl = this.gl; const impl = texture.impl; const format = impl?._glFormat ?? gl.RGBA; const pixelType = impl?._glPixelType ?? gl.UNSIGNED_BYTE; const buf = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, buf); gl.bufferData(gl.PIXEL_UNPACK_BUFFER, data, gl.STREAM_DRAW); gl.bindTexture(gl.TEXTURE_2D, impl._glTexture); gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, format, pixelType, 0); gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null); texture._needsUpload = false; texture._mipmapsUploaded = false; await this.clientWaitAsync(0, 16); } setAlphaToCoverage(state) { if (this.alphaToCoverage !== state) { this.alphaToCoverage = state; if (state) { this.gl.enable(this.gl.SAMPLE_ALPHA_TO_COVERAGE); } else { this.gl.disable(this.gl.SAMPLE_ALPHA_TO_COVERAGE); } } } setTransformFeedbackBuffer(tf) { if (this.transformFeedbackBuffer !== tf) { this.transformFeedbackBuffer = tf; const gl = this.gl; if (tf) { if (!this.feedback) { this.feedback = gl.createTransformFeedback(); } gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.feedback); } else { gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); } } } setRaster(on) { if (this.raster !== on) { this.raster = on; if (on) { this.gl.disable(this.gl.RASTERIZER_DISCARD); } else { this.gl.enable(this.gl.RASTERIZER_DISCARD); } } } setStencilTest(enable) { if (this.stencil !== enable) { const gl = this.gl; if (enable) { gl.enable(gl.STENCIL_TEST); } else { gl.disable(gl.STENCIL_TEST); } this.stencil = enable; } } setStencilFunc(func, ref, mask) { if (this.stencilFuncFront !== func || this.stencilRefFront !== ref || this.stencilMaskFront !== mask || this.stencilFuncBack !== func || this.stencilRefBack !== ref || this.stencilMaskBack !== mask) { this.gl.stencilFunc(this.glComparison[func], ref, mask); this.stencilFuncFront = this.stencilFuncBack = func; this.stencilRefFront = this.stencilRefBack = ref; this.stencilMaskFront = this.stencilMaskBack = mask; } } setStencilFuncFront(func, ref, mask) { if (this.stencilFuncFront !== func || this.stencilRefFront !== ref || this.stencilMaskFront !== mask) { const gl = this.gl; gl.stencilFuncSeparate(gl.FRONT, this.glComparison[func], ref, mask); this.stencilFuncFront = func; this.stencilRefFront = ref; this.stencilMaskFront = mask; } } setStencilFuncBack(func, ref, mask) { if (this.stencilFuncBack !== func || this.stencilRefBack !== ref || this.stencilMaskBack !== mask) { const gl = this.gl; gl.stencilFuncSeparate(gl.BACK, this.glComparison[func], ref, mask); this.stencilFuncBack = func; this.stencilRefBack = ref; this.stencilMaskBack = mask; } } setStencilOperation(fail, zfail, zpass, writeMask) { if (this.stencilFailFront !== fail || this.stencilZfailFront !== zfail || this.stencilZpassFront !== zpass || this.stencilFailBack !== fail || this.stencilZfailBack !== zfail || this.stencilZpassBack !== zpass) { this.gl.stencilOp(this.glStencilOp[fail], this.glStencilOp[zfail], this.glStencilOp[zpass]); this.stencilFailFront = this.stencilFailBack = fail; this.stencilZfailFront = this.stencilZfailBack = zfail; this.stencilZpassFront = this.stencilZpassBack = zpass; } if (this.stencilWriteMaskFront !== writeMask || this.stencilWriteMaskBack !== writeMask) { this.gl.stencilMask(writeMask); this.stencilWriteMaskFront = writeMask; this.stencilWriteMaskBack = writeMask; } } setStencilOperationFront(fail, zfail, zpass, writeMask) { if (this.stencilFailFront !== fail || this.stencilZfailFront !== zfail || this.stencilZpassFront !== zpass) { this.gl.stencilOpSeparate(this.gl.FRONT, this.glStencilOp[fail], this.glStencilOp[zfail], this.glStencilOp[zpass]); this.stencilFailFront = fail; this.stencilZfailFront = zfail; this.stencilZpassFront = zpass; } if (this.stencilWriteMaskFront !== writeMask) { this.gl.stencilMaskSeparate(this.gl.FRONT, writeMask); this.stencilWriteMaskFront = writeMask; } } setStencilOperationBack(fail, zfail, zpass, writeMask) { if (this.stencilFailBack !== fail || this.stencilZfailBack !== zfail || this.stencilZpassBack !== zpass) { this.gl.stencilOpSeparate(this.gl.BACK, this.glStencilOp[fail], this.glStencilOp[zfail], this.glStencilOp[zpass]); this.stencilFailBack = fail; this.stencilZfailBack = zfail; this.stencilZpassBack = zpass; } if (this.stencilWriteMaskBack !== writeMask) { this.gl.stencilMaskSeparate(this.gl.BACK, writeMask); this.stencilWriteMaskBack = writeMask; } } setBlendState(blendState) { const currentBlendState = this.blendState; if (!currentBlendState.equals(blendState)) { const gl = this.gl; const { blend, colorOp, alphaOp, colorSrcFactor, colorDstFactor, alphaSrcFactor, alphaDstFactor } = blendState; if (currentBlendState.blend !== blend) { if (blend) { gl.enable(gl.BLEND); } else { gl.disable(gl.BLEND); } } if (currentBlendState.colorOp !== colorOp || currentBlendState.alphaOp !== alphaOp) { const glBlendEquation = this.glBlendEquation; gl.blendEquationSeparate(glBlendEquation[colorOp], glBlendEquation[alphaOp]); } if (currentBlendState.colorSrcFactor !== colorSrcFactor || currentBlendState.colorDstFactor !== colorDstFactor || currentBlendState.alphaSrcFactor !== alphaSrcFactor || currentBlendState.alphaDstFactor !== alphaDstFactor) { gl.blendFuncSeparate(this.glBlendFunctionColor[colorSrcFactor], this.glBlendFunctionColor[colorDstFactor], this.glBlendFunctionAlpha[alphaSrcFactor], this.glBlendFunctionAlpha[alphaDstFactor]); } if (currentBlendState.allWrite !== blendState.allWrite) { this.gl.colorMask(blendState.redWrite, blendState.greenWrite, blendState.blueWrite, blendState.alphaWrite); } currentBlendState.copy(blendState); } } setBlendColor(r, g, b, a) { const c = this.blendColor; if (r !== c.r || g !== c.g || b !== c.b || a !== c.a) { this.gl.blendColor(r, g, b, a); c.set(r, g, b, a); } } setStencilState(stencilFront, stencilBack) { if (stencilFront || stencilBack) { this.setStencilTest(true); if (stencilFront === stencilBack) { this.setStencilFunc(stencilFront.func, stencilFront.ref, stencilFront.readMask); this.setStencilOperation(stencilFront.fail, stencilFront.zfail, stencilFront.zpass, stencilFront.writeMask); } else { stencilFront ?? (stencilFront = StencilParameters.DEFAULT); this.setStencilFuncFront(stencilFront.func, stencilFront.ref, stencilFront.readMask); this.setStencilOperationFront(stencilFront.fail, stencilFront.zfail, stencilFront.zpass, stencilFront.writeMask); stencilBack ?? (stencilBack = StencilParameters.DEFAULT); this.setStencilFuncBack(stencilBack.func, stencilBack.ref, stencilBack.readMask); this.setStencilOperationBack(stencilBack.fail, stencilBack.zfail, stencilBack.zpass, stencilBack.writeMask); } } else { this.setStencilTest(false); } } setDepthState(depthState) { const currentDepthState = this.depthState; if (!currentDepthState.equals(depthState)) { const gl = this.gl; const write = depthState.write; if (currentDepthState.write !== write) { gl.depthMask(write); } let { func, test } = depthState; if (!test && write) { test = true; func = FUNC_ALWAYS; } if (currentDepthState.func !== func) { gl.depthFunc(this.glComparison[func]); } if (currentDepthState.test !== test) { if (test) { gl.enable(gl.DEPTH_TEST); } else { gl.disable(gl.DEPTH_TEST); } } const { depthBias, depthBiasSlope } = depthState; if (depthBias || depthBiasSlope) { if (!this.depthBiasEnabled) { this.depthBiasEnabled = true; this.gl.enable(this.gl.POLYGON_OFFSET_FILL); } gl.polygonOffset(depthBiasSlope, depthBias); } else { if (this.depthBiasEnabled) { this.depthBiasEnabled = false; this.gl.disable(this.gl.POLYGON_OFFSET_FILL); } } currentDepthState.copy(depthState); } } setCullMode(cullMode) { if (this.cullMode !== cullMode) { if (cullMode === CULLFACE_NONE) { this.gl.disable(this.gl.CULL_FACE); } else { if (this.cullMode === CULLFACE_NONE) { this.gl.enable(this.gl.CULL_FACE); } const mode = this.glCull[cullMode]; if (this.cullFace !== mode) { this.gl.cullFace(mode); this.cullFace = mode; } } this.cullMode = cullMode; } } setShader(shader, asyncCompile = false) { if (shader !== this.shader) { this.shader = shader; this.shaderAsyncCompile = asyncCompile; this.shaderValid = undefined; } } activateShader() { const { shader } = this; const { impl } = shader; if (this.shaderValid === undefined) { if (shader.failed) { this.shaderValid = false; } else if (!shader.ready) { if (this.shaderAsyncCompile) { if (impl.isLinked(this)) { if (!impl.finalize(this, shader)) { shader.failed = true; this.shaderValid = false; } } else { this.shaderValid = false; } } else { if (!impl.finalize(this, shader)) { shader.failed = true; this.shaderValid = false; } } } } if (this.shaderValid === undefined) { this.gl.useProgram(impl.glProgram); this.shaderValid = true; } } clearVertexArrayObjectCache() { const gl = this.gl; this._vaoMap.forEach((item, key, mapObj)=>{ gl.deleteVertexArray(item); }); this._vaoMap.clear(); } set fullscreen(fullscreen) { if (fullscreen) { const canvas = this.gl.canvas; canvas.requestFullscreen(); } else { document.exitFullscreen(); } } get fullscreen() { return !!document.fullscreenElement; } constructor(canvas, options = {}){ super(canvas, options), this._defaultFramebuffer = null, this._defaultFramebufferChanged = false; options = this.initOptions; this.updateClientRect(); this.initTextureUnits(); this.contextLost = false; this._contextLostHandler = (event)=>{ event.preventDefault(); this.loseContext(); this.fire('devicelost'); }; this._contextRestoredHandler = ()=>{ this.restoreContext(); this.fire('devicerestored'); }; const ua = typeof navigator !== 'undefined' && navigator.userAgent; this.forceDisableMultisampling = ua && ua.includes('AppleWebKit') && (ua.includes('15.4') || ua.includes('15_4')); if (this.forceDisableMultisampling) { options.antialias = false; } if (platform.browserName === 'firefox') { const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''; const match = ua.match(/Firefox\/(\d+(\.\d+)*)/); const firefoxVersion = match ? match[1] : null; if (firefoxVersion) { const version = parseFloat(firefoxVersion); const disableAntialias = platform.name === 'windows' && (version >= 120 || version === 115) || platform.name === 'android' && version >= 132; if (disableAntialias) { options.antialias = false; } } } this.backBufferAntialias = options.antialias ?? false; options.antialias = false; const gl = options.gl ?? canvas.getContext('webgl2', options); if (!gl) { throw new Error('WebGL not supported'); } this.gl = gl; this.isWebGL2 = true; this._deviceType = DEVICETYPE_WEBGL2; this.updateBackbufferFormat(null); const isChrome = platform.browserName === 'chrome'; const isSafari = platform.browserName === 'safari'; const isMac = platform.browser && navigator.appVersion.indexOf('Mac') !== -1; this._tempEnableSafariTextureUnitWorkaround = isSafari; this._tempMacChromeBlitFramebufferWorkaround = isMac && isChrome && !options.alpha; canvas.addEventListener('webglcontextlost', this._contextLostHandler, false); canvas.addEventListener('webglcontextrestored', this._contextRestoredHandler, false); this.initializeExtensions(); this.initializeCapabilities(); this.initializeRenderState(); this.initializeContextCaches(); this.createBackbuffer(null); this.supportsImageBitmap = !isSafari && typeof ImageBitmap !== 'undefined'; this._samplerTypes = new Set([ gl.SAMPLER_2D, gl.SAMPLER_CUBE, gl.UNSIGNED_INT_SAMPLER_2D, gl.INT_SAMPLER_2D, gl.SAMPLER_2D_SHADOW, gl.SAMPLER_CUBE_SHADOW, gl.SAMPLER_3D, gl.INT_SAMPLER_3D, gl.UNSIGNED_INT_SAMPLER_3D, gl.SAMPLER_2D_ARRAY, gl.INT_SAMPLER_2D_ARRAY, gl.UNSIGNED_INT_SAMPLER_2D_ARRAY ]); this.glAddress = [ gl.REPEAT, gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT ]; this.glBlendEquation = [ gl.FUNC_ADD, gl.FUNC_SUBTRACT, gl.FUNC_REVERSE_SUBTRACT, gl.MIN, gl.MAX ]; this.glBlendFunctionColor = [ gl.ZERO, gl.ONE, gl.SRC_COLOR, gl.ONE_MINUS_SRC_COLOR, gl.DST_COLOR, gl.ONE_MINUS_DST_COLOR, gl.SRC_ALPHA, gl.SRC_ALPHA_SATURATE, gl.ONE_MINUS_SRC_ALPHA, gl.DST_ALPHA, gl.ONE_MINUS_DST_ALPHA, gl.CONSTANT_COLOR, gl.ONE_MINUS_CONSTANT_COLOR ]; this.glBlendFunctionAlpha = [ gl.ZERO, gl.ONE, gl.SRC_COLOR, gl.ONE_MINUS_SRC_COLOR, gl.DST_COLOR, gl.ONE_MINUS_DST_COLOR, gl.SRC_ALPHA, gl.SRC_ALPHA_SATURATE, gl.ONE_MINUS_SRC_ALPHA, gl.DST_ALPHA, gl.ONE_MINUS_DST_ALPHA, gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA ]; this.glComparison = [ gl.NEVER, gl.LESS, gl.EQUAL, gl.LEQUAL, gl.GREATER, gl.NOTEQUAL, gl.GEQUAL, gl.ALWAYS ]; this.glStencilOp = [ gl.KEEP, gl.ZERO, gl.REPLACE, gl.INCR, gl.INCR_WRAP, gl.DECR, gl.DECR_WRAP, gl.INVERT ]; this.glClearFlag = [ 0, gl.COLOR_BUFFER_BIT, gl.DEPTH_BUFFER_BIT, gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT, gl.STENCIL_BUFFER_BIT, gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT, gl.STENCIL_BUFFER_BIT | gl.DEPTH_BUFFER_BIT, gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ]; this.glCull = [ 0, gl.BACK, gl.FRONT, gl.FRONT_AND_BACK ]; this.glFilter = [ gl.NEAREST, gl.LINEAR, gl.NEAREST_MIPMAP_NEAREST, gl.NEAREST_MIPMAP_LINEAR, gl.LINEAR_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_LINEAR ]; this.glPrimitive = [ gl.POINTS, gl.LINES, gl.LINE_LOOP, gl.LINE_STRIP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN ]; this.glType = [ gl.BYTE, gl.UNSIGNED_BYTE, gl.SHORT, gl.UNSIGNED_SHORT, gl.INT, gl.UNSIGNED_INT, gl.FLOAT, gl.HALF_FLOAT ]; this.pcUniformType = {}; this.pcUniformType[gl.BOOL] = UNIFORMTYPE_BOOL; this.pcUniformType[gl.INT] = UNIFORMTYPE_INT; this.pcUniformType[gl.FLOAT] = UNIFORMTYPE_FLOAT; this.pcUniformType[gl.FLOAT_VEC2] = UNIFORMTYPE_VEC2; this.pcUniformType[gl.FLOAT_VEC3] = UNIFORMTYPE_VEC3; this.pcUniformType[gl.FLOAT_VEC4] = UNIFORMTYPE_VEC4; this.pcUniformType[gl.INT_VEC2] = UNIFORMTYPE_IVEC2; this.pcUniformType[gl.INT_VEC3] = UNIFORMTYPE_IVEC3; this.pcUniformType[gl.INT_VEC4] = UNIFORMTYPE_IVEC4; this.pcUniformType[gl.BOOL_VEC2] = UNIFORMTYPE_BVEC2; this.pcUniformType[gl.BOOL_VEC3] = UNIFORMTYPE_BVEC3; this.pcUniformType[gl.BOOL_VEC4] = UNIFORMTYPE_BVEC4; this.pcUniformType[gl.FLOAT_MAT2] = UNIFORMTYPE_MAT2; this.pcUniformType[gl.FLOAT_MAT3] = UNIFORMTYPE_MAT3; this.pcUniformType[gl.FLOAT_MAT4] = UNIFORMTYPE_MAT4; this.pcUniformType[gl.SAMPLER_2D] = UNIFORMTYPE_TEXTURE2D; this.pcUniformType[gl.SAMPLER_CUBE] = UNIFORMTYPE_TEXTURECUBE; this.pcUniformType[gl.UNSIGNED_INT] = UNIFORMTYPE_UINT; this.pcUniformType[gl.UNSIGNED_INT_VEC2] = UNIFORMTYPE_UVEC2; this.pcUniformType[gl.UNSIGNED_INT_VEC3] = UNIFORMTYPE_UVEC3; this.pcUniformType[gl.UNSIGNED_INT_VEC4] = UNIFORMTYPE_UVEC4; this.pcUniformType[gl.SAMPLER_2D_SHADOW] = UNIFORMTYPE_TEXTURE2D_SHADOW; this.pcUniformType[gl.SAMPLER_CUBE_SHADOW] = UNIFORMTYPE_TEXTURECUBE_SHADOW; this.pcUniformType[gl.SAMPLER_2D_ARRAY] = UNIFORMTYPE_TEXTURE2D_ARRAY; this.pcUniformType[gl.SAMPLER_3D] = UNIFORMTYPE_TEXTURE3D; this.pcUniformType[gl.INT_SAMPLER_2D] = UNIFORMTYPE_ITEXTURE2D; this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_2D] = UNIFORMTYPE_UTEXTURE2D; this.pcUniformType[gl.INT_SAMPLER_CUBE] = UNIFORMTYPE_ITEXTURECUBE; this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_2D] = UNIFORMTYPE_UTEXTURECUBE; this.pcUniformType[gl.INT_SAMPLER_3D] = UNIFORMTYPE_ITEXTURE3D; this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_3D] = UNIFORMTYPE_UTEXTURE3D; this.pcUniformType[gl.INT_SAMPLER_2D_ARRAY] = UNIFORMTYPE_ITEXTURE2D_ARRAY; this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_2D_ARRAY] = UNIFORMTYPE_UTEXTURE2D_ARRAY; this.targetToSlot = {}; this.targetToSlot[gl.TEXTURE_2D] = 0; this.targetToSlot[gl.TEXTURE_CUBE_MAP] = 1; this.targetToSlot[gl.TEXTURE_3D] = 2; let scopeX, scopeY, scopeZ, scopeW; let uniformValue; this.commitFunction = []; this.commitFunction[UNIFORMTYPE_BOOL] = function(uniform, value) { if (uniform.value !== value) { gl.uniform1i(uniform.locationId, value); uniform.value = value; } }; this.commitFunction[UNIFORMTYPE_INT] = this.commitFunction[UNIFORMTYPE_BOOL]; this.commitFunction[UNIFORMTYPE_FLOAT] = function(uniform, value) { if (uniform.value !== value) { gl.uniform1f(uniform.locationId, value); uniform.value = value; } }; this.commitFunction[UNIFORMTYPE_VEC2] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY) { gl.uniform2fv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; } }; this.commitFunction[UNIFORMTYPE_VEC3] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; scopeZ = value[2]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ) { gl.uniform3fv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; uniformValue[2] = scopeZ; } }; this.commitFunction[UNIFORMTYPE_VEC4] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; scopeZ = value[2]; scopeW = value[3]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ || uniformValue[3] !== scopeW) { gl.uniform4fv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; uniformValue[2] = scopeZ; uniformValue[3] = scopeW; } }; this.commitFunction[UNIFORMTYPE_IVEC2] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY) { gl.uniform2iv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; } }; this.commitFunction[UNIFORMTYPE_BVEC2] = this.commitFunction[UNIFORMTYPE_IVEC2]; this.commitFunction[UNIFORMTYPE_IVEC3] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; scopeZ = value[2]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ) { gl.uniform3iv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; uniformValue[2] = scopeZ; } }; this.commitFunction[UNIFORMTYPE_BVEC3] = this.commitFunction[UNIFORMTYPE_IVEC3]; this.commitFunction[UNIFORMTYPE_IVEC4] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; scopeZ = value[2]; scopeW = value[3]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ || uniformValue[3] !== scopeW) { gl.uniform4iv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; uniformValue[2] = scopeZ; uniformValue[3] = scopeW; } }; this.commitFunction[UNIFORMTYPE_BVEC4] = this.commitFunction[UNIFORMTYPE_IVEC4]; this.commitFunction[UNIFORMTYPE_MAT2] = function(uniform, value) { gl.uniformMatrix2fv(uniform.locationId, false, value); }; this.commitFunction[UNIFORMTYPE_MAT3] = function(uniform, value) { gl.uniformMatrix3fv(uniform.locationId, false, value); }; this.commitFunction[UNIFORMTYPE_MAT4] = function(uniform, value) { gl.uniformMatrix4fv(uniform.locationId, false, value); }; this.commitFunction[UNIFORMTYPE_FLOATARRAY] = function(uniform, value) { gl.uniform1fv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_VEC2ARRAY] = function(uniform, value) { gl.uniform2fv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_VEC3ARRAY] = function(uniform, value) { gl.uniform3fv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_VEC4ARRAY] = function(uniform, value) { gl.uniform4fv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_UINT] = function(uniform, value) { if (uniform.value !== value) { gl.uniform1ui(uniform.locationId, value); uniform.value = value; } }; this.commitFunction[UNIFORMTYPE_UVEC2] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY) { gl.uniform2uiv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; } }; this.commitFunction[UNIFORMTYPE_UVEC3] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; scopeZ = value[2]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ) { gl.uniform3uiv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; uniformValue[2] = scopeZ; } }; this.commitFunction[UNIFORMTYPE_UVEC4] = function(uniform, value) { uniformValue = uniform.value; scopeX = value[0]; scopeY = value[1]; scopeZ = value[2]; scopeW = value[3]; if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ || uniformValue[3] !== scopeW) { gl.uniform4uiv(uniform.locationId, value); uniformValue[0] = scopeX; uniformValue[1] = scopeY; uniformValue[2] = scopeZ; uniformValue[3] = scopeW; } }; this.commitFunction[UNIFORMTYPE_INTARRAY] = function(uniform, value) { gl.uniform1iv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_UINTARRAY] = function(uniform, value) { gl.uniform1uiv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_BOOLARRAY] = this.commitFunction[UNIFORMTYPE_INTARRAY]; this.commitFunction[UNIFORMTYPE_IVEC2ARRAY] = function(uniform, value) { gl.uniform2iv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_UVEC2ARRAY] = function(uniform, value) { gl.uniform2uiv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_BVEC2ARRAY] = this.commitFunction[UNIFORMTYPE_IVEC2ARRAY]; this.commitFunction[UNIFORMTYPE_IVEC3ARRAY] = function(uniform, value) { gl.uniform3iv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_UVEC3ARRAY] = function(uniform, value) { gl.uniform3uiv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_BVEC3ARRAY] = this.commitFunction[UNIFORMTYPE_IVEC3ARRAY]; this.commitFunction[UNIFORMTYPE_IVEC4ARRAY] = function(uniform, value) { gl.uniform4iv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_UVEC4ARRAY] = function(uniform, value) { gl.uniform4uiv(uniform.locationId, value); }; this.commitFunction[UNIFORMTYPE_BVEC4ARRAY] = this.commitFunction[UNIFORMTYPE_IVEC4ARRAY]; this.commitFunction[UNIFORMTYPE_MAT4ARRAY] = function(uniform, value) { gl.uniformMatrix4fv(uniform.locationId, false, value); }; this.constantTexSource = this.scope.resolve('source'); this.postInit(); } } class NullIndexBuffer { unlock(indexBuffer) {} } class NullRenderTarget { destroy(device) {} init(device, renderTarget) {} loseContext() {} resolve(device, target, color, depth) {} } class NullShader { destroy(shader) {} loseContext() {} restoreContext(device, shader) {} } class NullTexture { destroy(device) {} propertyChanged(flag) {} loseContext() {} } class NullVertexBuffer { destroy(device) {} unlock(vertexBuffer) {} } class NullDrawCommands { add(i, indexOrVertexCount, instanceCount, firstIndexOrVertex) {} } class NullGraphicsDevice extends GraphicsDevice { destroy() { super.destroy(); } initDeviceCaps() { this.disableParticleSystem = true; this.precision = 'highp'; this.maxPrecision = 'highp'; this.maxSamples = 4; this.maxTextures = 16; this.maxTextureSize = 4096; this.maxCubeMapSize = 4096; this.maxVolumeSize = 4096; this.maxColorAttachments = 8; this.maxPixelRatio = 1; this.maxAnisotropy = 16; this.supportsUniformBuffers = false; this.supportsAreaLights = true; this.supportsGpuParticles = false; this.textureFloatRenderable = true; this.textureHalfFloatRenderable = true; this.supportsImageBitmap = false; } postInit() { super.postInit(); } frameStart() { super.frameStart(); } frameEnd() { super.frameEnd(); } updateBegin() {} updateEnd() {} readPixels(x, y, w, h, pixels) {} createVertexBufferImpl(vertexBuffer, format) { return new NullVertexBuffer(vertexBuffer, format); } createIndexBufferImpl(indexBuffer) { return new NullIndexBuffer(indexBuffer); } createShaderImpl(shader) { return new NullShader(shader); } createTextureImpl(texture) { return new NullTexture(texture); } createRenderTargetImpl(renderTarget) { return new NullRenderTarget(renderTarget); } createDrawCommandImpl(drawCommands) { return new NullDrawCommands(); } createUploadStreamImpl(uploadStream) { return null; } draw(primitive, indexBuffer, numInstances, drawCommands, first = true, last = true) {} setShader(shader, asyncCompile = false) {} setBlendState(blendState) {} setDepthState(depthState) {} setStencilState(stencilFront, stencilBack) {} setBlendColor(r, g, b, a) {} setCullMode(cullMode) {} setAlphaToCoverage(state) {} initializeContextCaches() { super.initializeContextCaches(); } clear(options) {} setViewport(x, y, w, h) {} setScissor(x, y, w, h) {} copyRenderTarget(source, dest, color, depth) { return true; } constructor(canvas, options = {}){ super(canvas, options); options = this.initOptions; this.isNull = true; this._deviceType = DEVICETYPE_NULL; this.samples = 1; this.backBuffer = new RenderTarget({ name: 'Framebuffer', graphicsDevice: this, depth: this.initOptions.depth, stencil: this.supportsStencil, samples: this.samples }); this.initDeviceCaps(); } } function createGraphicsDevice(canvas, options = {}) { const deviceTypes = options.deviceTypes ?? []; if (!deviceTypes.includes(DEVICETYPE_WEBGL2)) { deviceTypes.push(DEVICETYPE_WEBGL2); } if (!deviceTypes.includes(DEVICETYPE_NULL)) { deviceTypes.push(DEVICETYPE_NULL); } if (platform.browser && !!navigator.xr) { var _options; (_options = options).xrCompatible ?? (_options.xrCompatible = true); } const deviceCreateFuncs = []; for(let i = 0; i < deviceTypes.length; i++){ const deviceType = deviceTypes[i]; if (deviceType === DEVICETYPE_WEBGPU && window?.navigator?.gpu) { deviceCreateFuncs.push(()=>{ const device = new WebgpuGraphicsDevice(canvas, options); return device.initWebGpu(options.glslangUrl, options.twgslUrl); }); } if (deviceType === DEVICETYPE_WEBGL2) { deviceCreateFuncs.push(()=>{ return new WebglGraphicsDevice(canvas, options); }); } if (deviceType === DEVICETYPE_NULL) { deviceCreateFuncs.push(()=>{ return new NullGraphicsDevice(canvas, options); }); } } return new Promise((resolve, reject)=>{ let attempt = 0; const next = ()=>{ if (attempt >= deviceCreateFuncs.length) { reject(new Error('Failed to create a graphics device')); } else { Promise.resolve(deviceCreateFuncs[attempt++]()).then((device)=>{ if (device) { resolve(device); } else { next(); } }).catch((err)=>{ console.log(err); next(); }); } }; next(); }); } class ComputeParameter { constructor(){ this.scopeId = null; } } class Compute { setParameter(name, value) { let param = this.parameters.get(name); if (!param) { param = new ComputeParameter(); param.scopeId = this.device.scope.resolve(name); this.parameters.set(name, param); } param.value = value; } getParameter(name) { return this.parameters.get(name)?.value; } deleteParameter(name) { this.parameters.delete(name); } applyParameters() { for (const [, param] of this.parameters){ param.scopeId.setValue(param.value); } } setupDispatch(x, y, z) { this.countX = x; this.countY = y; this.countZ = z; this.indirectSlotIndex = -1; this.indirectBuffer = null; } setupIndirectDispatch(slotIndex, buffer = null) { this.indirectSlotIndex = slotIndex; this.indirectBuffer = buffer; this.indirectFrameStamp = this.device.renderVersion; } constructor(graphicsDevice, shader, name = 'Unnamed'){ this.shader = null; this.parameters = new Map(); this.countX = 1; this.indirectSlotIndex = -1; this.indirectBuffer = null; this.indirectFrameStamp = 0; this.device = graphicsDevice; this.shader = shader; this.name = name; if (graphicsDevice.supportsCompute) { this.impl = graphicsDevice.createComputeImpl(this); } } } class DrawCommands { get maxCount() { return this._maxCount; } get count() { return this._count; } destroy() { this.impl?.destroy?.(); this.impl = null; } allocate(maxCount) { this._maxCount = maxCount; this.impl.allocate?.(maxCount); } add(i, indexOrVertexCount, instanceCount, firstIndexOrVertex, baseVertex = 0, firstInstance = 0) { this.impl.add(i, indexOrVertexCount, instanceCount, firstIndexOrVertex, baseVertex, firstInstance); } update(count) { this._count = count; this.primitiveCount = this.impl.update?.(count) ?? 0; } constructor(device, indexSizeBytes = 0){ this._maxCount = 0; this.impl = null; this._count = 1; this.slotIndex = 0; this.primitiveCount = 0; this.device = device; this.indexSizeBytes = indexSizeBytes; this.impl = device.createDrawCommandImpl(this); } } let id$6 = 0; class IndexBuffer { destroy() { const device = this.device; device.buffers.delete(this); if (this.device.indexBuffer === this) { this.device.indexBuffer = null; } if (this.impl.initialized) { this.impl.destroy(device); this.adjustVramSizeTracking(device._vram, -this.storage.byteLength); } } adjustVramSizeTracking(vram, size) { vram.ib += size; } loseContext() { this.impl.loseContext(); } getFormat() { return this.format; } getNumIndices() { return this.numIndices; } lock() { return this.storage; } unlock() { this.impl.unlock(this); } setData(data) { if (data.byteLength !== this.numBytes) { return false; } this.storage = data; this.unlock(); return true; } _lockTypedArray() { const lock = this.lock(); const indices = this.format === INDEXFORMAT_UINT32 ? new Uint32Array(lock) : this.format === INDEXFORMAT_UINT16 ? new Uint16Array(lock) : new Uint8Array(lock); return indices; } writeData(data, count) { const indices = this._lockTypedArray(); if (data.length > count) { if (ArrayBuffer.isView(data)) { data = data.subarray(0, count); indices.set(data); } else { for(let i = 0; i < count; i++){ indices[i] = data[i]; } } } else { indices.set(data); } this.unlock(); } readData(data) { const indices = this._lockTypedArray(); const count = this.numIndices; if (ArrayBuffer.isView(data)) { data.set(indices); } else { data.length = 0; for(let i = 0; i < count; i++){ data[i] = indices[i]; } } return count; } constructor(graphicsDevice, format, numIndices, usage = BUFFER_STATIC, initialData, options){ this.device = graphicsDevice; this.format = format; this.numIndices = numIndices; this.usage = usage; this.id = id$6++; this.impl = graphicsDevice.createIndexBufferImpl(this, options); const bytesPerIndex = typedArrayIndexFormatsByteSize[format]; this.bytesPerIndex = bytesPerIndex; this.numBytes = this.numIndices * bytesPerIndex; if (initialData) { this.setData(initialData); } else { this.storage = new ArrayBuffer(this.numBytes); } this.adjustVramSizeTracking(graphicsDevice._vram, this.numBytes); this.device.buffers.add(this); } } class ColorAttachmentOps { constructor(){ this.clearValue = new Color(0, 0, 0, 1); this.clearValueLinear = new Color(0, 0, 0, 1); this.clear = false; this.store = false; this.resolve = true; this.genMipmaps = false; } } class DepthStencilAttachmentOps { constructor(){ this.clearDepthValue = 1; this.clearStencilValue = 0; this.clearDepth = false; this.clearStencil = false; this.storeDepth = false; this.resolveDepth = false; this.storeStencil = false; } } class RenderPass { get colorOps() { return this.colorArrayOps[0]; } set name(value) { this._name = value; } get name() { if (!this._name) { this._name = this.constructor.name; } return this._name; } set scaleX(value) { this._options.scaleX = value; } get scaleX() { return this._options.scaleX; } set scaleY(value) { this._options.scaleY = value; } get scaleY() { return this._options.scaleY; } set options(value) { this._options = value; if (value) { this.scaleX = this.scaleX ?? 1; this.scaleY = this.scaleY ?? 1; } } get options() { return this._options; } init(renderTarget = null, options) { this.options = options; this.renderTarget = renderTarget; this.samples = Math.max(this.renderTarget ? this.renderTarget.samples : this.device.samples, 1); this.allocateAttachments(); this.postInit(); } allocateAttachments() { const rt = this.renderTarget; this.depthStencilOps = new DepthStencilAttachmentOps(); if (rt?.depthBuffer) { this.depthStencilOps.storeDepth = true; } const numColorOps = rt ? rt._colorBuffers?.length ?? 0 : 1; this.colorArrayOps.length = 0; for(let i = 0; i < numColorOps; i++){ const colorOps = new ColorAttachmentOps(); this.colorArrayOps[i] = colorOps; if (this.samples === 1) { colorOps.store = true; colorOps.resolve = false; } const colorBuffer = this.renderTarget?._colorBuffers?.[i]; if (this.renderTarget?.mipmaps && colorBuffer?.mipmaps) { const intFormat = isIntegerPixelFormat(colorBuffer._format); colorOps.genMipmaps = !intFormat; } } } destroy() {} postInit() {} frameUpdate() { if (this._options && this.renderTarget) { const resizeSource = this._options.resizeSource ?? this.device.backBuffer; const width = Math.floor(resizeSource.width * this.scaleX); const height = Math.floor(resizeSource.height * this.scaleY); this.renderTarget.resize(width, height); } } before() {} execute() {} after() {} onEnable() {} onDisable() {} set enabled(value) { if (this._enabled !== value) { this._enabled = value; if (value) { this.onEnable(); } else { this.onDisable(); } } } get enabled() { return this._enabled; } setClearColor(color) { const count = this.colorArrayOps.length; for(let i = 0; i < count; i++){ const colorOps = this.colorArrayOps[i]; if (color) { colorOps.clearValue.copy(color); colorOps.clearValueLinear.linear(color); } colorOps.clear = !!color; } } setClearDepth(depthValue) { if (depthValue !== undefined) { this.depthStencilOps.clearDepthValue = depthValue; } this.depthStencilOps.clearDepth = depthValue !== undefined; } setClearStencil(stencilValue) { if (stencilValue !== undefined) { this.depthStencilOps.clearStencilValue = stencilValue; } this.depthStencilOps.clearStencil = stencilValue !== undefined; } render() { if (this.enabled) { const device = this.device; const realPass = this.renderTarget !== undefined; this.before(); if (this.executeEnabled) { if (realPass && !this._skipStart) { device.startRenderPass(this); } this.execute(); if (realPass && !this._skipEnd) { device.endRenderPass(this); } } this.after(); device.renderPassIndex++; } } constructor(graphicsDevice){ this._enabled = true; this._skipStart = false; this._skipEnd = false; this.executeEnabled = true; this.samples = 0; this.colorArrayOps = []; this.requiresCubemaps = true; this.fullSizeClearRect = true; this.beforePasses = []; this.afterPasses = []; this.device = graphicsDevice; } } function set1(a) { this.array[this.index] = a; } function set2(a, b) { this.array[this.index] = a; this.array[this.index + 1] = b; } function set3(a, b, c) { this.array[this.index] = a; this.array[this.index + 1] = b; this.array[this.index + 2] = c; } function set4(a, b, c, d) { this.array[this.index] = a; this.array[this.index + 1] = b; this.array[this.index + 2] = c; this.array[this.index + 3] = d; } function arraySet1(index, inputArray, inputIndex) { this.array[index] = inputArray[inputIndex]; } function arraySet2(index, inputArray, inputIndex) { this.array[index] = inputArray[inputIndex]; this.array[index + 1] = inputArray[inputIndex + 1]; } function arraySet3(index, inputArray, inputIndex) { this.array[index] = inputArray[inputIndex]; this.array[index + 1] = inputArray[inputIndex + 1]; this.array[index + 2] = inputArray[inputIndex + 2]; } function arraySet4(index, inputArray, inputIndex) { this.array[index] = inputArray[inputIndex]; this.array[index + 1] = inputArray[inputIndex + 1]; this.array[index + 2] = inputArray[inputIndex + 2]; this.array[index + 3] = inputArray[inputIndex + 3]; } function arrayGet1(offset, outputArray, outputIndex) { outputArray[outputIndex] = this.array[offset]; } function arrayGet2(offset, outputArray, outputIndex) { outputArray[outputIndex] = this.array[offset]; outputArray[outputIndex + 1] = this.array[offset + 1]; } function arrayGet3(offset, outputArray, outputIndex) { outputArray[outputIndex] = this.array[offset]; outputArray[outputIndex + 1] = this.array[offset + 1]; outputArray[outputIndex + 2] = this.array[offset + 2]; } function arrayGet4(offset, outputArray, outputIndex) { outputArray[outputIndex] = this.array[offset]; outputArray[outputIndex + 1] = this.array[offset + 1]; outputArray[outputIndex + 2] = this.array[offset + 2]; outputArray[outputIndex + 3] = this.array[offset + 3]; } class VertexIteratorAccessor { get(offset) { return this.array[this.index + offset]; } set(a, b, c, d) {} getToArray(offset, outputArray, outputIndex) {} setFromArray(index, inputArray, inputIndex) {} constructor(buffer, vertexElement, vertexFormat){ this.index = 0; this.numComponents = vertexElement.numComponents; if (vertexFormat.interleaved) { this.array = new typedArrayTypes[vertexElement.dataType](buffer, vertexElement.offset); } else { this.array = new typedArrayTypes[vertexElement.dataType](buffer, vertexElement.offset, vertexFormat.vertexCount * vertexElement.numComponents); } this.stride = vertexElement.stride / this.array.constructor.BYTES_PER_ELEMENT; switch(vertexElement.numComponents){ case 1: this.set = set1; this.getToArray = arrayGet1; this.setFromArray = arraySet1; break; case 2: this.set = set2; this.getToArray = arrayGet2; this.setFromArray = arraySet2; break; case 3: this.set = set3; this.getToArray = arrayGet3; this.setFromArray = arraySet3; break; case 4: this.set = set4; this.getToArray = arrayGet4; this.setFromArray = arraySet4; break; } } } class VertexIterator { next(count = 1) { let i = 0; const accessors = this.accessors; const numAccessors = this.accessors.length; while(i < numAccessors){ const accessor = accessors[i++]; accessor.index += count * accessor.stride; } } end() { this.vertexBuffer.unlock(); } writeData(semantic, data, numVertices) { const element = this.element[semantic]; if (element) { if (numVertices > this.vertexBuffer.numVertices) { numVertices = this.vertexBuffer.numVertices; } const numComponents = element.numComponents; if (this.vertexBuffer.getFormat().interleaved) { let index = 0; for(let i = 0; i < numVertices; i++){ element.setFromArray(index, data, i * numComponents); index += element.stride; } } else { if (data.length > numVertices * numComponents) { const copyCount = numVertices * numComponents; if (ArrayBuffer.isView(data)) { data = data.subarray(0, copyCount); element.array.set(data); } else { for(let i = 0; i < copyCount; i++){ element.array[i] = data[i]; } } } else { element.array.set(data); } } } } readData(semantic, data) { const element = this.element[semantic]; let count = 0; if (element) { count = this.vertexBuffer.numVertices; let i; const numComponents = element.numComponents; if (this.vertexBuffer.getFormat().interleaved) { if (Array.isArray(data)) { data.length = 0; } element.index = 0; let offset = 0; for(i = 0; i < count; i++){ element.getToArray(offset, data, i * numComponents); offset += element.stride; } } else { if (ArrayBuffer.isView(data)) { data.set(element.array); } else { data.length = 0; const copyCount = count * numComponents; for(i = 0; i < copyCount; i++){ data[i] = element.array[i]; } } } } return count; } constructor(vertexBuffer){ this.vertexBuffer = vertexBuffer; this.vertexFormatSize = vertexBuffer.getFormat().size; this.buffer = this.vertexBuffer.lock(); this.accessors = []; this.element = {}; const vertexFormat = this.vertexBuffer.getFormat(); for(let i = 0; i < vertexFormat.elements.length; i++){ const vertexElement = vertexFormat.elements[i]; this.accessors[i] = new VertexIteratorAccessor(this.buffer, vertexElement, vertexFormat); this.element[vertexElement.name] = this.accessors[i]; } } } const MOUSEBUTTON_NONE = -1; class KeyboardEvent { constructor(keyboard, event){ this.key = null; this.element = null; this.event = null; if (event) { this.key = event.keyCode; this.element = event.target; this.event = event; } } } const _keyboardEvent = new KeyboardEvent(); function makeKeyboardEvent(event) { _keyboardEvent.key = event.keyCode; _keyboardEvent.element = event.target; _keyboardEvent.event = event; return _keyboardEvent; } function toKeyCode(s) { if (typeof s === 'string') { return s.toUpperCase().charCodeAt(0); } return s; } const _keyCodeToKeyIdentifier = { '9': 'Tab', '13': 'Enter', '16': 'Shift', '17': 'Control', '18': 'Alt', '27': 'Escape', '37': 'Left', '38': 'Up', '39': 'Right', '40': 'Down', '46': 'Delete', '91': 'Win' }; class Keyboard extends EventHandler { attach(element) { if (this._element) { this.detach(); } this._element = element; this._element.addEventListener('keydown', this._keyDownHandler, false); this._element.addEventListener('keypress', this._keyPressHandler, false); this._element.addEventListener('keyup', this._keyUpHandler, false); document.addEventListener('visibilitychange', this._visibilityChangeHandler, false); window.addEventListener('blur', this._windowBlurHandler, false); } detach() { if (!this._element) { return; } this._element.removeEventListener('keydown', this._keyDownHandler); this._element.removeEventListener('keypress', this._keyPressHandler); this._element.removeEventListener('keyup', this._keyUpHandler); this._element = null; document.removeEventListener('visibilitychange', this._visibilityChangeHandler, false); window.removeEventListener('blur', this._windowBlurHandler, false); } toKeyIdentifier(keyCode) { keyCode = toKeyCode(keyCode); const id = _keyCodeToKeyIdentifier[keyCode.toString()]; if (id) { return id; } let hex = keyCode.toString(16).toUpperCase(); const length = hex.length; for(let count = 0; count < 4 - length; count++){ hex = `0${hex}`; } return `U+${hex}`; } _handleKeyDown(event) { const code = event.keyCode || event.charCode; if (code === undefined) return; const id = this.toKeyIdentifier(code); this._keymap[id] = true; this.fire('keydown', makeKeyboardEvent(event)); if (this.preventDefault) { event.preventDefault(); } if (this.stopPropagation) { event.stopPropagation(); } } _handleKeyUp(event) { const code = event.keyCode || event.charCode; if (code === undefined) return; const id = this.toKeyIdentifier(code); delete this._keymap[id]; this.fire('keyup', makeKeyboardEvent(event)); if (this.preventDefault) { event.preventDefault(); } if (this.stopPropagation) { event.stopPropagation(); } } _handleKeyPress(event) { this.fire('keypress', makeKeyboardEvent(event)); if (this.preventDefault) { event.preventDefault(); } if (this.stopPropagation) { event.stopPropagation(); } } _handleVisibilityChange() { if (document.visibilityState === 'hidden') { this._handleWindowBlur(); } } _handleWindowBlur() { this._keymap = {}; this._lastmap = {}; } update() { for(const prop in this._lastmap){ delete this._lastmap[prop]; } for(const prop in this._keymap){ if (this._keymap.hasOwnProperty(prop)) { this._lastmap[prop] = this._keymap[prop]; } } } isPressed(key) { const keyCode = toKeyCode(key); const id = this.toKeyIdentifier(keyCode); return !!this._keymap[id]; } wasPressed(key) { const keyCode = toKeyCode(key); const id = this.toKeyIdentifier(keyCode); return !!this._keymap[id] && !!!this._lastmap[id]; } wasReleased(key) { const keyCode = toKeyCode(key); const id = this.toKeyIdentifier(keyCode); return !!!this._keymap[id] && !!this._lastmap[id]; } constructor(element, options = {}){ super(), this._element = null, this._keymap = {}, this._lastmap = {}; this._keyDownHandler = this._handleKeyDown.bind(this); this._keyUpHandler = this._handleKeyUp.bind(this); this._keyPressHandler = this._handleKeyPress.bind(this); this._visibilityChangeHandler = this._handleVisibilityChange.bind(this); this._windowBlurHandler = this._handleWindowBlur.bind(this); if (element) { this.attach(element); } this.preventDefault = options.preventDefault || false; this.stopPropagation = options.stopPropagation || false; } } Keyboard.EVENT_KEYDOWN = 'keydown'; Keyboard.EVENT_KEYUP = 'keyup'; function isMousePointerLocked() { return !!(document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement); } class MouseEvent { constructor(mouse, event){ this.x = 0; this.y = 0; this.dx = 0; this.dy = 0; this.button = MOUSEBUTTON_NONE; this.wheelDelta = 0; this.ctrlKey = false; this.altKey = false; this.shiftKey = false; this.metaKey = false; let coords = { x: 0, y: 0 }; if (event) { if (event instanceof MouseEvent) { throw Error('Expected MouseEvent'); } coords = mouse._getTargetCoords(event); } else { event = {}; } if (coords) { this.x = coords.x; this.y = coords.y; } else if (isMousePointerLocked()) { this.x = 0; this.y = 0; } else { return; } if (event.type === 'wheel') { if (event.deltaY > 0) { this.wheelDelta = 1; } else if (event.deltaY < 0) { this.wheelDelta = -1; } } if (isMousePointerLocked()) { this.dx = event.movementX || event.webkitMovementX || event.mozMovementX || 0; this.dy = event.movementY || event.webkitMovementY || event.mozMovementY || 0; } else { this.dx = this.x - mouse._lastX; this.dy = this.y - mouse._lastY; } if (event.type === 'mousedown' || event.type === 'mouseup') { this.button = event.button; } this.buttons = mouse._buttons.slice(0); this.element = event.target; this.ctrlKey = event.ctrlKey ?? false; this.altKey = event.altKey ?? false; this.shiftKey = event.shiftKey ?? false; this.metaKey = event.metaKey ?? false; this.event = event; } } class Mouse extends EventHandler { static isPointerLocked() { return isMousePointerLocked(); } attach(element) { this._target = element; if (this._attached) return; this._attached = true; const passiveOptions = { passive: false }; const options = platform.passiveEvents ? passiveOptions : false; window.addEventListener('mouseup', this._upHandler, options); window.addEventListener('mousedown', this._downHandler, options); window.addEventListener('mousemove', this._moveHandler, options); window.addEventListener('wheel', this._wheelHandler, options); } detach() { if (!this._attached) return; this._attached = false; this._target = null; const passiveOptions = { passive: false }; const options = platform.passiveEvents ? passiveOptions : false; window.removeEventListener('mouseup', this._upHandler, options); window.removeEventListener('mousedown', this._downHandler, options); window.removeEventListener('mousemove', this._moveHandler, options); window.removeEventListener('wheel', this._wheelHandler, options); } disableContextMenu() { if (!this._target) return; this._target.addEventListener('contextmenu', this._contextMenuHandler); } enableContextMenu() { if (!this._target) return; this._target.removeEventListener('contextmenu', this._contextMenuHandler); } enablePointerLock(success, error) { if (!document.body.requestPointerLock) { if (error) { error(); } return; } const s = ()=>{ success(); document.removeEventListener('pointerlockchange', s); }; const e = ()=>{ error(); document.removeEventListener('pointerlockerror', e); }; if (success) { document.addEventListener('pointerlockchange', s, false); } if (error) { document.addEventListener('pointerlockerror', e, false); } document.body.requestPointerLock(); } disablePointerLock(success) { if (!document.exitPointerLock) { return; } const s = ()=>{ success(); document.removeEventListener('pointerlockchange', s); }; if (success) { document.addEventListener('pointerlockchange', s, false); } document.exitPointerLock(); } update() { this._lastbuttons[0] = this._buttons[0]; this._lastbuttons[1] = this._buttons[1]; this._lastbuttons[2] = this._buttons[2]; } isPressed(button) { return this._buttons[button]; } wasPressed(button) { return this._buttons[button] && !this._lastbuttons[button]; } wasReleased(button) { return !this._buttons[button] && this._lastbuttons[button]; } _handleUp(event) { this._buttons[event.button] = false; const e = new MouseEvent(this, event); if (!e.event) return; this.fire('mouseup', e); } _handleDown(event) { this._buttons[event.button] = true; const e = new MouseEvent(this, event); if (!e.event) return; this.fire('mousedown', e); } _handleMove(event) { const e = new MouseEvent(this, event); if (!e.event) return; this.fire('mousemove', e); this._lastX = e.x; this._lastY = e.y; } _handleWheel(event) { const e = new MouseEvent(this, event); if (!e.event) return; this.fire('mousewheel', e); } _getTargetCoords(event) { const rect = this._target.getBoundingClientRect(); const left = Math.floor(rect.left); const top = Math.floor(rect.top); if (event.clientX < left || event.clientX >= left + this._target.clientWidth || event.clientY < top || event.clientY >= top + this._target.clientHeight) { return null; } return { x: event.clientX - left, y: event.clientY - top }; } constructor(element){ super(), this._lastX = 0, this._lastY = 0, this._buttons = [ false, false, false ], this._lastbuttons = [ false, false, false ], this._target = null, this._attached = false; this._upHandler = this._handleUp.bind(this); this._downHandler = this._handleDown.bind(this); this._moveHandler = this._handleMove.bind(this); this._wheelHandler = this._handleWheel.bind(this); this._contextMenuHandler = (event)=>{ event.preventDefault(); }; this.attach(element); } } Mouse.EVENT_MOUSEMOVE = 'mousemove'; Mouse.EVENT_MOUSEDOWN = 'mousedown'; Mouse.EVENT_MOUSEUP = 'mouseup'; Mouse.EVENT_MOUSEWHEEL = 'mousewheel'; class Http { get(url, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } const result = this.request('GET', url, options, callback); const { progress } = options; if (progress) { const handler = (event)=>{ if (event.lengthComputable) { progress.fire('progress', event.loaded, event.total); } }; const endHandler = (event)=>{ handler(event); result.removeEventListener('loadstart', handler); result.removeEventListener('progress', handler); result.removeEventListener('loadend', endHandler); }; result.addEventListener('loadstart', handler); result.addEventListener('progress', handler); result.addEventListener('loadend', endHandler); } return result; } post(url, data, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options.postdata = data; return this.request('POST', url, options, callback); } put(url, data, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options.postdata = data; return this.request('PUT', url, options, callback); } del(url, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } return this.request('DELETE', url, options, callback); } request(method, url, options, callback) { let uri, query, postdata; let errored = false; if (typeof options === 'function') { callback = options; options = {}; } if (options.retry) { options = Object.assign({ retries: 0, maxRetries: 5 }, options); } options.callback = callback; if (options.async == null) { options.async = true; } if (options.headers == null) { options.headers = {}; } if (options.postdata != null) { if (options.postdata instanceof Document) { postdata = options.postdata; } else if (options.postdata instanceof FormData) { postdata = options.postdata; } else if (options.postdata instanceof Object) { let contentType = options.headers['Content-Type']; if (contentType === undefined) { options.headers['Content-Type'] = Http.ContentType.FORM_URLENCODED; contentType = options.headers['Content-Type']; } switch(contentType){ case Http.ContentType.FORM_URLENCODED: { postdata = ''; let bFirstItem = true; for(const key in options.postdata){ if (options.postdata.hasOwnProperty(key)) { if (bFirstItem) { bFirstItem = false; } else { postdata += '&'; } const encodedKey = encodeURIComponent(key); const encodedValue = encodeURIComponent(options.postdata[key]); postdata += `${encodedKey}=${encodedValue}`; } } break; } default: case Http.ContentType.JSON: if (contentType == null) { options.headers['Content-Type'] = Http.ContentType.JSON; } postdata = JSON.stringify(options.postdata); break; } } else { postdata = options.postdata; } } if (options.cache === false) { const timestamp = now(); uri = new URI(url); if (!uri.query) { uri.query = `ts=${timestamp}`; } else { uri.query = `${uri.query}&ts=${timestamp}`; } url = uri.toString(); } if (options.query) { uri = new URI(url); query = extend(uri.getQuery(), options.query); uri.setQuery(query); url = uri.toString(); } const xhr = new XMLHttpRequest(); xhr.open(method, url, options.async); xhr.withCredentials = options.withCredentials !== undefined ? options.withCredentials : false; xhr.responseType = options.responseType || this._guessResponseType(url); for(const header in options.headers){ if (options.headers.hasOwnProperty(header)) { xhr.setRequestHeader(header, options.headers[header]); } } xhr.onreadystatechange = ()=>{ this._onReadyStateChange(method, url, options, xhr); }; xhr.onerror = ()=>{ this._onError(method, url, options, xhr); errored = true; }; try { xhr.send(postdata); } catch (e) { if (!errored) { options.error(xhr.status, xhr, e); } } return xhr; } _guessResponseType(url) { const uri = new URI(url); const ext = path.getExtension(uri.path).toLowerCase(); if (Http.binaryExtensions.indexOf(ext) >= 0) { return Http.ResponseType.ARRAY_BUFFER; } else if (ext === '.json') { return Http.ResponseType.JSON; } else if (ext === '.xml') { return Http.ResponseType.DOCUMENT; } return Http.ResponseType.TEXT; } _isBinaryContentType(contentType) { const binTypes = [ Http.ContentType.BASIS, Http.ContentType.BIN, Http.ContentType.DDS, Http.ContentType.GLB, Http.ContentType.MP3, Http.ContentType.MP4, Http.ContentType.OGG, Http.ContentType.OPUS, Http.ContentType.WAV ]; if (binTypes.indexOf(contentType) >= 0) { return true; } return false; } _isBinaryResponseType(responseType) { return responseType === Http.ResponseType.ARRAY_BUFFER || responseType === Http.ResponseType.BLOB || responseType === Http.ResponseType.JSON; } _onReadyStateChange(method, url, options, xhr) { if (xhr.readyState === 4) { switch(xhr.status){ case 0: { if (xhr.responseURL && xhr.responseURL.startsWith('file:///')) { this._onSuccess(method, url, options, xhr); } else { this._onError(method, url, options, xhr); } break; } case 200: case 201: case 206: case 304: { this._onSuccess(method, url, options, xhr); break; } default: { this._onError(method, url, options, xhr); break; } } } } _onSuccess(method, url, options, xhr) { let response; let contentType; const header = xhr.getResponseHeader('Content-Type'); if (header) { const parts = header.split(';'); contentType = parts[0].trim(); } try { if (this._isBinaryContentType(contentType) || this._isBinaryResponseType(xhr.responseType)) { response = xhr.response; } else if (contentType === Http.ContentType.JSON || url.split('?')[0].endsWith('.json')) { response = JSON.parse(xhr.responseText); } else if (xhr.responseType === Http.ResponseType.DOCUMENT || contentType === Http.ContentType.XML) { response = xhr.responseXML; } else { response = xhr.responseText; } options.callback(null, response); } catch (err) { options.callback(err); } } _onError(method, url, options, xhr) { if (options.retrying) { return; } if (options.retry && options.retries < options.maxRetries) { options.retries++; options.retrying = true; const retryDelay = math.clamp(Math.pow(2, options.retries) * Http.retryDelay, 0, options.maxRetryDelay || 5000); console.log(`${method}: ${url} - Error ${xhr.status}. Retrying in ${retryDelay} ms`); setTimeout(()=>{ options.retrying = false; this.request(method, url, options, options.callback); }, retryDelay); } else { options.callback(xhr.status === 0 ? 'Network error' : xhr.status, null); } } } Http.ContentType = { AAC: 'audio/aac', BASIS: 'image/basis', BIN: 'application/octet-stream', DDS: 'image/dds', FORM_URLENCODED: 'application/x-www-form-urlencoded', GIF: 'image/gif', GLB: 'model/gltf-binary', JPEG: 'image/jpeg', JSON: 'application/json', MP3: 'audio/mpeg', MP4: 'audio/mp4', OGG: 'audio/ogg', OPUS: 'audio/ogg; codecs="opus"', PNG: 'image/png', TEXT: 'text/plain', WAV: 'audio/x-wav', XML: 'application/xml' }; Http.ResponseType = { TEXT: 'text', ARRAY_BUFFER: 'arraybuffer', BLOB: 'blob', DOCUMENT: 'document', JSON: 'json' }; Http.binaryExtensions = [ '.model', '.wav', '.ogg', '.mp3', '.mp4', '.m4a', '.aac', '.dds', '.basis', '.glb', '.opus' ]; Http.retryDelay = 100; const http = new Http(); const DISTANCE_LINEAR = 'linear'; const DISTANCE_INVERSE = 'inverse'; const DISTANCE_EXPONENTIAL = 'exponential'; class Listener { getPosition() { return this.position; } setPosition(position) { this.position.copy(position); const listener = this.listener; if (listener) { if ('positionX' in listener) { listener.positionX.value = position.x; listener.positionY.value = position.y; listener.positionZ.value = position.z; } else if (listener.setPosition) { listener.setPosition(position.x, position.y, position.z); } } } setOrientation(orientation) { this.orientation.copy(orientation); const listener = this.listener; if (listener) { const m = orientation.data; if ('forwardX' in listener) { listener.forwardX.value = -m[8]; listener.forwardY.value = -m[9]; listener.forwardZ.value = -m[10]; listener.upX.value = m[4]; listener.upY.value = m[5]; listener.upZ.value = m[6]; } else if (listener.setOrientation) { listener.setOrientation(-m[8], -m[9], -m[10], m[4], m[5], m[6]); } } } getOrientation() { return this.orientation; } get listener() { const context = this._manager.context; return context ? context.listener : null; } constructor(manager){ this.position = new Vec3(); this.orientation = new Mat4(); this._manager = manager; } } const CONTEXT_STATE_RUNNING = 'running'; const USER_INPUT_EVENTS = [ 'click', 'touchstart', 'mousedown' ]; class SoundManager extends EventHandler { set volume(volume) { volume = math.clamp(volume, 0, 1); this._volume = volume; this.fire('volumechange', volume); } get volume() { return this._volume; } get suspended() { return this._userSuspended; } get context() { if (!this._context && this.AudioContext) { this._context = new this.AudioContext(); if (this._context.state !== CONTEXT_STATE_RUNNING) { this._registerUnlockListeners(); } } return this._context; } suspend() { if (!this._userSuspended) { this._userSuspended = true; if (this._context && this._context.state === CONTEXT_STATE_RUNNING) { this._suspend(); } } } resume() { if (this._userSuspended) { this._userSuspended = false; if (this._context && this._context.state !== CONTEXT_STATE_RUNNING) { this._resume(); } } } destroy() { this.fire('destroy'); if (this._context) { this._removeUnlockListeners(); this._context?.close(); this._context = null; } } _resume() { this._context.resume().then(()=>{ const source = this._context.createBufferSource(); source.buffer = this._context.createBuffer(1, 1, this._context.sampleRate); source.connect(this._context.destination); source.start(0); source.onended = (event)=>{ source.disconnect(0); this.fire('resume'); }; }, (e)=>{}).catch((e)=>{}); } _suspend() { this._context.suspend().then(()=>{ this.fire('suspend'); }, (e)=>{}).catch((e)=>{}); } _unlockHandler() { this._removeUnlockListeners(); if (!this._userSuspended && this._context.state !== CONTEXT_STATE_RUNNING) { this._resume(); } } _registerUnlockListeners() { USER_INPUT_EVENTS.forEach((eventName)=>{ window.addEventListener(eventName, this._unlockHandlerFunc, false); }); } _removeUnlockListeners() { USER_INPUT_EVENTS.forEach((eventName)=>{ window.removeEventListener(eventName, this._unlockHandlerFunc, false); }); } constructor(){ super(); this._context = null; this.AudioContext = typeof AudioContext !== 'undefined' && AudioContext || typeof webkitAudioContext !== 'undefined' && webkitAudioContext; if (!this.AudioContext) ; this._unlockHandlerFunc = this._unlockHandler.bind(this); this._userSuspended = false; this.listener = new Listener(this); this._volume = 1; } } class Sound { get duration() { let duration = 0; if (this.buffer) { duration = this.buffer.duration; } else if (this.audio) { duration = this.audio.duration; } return duration || 0; } constructor(resource){ if (resource instanceof Audio) { this.audio = resource; } else { this.buffer = resource; } } } function hasAudioContext() { return !!(typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined'); } const STATE_PLAYING = 0; const STATE_PAUSED = 1; const STATE_STOPPED = 2; function capTime(time, duration) { return time % duration || 0; } class SoundInstance extends EventHandler { set currentTime(value) { if (value < 0) return; if (this._state === STATE_PLAYING) { const suspend = this._suspendInstanceEvents; this._suspendInstanceEvents = true; this.stop(); this._startOffset = value; this.play(); this._suspendInstanceEvents = suspend; } else { this._startOffset = value; this._currentTime = value; } } get currentTime() { if (this._startOffset !== null) { return this._startOffset; } if (this._state === STATE_PAUSED) { return this._currentTime; } if (this._state === STATE_STOPPED || !this.source) { return 0; } this._updateCurrentTime(); return this._currentTime; } set duration(value) { this._duration = Math.max(0, Number(value) || 0); const isPlaying = this._state === STATE_PLAYING; this.stop(); if (isPlaying) { this.play(); } } get duration() { if (!this._sound) { return 0; } if (this._duration) { return capTime(this._duration, this._sound.duration); } return this._sound.duration; } get isPaused() { return this._state === STATE_PAUSED; } get isPlaying() { return this._state === STATE_PLAYING; } get isStopped() { return this._state === STATE_STOPPED; } get isSuspended() { return this._suspended; } set loop(value) { this._loop = !!value; if (this.source) { this.source.loop = this._loop; } } get loop() { return this._loop; } set pitch(pitch) { this._currentOffset = this.currentTime; this._startedAt = this._manager.context.currentTime; this._pitch = Math.max(Number(pitch) || 0, 0.01); if (this.source) { this.source.playbackRate.value = this._pitch; } } get pitch() { return this._pitch; } set sound(value) { this._sound = value; if (this._state !== STATE_STOPPED) { this.stop(); } else { this._createSource(); } } get sound() { return this._sound; } set startTime(value) { this._startTime = Math.max(0, Number(value) || 0); const isPlaying = this._state === STATE_PLAYING; this.stop(); if (isPlaying) { this.play(); } } get startTime() { return this._startTime; } set volume(volume) { volume = math.clamp(volume, 0, 1); this._volume = volume; if (this.gain) { this.gain.gain.value = volume * this._manager.volume; } } get volume() { return this._volume; } _onPlay() { this.fire('play'); if (this._onPlayCallback) { this._onPlayCallback(this); } } _onPause() { this.fire('pause'); if (this._onPauseCallback) { this._onPauseCallback(this); } } _onResume() { this.fire('resume'); if (this._onResumeCallback) { this._onResumeCallback(this); } } _onStop() { this.fire('stop'); if (this._onStopCallback) { this._onStopCallback(this); } } _onEnded() { if (this._suspendEndEvent > 0) { this._suspendEndEvent--; return; } this.fire('end'); if (this._onEndCallback) { this._onEndCallback(this); } this.stop(); } _onManagerVolumeChange() { this.volume = this._volume; } _onManagerSuspend() { if (this._state === STATE_PLAYING && !this._suspended) { this._suspended = true; this.pause(); } } _onManagerResume() { if (this._suspended) { this._suspended = false; this.resume(); } } _initializeNodes() { this.gain = this._manager.context.createGain(); this._inputNode = this.gain; this._connectorNode = this.gain; this._connectorNode.connect(this._manager.context.destination); } play() { if (this._state !== STATE_STOPPED) { this.stop(); } this._state = STATE_PLAYING; this._playWhenLoaded = false; if (this._waitingContextSuspension) { return false; } if (this._manager.suspended) { this._manager.once('resume', this._playAudioImmediate, this); this._waitingContextSuspension = true; return false; } this._playAudioImmediate(); return true; } _playAudioImmediate() { this._waitingContextSuspension = false; if (this._state !== STATE_PLAYING) { return; } if (!this.source) { this._createSource(); } let offset = capTime(this._startOffset, this.duration); offset = capTime(this._startTime + offset, this._sound.duration); this._startOffset = null; if (this._duration) { this.source.start(0, offset, this._duration); } else { this.source.start(0, offset); } this._startedAt = this._manager.context.currentTime; this._currentTime = 0; this._currentOffset = offset; this.volume = this._volume; this.loop = this._loop; this.pitch = this._pitch; this._manager.on('volumechange', this._onManagerVolumeChange, this); this._manager.on('suspend', this._onManagerSuspend, this); this._manager.on('resume', this._onManagerResume, this); this._manager.on('destroy', this._onManagerDestroy, this); if (!this._suspendInstanceEvents) { this._onPlay(); } } pause() { this._playWhenLoaded = false; if (this._state !== STATE_PLAYING) { return false; } this._state = STATE_PAUSED; if (this._waitingContextSuspension) { return true; } this._updateCurrentTime(); this._suspendEndEvent++; this.source.stop(0); this.source = null; this._startOffset = null; if (!this._suspendInstanceEvents) { this._onPause(); } return true; } resume() { if (this._state !== STATE_PAUSED) { return false; } let offset = this.currentTime; this._state = STATE_PLAYING; if (this._waitingContextSuspension) { return true; } if (!this.source) { this._createSource(); } if (this._startOffset !== null) { offset = capTime(this._startOffset, this.duration); offset = capTime(this._startTime + offset, this._sound.duration); this._startOffset = null; } if (this._duration) { this.source.start(0, offset, this._duration); } else { this.source.start(0, offset); } this._startedAt = this._manager.context.currentTime; this._currentOffset = offset; this.volume = this._volume; this.loop = this._loop; this.pitch = this._pitch; this._playWhenLoaded = false; if (!this._suspendInstanceEvents) { this._onResume(); } return true; } stop() { this._playWhenLoaded = false; if (this._state === STATE_STOPPED) { return false; } const wasPlaying = this._state === STATE_PLAYING; this._state = STATE_STOPPED; if (this._waitingContextSuspension) { return true; } this._manager.off('volumechange', this._onManagerVolumeChange, this); this._manager.off('suspend', this._onManagerSuspend, this); this._manager.off('resume', this._onManagerResume, this); this._manager.off('destroy', this._onManagerDestroy, this); this._startedAt = 0; this._currentTime = 0; this._currentOffset = 0; this._startOffset = null; this._suspendEndEvent++; if (wasPlaying && this.source) { this.source.stop(0); } this.source = null; if (!this._suspendInstanceEvents) { this._onStop(); } return true; } setExternalNodes(firstNode, lastNode) { if (!firstNode) { console.error('The firstNode must be a valid Audio Node'); return; } if (!lastNode) { lastNode = firstNode; } const speakers = this._manager.context.destination; if (this._firstNode !== firstNode) { if (this._firstNode) { this._connectorNode.disconnect(this._firstNode); } else { this._connectorNode.disconnect(speakers); } this._firstNode = firstNode; this._connectorNode.connect(firstNode); } if (this._lastNode !== lastNode) { if (this._lastNode) { this._lastNode.disconnect(speakers); } this._lastNode = lastNode; this._lastNode.connect(speakers); } } clearExternalNodes() { const speakers = this._manager.context.destination; if (this._firstNode) { this._connectorNode.disconnect(this._firstNode); this._firstNode = null; } if (this._lastNode) { this._lastNode.disconnect(speakers); this._lastNode = null; } this._connectorNode.connect(speakers); } getExternalNodes() { return [ this._firstNode, this._lastNode ]; } _createSource() { if (!this._sound) { return null; } const context = this._manager.context; if (this._sound.buffer) { this.source = context.createBufferSource(); this.source.buffer = this._sound.buffer; this.source.connect(this._inputNode); this.source.onended = this._endedHandler; this.source.loopStart = capTime(this._startTime, this.source.buffer.duration); if (this._duration) { this.source.loopEnd = Math.max(this.source.loopStart, capTime(this._startTime + this._duration, this.source.buffer.duration)); } } return this.source; } _updateCurrentTime() { this._currentTime = capTime((this._manager.context.currentTime - this._startedAt) * this._pitch + this._currentOffset, this.duration); } _onManagerDestroy() { if (this.source && this._state === STATE_PLAYING) { this.source.stop(0); this.source = null; } } constructor(manager, sound, options){ super(), this.source = null; this._manager = manager; this._volume = options.volume !== undefined ? math.clamp(Number(options.volume) || 0, 0, 1) : 1; this._pitch = options.pitch !== undefined ? Math.max(0.01, Number(options.pitch) || 0) : 1; this._loop = !!(options.loop !== undefined ? options.loop : false); this._sound = sound; this._state = STATE_STOPPED; this._suspended = false; this._suspendEndEvent = 0; this._suspendInstanceEvents = false; this._playWhenLoaded = true; this._startTime = Math.max(0, Number(options.startTime) || 0); this._duration = Math.max(0, Number(options.duration) || 0); this._startOffset = null; this._onPlayCallback = options.onPlay; this._onPauseCallback = options.onPause; this._onResumeCallback = options.onResume; this._onStopCallback = options.onStop; this._onEndCallback = options.onEnd; if (hasAudioContext()) { this._startedAt = 0; this._currentTime = 0; this._currentOffset = 0; this._inputNode = null; this._connectorNode = null; this._firstNode = null; this._lastNode = null; this._waitingContextSuspension = false; this._initializeNodes(); this._endedHandler = this._onEnded.bind(this); } else { this._isReady = false; this._loadedMetadataHandler = this._onLoadedMetadata.bind(this); this._timeUpdateHandler = this._onTimeUpdate.bind(this); this._endedHandler = this._onEnded.bind(this); this._createSource(); } } } SoundInstance.EVENT_PLAY = 'play'; SoundInstance.EVENT_PAUSE = 'pause'; SoundInstance.EVENT_RESUME = 'resume'; SoundInstance.EVENT_STOP = 'stop'; SoundInstance.EVENT_END = 'end'; if (!hasAudioContext()) { Object.assign(SoundInstance.prototype, { play: function() { if (this._state !== STATE_STOPPED) { this.stop(); } if (!this.source) { if (!this._createSource()) { return false; } } this.volume = this._volume; this.pitch = this._pitch; this.loop = this._loop; this.source.play(); this._state = STATE_PLAYING; this._playWhenLoaded = false; this._manager.on('volumechange', this._onManagerVolumeChange, this); this._manager.on('suspend', this._onManagerSuspend, this); this._manager.on('resume', this._onManagerResume, this); this._manager.on('destroy', this._onManagerDestroy, this); if (this._manager.suspended) { this._onManagerSuspend(); } if (!this._suspendInstanceEvents) { this._onPlay(); } return true; }, pause: function() { if (!this.source || this._state !== STATE_PLAYING) { return false; } this._suspendEndEvent++; this.source.pause(); this._playWhenLoaded = false; this._state = STATE_PAUSED; this._startOffset = null; if (!this._suspendInstanceEvents) { this._onPause(); } return true; }, resume: function() { if (!this.source || this._state !== STATE_PAUSED) { return false; } this._state = STATE_PLAYING; this._playWhenLoaded = false; if (this.source.paused) { this.source.play(); if (!this._suspendInstanceEvents) { this._onResume(); } } return true; }, stop: function() { if (!this.source || this._state === STATE_STOPPED) { return false; } this._manager.off('volumechange', this._onManagerVolumeChange, this); this._manager.off('suspend', this._onManagerSuspend, this); this._manager.off('resume', this._onManagerResume, this); this._manager.off('destroy', this._onManagerDestroy, this); this._suspendEndEvent++; this.source.pause(); this._playWhenLoaded = false; this._state = STATE_STOPPED; this._startOffset = null; if (!this._suspendInstanceEvents) { this._onStop(); } return true; }, setExternalNodes: function() {}, clearExternalNodes: function() {}, getExternalNodes: function() { return [ null, null ]; }, _onLoadedMetadata: function() { this.source.removeEventListener('loadedmetadata', this._loadedMetadataHandler); this._isReady = true; let offset = capTime(this._startOffset, this.duration); offset = capTime(this._startTime + offset, this._sound.duration); this._startOffset = null; this.source.currentTime = offset; }, _createSource: function() { if (this._sound && this._sound.audio) { this._isReady = false; this.source = this._sound.audio.cloneNode(true); this.source.addEventListener('loadedmetadata', this._loadedMetadataHandler); this.source.addEventListener('timeupdate', this._timeUpdateHandler); this.source.onended = this._endedHandler; } return this.source; }, _onTimeUpdate: function() { if (!this._duration) { return; } if (this.source.currentTime > capTime(this._startTime + this._duration, this.source.duration)) { if (this.loop) { this.source.currentTime = capTime(this._startTime, this.source.duration); } else { this.source.removeEventListener('timeupdate', this._timeUpdateHandler); this.source.pause(); this._onEnded(); } } }, _onManagerDestroy: function() { if (this.source) { this.source.pause(); } } }); Object.defineProperty(SoundInstance.prototype, 'volume', { get: function() { return this._volume; }, set: function(volume) { volume = math.clamp(volume, 0, 1); this._volume = volume; if (this.source) { this.source.volume = volume * this._manager.volume; } } }); Object.defineProperty(SoundInstance.prototype, 'pitch', { get: function() { return this._pitch; }, set: function(pitch) { this._pitch = Math.max(Number(pitch) || 0, 0.01); if (this.source) { this.source.playbackRate = this._pitch; } } }); Object.defineProperty(SoundInstance.prototype, 'sound', { get: function() { return this._sound; }, set: function(value) { this.stop(); this._sound = value; } }); Object.defineProperty(SoundInstance.prototype, 'currentTime', { get: function() { if (this._startOffset !== null) { return this._startOffset; } if (this._state === STATE_STOPPED || !this.source) { return 0; } return this.source.currentTime - this._startTime; }, set: function(value) { if (value < 0) return; this._startOffset = value; if (this.source && this._isReady) { this.source.currentTime = capTime(this._startTime + capTime(value, this.duration), this._sound.duration); this._startOffset = null; } } }); } const MAX_DISTANCE = 10000; class SoundInstance3d extends SoundInstance { _initializeNodes() { this.gain = this._manager.context.createGain(); this.panner = this._manager.context.createPanner(); this.panner.connect(this.gain); this._inputNode = this.panner; this._connectorNode = this.gain; this._connectorNode.connect(this._manager.context.destination); } set position(value) { this._position.copy(value); const panner = this.panner; if ('positionX' in panner) { panner.positionX.value = value.x; panner.positionY.value = value.y; panner.positionZ.value = value.z; } else if (panner.setPosition) { panner.setPosition(value.x, value.y, value.z); } } get position() { return this._position; } set velocity(velocity) { this._velocity.copy(velocity); } get velocity() { return this._velocity; } set maxDistance(value) { this.panner.maxDistance = value; } get maxDistance() { return this.panner.maxDistance; } set refDistance(value) { this.panner.refDistance = value; } get refDistance() { return this.panner.refDistance; } set rollOffFactor(value) { this.panner.rolloffFactor = value; } get rollOffFactor() { return this.panner.rolloffFactor; } set distanceModel(value) { this.panner.distanceModel = value; } get distanceModel() { return this.panner.distanceModel; } constructor(manager, sound, options = {}){ super(manager, sound, options), this._position = new Vec3(), this._velocity = new Vec3(); if (options.position) { this.position = options.position; } this.maxDistance = options.maxDistance !== undefined ? Number(options.maxDistance) : MAX_DISTANCE; this.refDistance = options.refDistance !== undefined ? Number(options.refDistance) : 1; this.rollOffFactor = options.rollOffFactor !== undefined ? Number(options.rollOffFactor) : 1; this.distanceModel = options.distanceModel !== undefined ? options.distanceModel : DISTANCE_LINEAR; } } if (!hasAudioContext()) { let offset = new Vec3(); const fallOff = function(posOne, posTwo, refDistance, maxDistance, rollOffFactor, distanceModel) { offset = offset.sub2(posOne, posTwo); const distance = offset.length(); if (distance < refDistance) { return 1; } else if (distance > maxDistance) { return 0; } let result = 0; if (distanceModel === DISTANCE_LINEAR) { result = 1 - rollOffFactor * (distance - refDistance) / (maxDistance - refDistance); } else if (distanceModel === DISTANCE_INVERSE) { result = refDistance / (refDistance + rollOffFactor * (distance - refDistance)); } else if (distanceModel === DISTANCE_EXPONENTIAL) { result = Math.pow(distance / refDistance, -rollOffFactor); } return math.clamp(result, 0, 1); }; Object.defineProperty(SoundInstance3d.prototype, 'position', { get: function() { return this._position; }, set: function(position) { this._position.copy(position); if (this.source) { const listener = this._manager.listener; const lpos = listener.getPosition(); const factor = fallOff(lpos, this._position, this.refDistance, this.maxDistance, this.rollOffFactor, this.distanceModel); const v = this.volume; this.source.volume = v * factor * this._manager.volume; } } }); Object.defineProperty(SoundInstance3d.prototype, 'maxDistance', { get: function() { return this._maxDistance; }, set: function(value) { this._maxDistance = value; } }); Object.defineProperty(SoundInstance3d.prototype, 'refDistance', { get: function() { return this._refDistance; }, set: function(value) { this._refDistance = value; } }); Object.defineProperty(SoundInstance3d.prototype, 'rollOffFactor', { get: function() { return this._rollOffFactor; }, set: function(value) { this._rollOffFactor = value; } }); Object.defineProperty(SoundInstance3d.prototype, 'distanceModel', { get: function() { return this._distanceModel; }, set: function(value) { this._distanceModel = value; } }); } const BLEND_SUBTRACTIVE = 0; const BLEND_ADDITIVE = 1; const BLEND_NORMAL = 2; const BLEND_NONE = 3; const BLEND_PREMULTIPLIED = 4; const BLEND_MULTIPLICATIVE = 5; const BLEND_ADDITIVEALPHA = 6; const BLEND_MULTIPLICATIVE2X = 7; const BLEND_SCREEN = 8; const BLEND_MIN = 9; const BLEND_MAX = 10; const blendNames = { [BLEND_SUBTRACTIVE]: 'SUBTRACTIVE', [BLEND_ADDITIVE]: 'ADDITIVE', [BLEND_NORMAL]: 'NORMAL', [BLEND_NONE]: 'NONE', [BLEND_PREMULTIPLIED]: 'PREMULTIPLIED', [BLEND_MULTIPLICATIVE]: 'MULTIPLICATIVE', [BLEND_ADDITIVEALPHA]: 'ADDITIVEALPHA', [BLEND_MULTIPLICATIVE2X]: 'MULTIPLICATIVE2X', [BLEND_SCREEN]: 'SCREEN', [BLEND_MIN]: 'MIN', [BLEND_MAX]: 'MAX' }; const FOG_NONE = 'none'; const FOG_LINEAR = 'linear'; const FRESNEL_NONE = 0; const FRESNEL_SCHLICK = 2; const fresnelNames = { [FRESNEL_NONE]: 'NONE', [FRESNEL_SCHLICK]: 'SCHLICK' }; const LAYER_HUD = 0; const LAYER_WORLD = 15; const LAYERID_WORLD = 0; const LAYERID_DEPTH = 1; const LAYERID_SKYBOX = 2; const LAYERID_IMMEDIATE = 3; const LAYERID_UI = 4; const LIGHTTYPE_DIRECTIONAL = 0; const LIGHTTYPE_OMNI = 1; const LIGHTTYPE_SPOT = 2; const lightTypeNames = { [LIGHTTYPE_DIRECTIONAL]: 'DIRECTIONAL', [LIGHTTYPE_OMNI]: 'OMNI', [LIGHTTYPE_SPOT]: 'SPOT' }; const LIGHT_COLOR_DIVIDER = 100; const LIGHTSHAPE_PUNCTUAL = 0; const LIGHTSHAPE_RECT = 1; const LIGHTSHAPE_DISK = 2; const LIGHTSHAPE_SPHERE = 3; const lightShapeNames = { [LIGHTSHAPE_PUNCTUAL]: 'PUNCTUAL', [LIGHTSHAPE_RECT]: 'RECT', [LIGHTSHAPE_DISK]: 'DISK', [LIGHTSHAPE_SPHERE]: 'SPHERE' }; const LIGHTFALLOFF_LINEAR = 0; const LIGHTFALLOFF_INVERSESQUARED = 1; const lightFalloffNames = { [LIGHTFALLOFF_LINEAR]: 'LINEAR', [LIGHTFALLOFF_INVERSESQUARED]: 'INVERSESQUARED' }; const SHADOW_PCF3_32F = 0; const SHADOW_VSM_16F = 2; const SHADOW_VSM_32F = 3; const SHADOW_PCF5_32F = 4; const SHADOW_PCF1_32F = 5; const SHADOW_PCSS_32F = 6; const SHADOW_PCF1_16F = 7; const SHADOW_PCF3_16F = 8; const SHADOW_PCF5_16F = 9; const shadowTypeInfo = new Map([ [ SHADOW_PCF1_32F, { name: 'PCF1_32F', kind: 'PCF1', format: PIXELFORMAT_DEPTH, pcf: true } ], [ SHADOW_PCF3_32F, { name: 'PCF3_32F', kind: 'PCF3', format: PIXELFORMAT_DEPTH, pcf: true } ], [ SHADOW_PCF5_32F, { name: 'PCF5_32F', kind: 'PCF5', format: PIXELFORMAT_DEPTH, pcf: true } ], [ SHADOW_PCF1_16F, { name: 'PCF1_16F', kind: 'PCF1', format: PIXELFORMAT_DEPTH16, pcf: true } ], [ SHADOW_PCF3_16F, { name: 'PCF3_16F', kind: 'PCF3', format: PIXELFORMAT_DEPTH16, pcf: true } ], [ SHADOW_PCF5_16F, { name: 'PCF5_16F', kind: 'PCF5', format: PIXELFORMAT_DEPTH16, pcf: true } ], [ SHADOW_VSM_16F, { name: 'VSM_16F', kind: 'VSM', format: PIXELFORMAT_RGBA16F, vsm: true } ], [ SHADOW_VSM_32F, { name: 'VSM_32F', kind: 'VSM', format: PIXELFORMAT_RGBA32F, vsm: true } ], [ SHADOW_PCSS_32F, { name: 'PCSS_32F', kind: 'PCSS', format: PIXELFORMAT_R32F, pcss: true } ] ]); const SHADOW_CASCADE_ALL = 255; const BLUR_GAUSSIAN = 1; const PARTICLESORT_NONE = 0; const PARTICLEMODE_GPU = 0; const EMITTERSHAPE_BOX = 0; const EMITTERSHAPE_SPHERE = 1; const PARTICLEORIENTATION_SCREEN = 0; const PARTICLEORIENTATION_WORLD = 1; const PROJECTION_PERSPECTIVE = 0; const PROJECTION_ORTHOGRAPHIC = 1; const RENDERSTYLE_SOLID = 0; const RENDERSTYLE_WIREFRAME = 1; const RENDERSTYLE_POINTS = 2; const CUBEPROJ_NONE = 0; const CUBEPROJ_BOX = 1; const cubemaProjectionNames = { [CUBEPROJ_NONE]: 'NONE', [CUBEPROJ_BOX]: 'BOX' }; const DETAILMODE_MUL = 'mul'; const GAMMA_NONE = 0; const GAMMA_SRGB = 1; const gammaNames = { [GAMMA_NONE]: 'NONE', [GAMMA_SRGB]: 'SRGB' }; const TONEMAP_LINEAR = 0; const TONEMAP_FILMIC = 1; const TONEMAP_HEJL = 2; const TONEMAP_ACES = 3; const TONEMAP_ACES2 = 4; const TONEMAP_NEUTRAL = 5; const TONEMAP_NONE = 6; const tonemapNames = [ 'LINEAR', 'FILMIC', 'HEJL', 'ACES', 'ACES2', 'NEUTRAL', 'NONE' ]; const SPECOCC_NONE = 0; const SPECOCC_AO = 1; const SPECOCC_GLOSSDEPENDENT = 2; const specularOcclusionNames = { [SPECOCC_NONE]: 'NONE', [SPECOCC_AO]: 'AO', [SPECOCC_GLOSSDEPENDENT]: 'GLOSSDEPENDENT' }; const REFLECTIONSRC_NONE = 'none'; const REFLECTIONSRC_ENVATLAS = 'envAtlas'; const REFLECTIONSRC_ENVATLASHQ = 'envAtlasHQ'; const REFLECTIONSRC_CUBEMAP = 'cubeMap'; const REFLECTIONSRC_SPHEREMAP = 'sphereMap'; const reflectionSrcNames = { [REFLECTIONSRC_NONE]: 'NONE', [REFLECTIONSRC_ENVATLAS]: 'ENVATLAS', [REFLECTIONSRC_ENVATLASHQ]: 'ENVATLASHQ', [REFLECTIONSRC_CUBEMAP]: 'CUBEMAP', [REFLECTIONSRC_SPHEREMAP]: 'SPHEREMAP' }; const AMBIENTSRC_AMBIENTSH = 'ambientSH'; const AMBIENTSRC_ENVALATLAS = 'envAtlas'; const AMBIENTSRC_CONSTANT = 'constant'; const ambientSrcNames = { [AMBIENTSRC_AMBIENTSH]: 'AMBIENTSH', [AMBIENTSRC_ENVALATLAS]: 'ENVALATLAS', [AMBIENTSRC_CONSTANT]: 'CONSTANT' }; const SHADERDEF_NOSHADOW = 1; const SHADERDEF_SKIN = 2; const SHADERDEF_UV0 = 4; const SHADERDEF_UV1 = 8; const SHADERDEF_VCOLOR = 16; const SHADERDEF_INSTANCING = 32; const SHADERDEF_LM = 64; const SHADERDEF_DIRLM = 128; const SHADERDEF_SCREENSPACE = 256; const SHADERDEF_TANGENTS = 512; const SHADERDEF_MORPH_POSITION = 1024; const SHADERDEF_MORPH_NORMAL = 2048; const SHADERDEF_LMAMBIENT = 4096; const SHADERDEF_MORPH_TEXTURE_BASED_INT = 8192; const SHADERDEF_BATCH = 16384; const SHADOWUPDATE_NONE = 0; const SHADOWUPDATE_THISFRAME = 1; const SHADOWUPDATE_REALTIME = 2; const MASK_AFFECT_DYNAMIC = 1; const MASK_AFFECT_LIGHTMAPPED = 2; const MASK_BAKE = 4; const SHADER_FORWARD = 0; const SHADER_PREPASS = 1; const SHADER_SHADOW = 2; const SHADER_PICK = 3; const SHADER_DEPTH_PICK = 4; const SPRITE_RENDERMODE_SIMPLE = 0; const SPRITE_RENDERMODE_SLICED = 1; const SPRITE_RENDERMODE_TILED = 2; const spriteRenderModeNames = { [SPRITE_RENDERMODE_SIMPLE]: 'SIMPLE', [SPRITE_RENDERMODE_SLICED]: 'SLICED', [SPRITE_RENDERMODE_TILED]: 'TILED' }; const BAKE_COLORDIR = 1; const VIEW_CENTER = 0; const SORTMODE_NONE = 0; const SORTMODE_MANUAL = 1; const SORTMODE_MATERIALMESH = 2; const SORTMODE_BACK2FRONT = 3; const SORTMODE_FRONT2BACK = 4; const SORTMODE_CUSTOM = 5; const ASPECT_AUTO = 0; const ASPECT_MANUAL = 1; const ORIENTATION_HORIZONTAL = 0; const ORIENTATION_VERTICAL = 1; const SKYTYPE_INFINITE = 'infinite'; const SKYTYPE_BOX = 'box'; const SKYTYPE_DOME = 'dome'; const DITHER_NONE = 'none'; const DITHER_BAYER8 = 'bayer8'; const DITHER_BLUENOISE = 'bluenoise'; const DITHER_IGNNOISE = 'ignnoise'; const ditherNames = { [DITHER_NONE]: 'NONE', [DITHER_BAYER8]: 'BAYER8', [DITHER_BLUENOISE]: 'BLUENOISE', [DITHER_IGNNOISE]: 'IGNNOISE' }; const EVENT_PRERENDER = 'prerender'; const EVENT_POSTRENDER = 'postrender'; const EVENT_PRERENDER_LAYER = 'prerender:layer'; const EVENT_POSTRENDER_LAYER = 'postrender:layer'; const EVENT_PRECULL = 'precull'; const EVENT_POSTCULL = 'postcull'; const EVENT_CULL_END = 'cull:end'; const GSPLAT_FORWARD = 1; const GSPLAT_SHADOW = 2; const SHADOWCAMERA_NAME = 'pcShadowCamera'; class ShaderProcessorOptions { hasUniform(name) { for(let i = 0; i < this.uniformFormats.length; i++){ const uniformFormat = this.uniformFormats[i]; if (uniformFormat?.get(name)) { return true; } } return false; } hasTexture(name) { for(let i = 0; i < this.bindGroupFormats.length; i++){ const groupFormat = this.bindGroupFormats[i]; if (groupFormat?.getTexture(name)) { return true; } } return false; } getVertexElement(semantic) { return this.vertexFormat?.elements.find((element)=>element.name === semantic); } generateKey(device) { let key = JSON.stringify(this.uniformFormats) + JSON.stringify(this.bindGroupFormats); if (device.isWebGPU) { key += this.vertexFormat?.shaderProcessingHashString; } return key; } constructor(viewUniformFormat, viewBindGroupFormat, vertexFormat){ this.uniformFormats = []; this.bindGroupFormats = []; this.uniformFormats[BINDGROUP_VIEW] = viewUniformFormat; this.bindGroupFormats[BINDGROUP_VIEW] = viewBindGroupFormat; this.vertexFormat = vertexFormat; } } const programLibraryDeviceCache = new DeviceCache(); function getProgramLibrary(device) { const library = programLibraryDeviceCache.get(device); return library; } function setProgramLibrary(device, library) { programLibraryDeviceCache.get(device, ()=>{ return library; }); } class ShaderGenerator { static definesHash(defines) { const sortedArray = Array.from(defines).sort((a, b)=>a[0] > b[0] ? 1 : -1); return hashCode(JSON.stringify(sortedArray)); } } const shaderPassDeviceCache = new DeviceCache(); class ShaderPassInfo { buildShaderDefines() { let keyword; if (this.isShadow) { keyword = 'SHADOW'; } else if (this.isForward) { keyword = 'FORWARD'; } else if (this.index === SHADER_PICK) { keyword = 'PICK'; } else if (this.index === SHADER_DEPTH_PICK) { keyword = 'PICK'; this.defines.set('DEPTH_PICK_PASS', ''); } this.defines.set(`${keyword}_PASS`, ''); this.defines.set(`${this.name.toUpperCase()}_PASS`, ''); } constructor(name, index, options = {}){ this.defines = new Map(); this.name = name; this.index = index; Object.assign(this, options); this.buildShaderDefines(); } } class ShaderPass { static get(device) { return shaderPassDeviceCache.get(device, ()=>{ return new ShaderPass(); }); } allocate(name, options) { let info = this.passesNamed.get(name); if (info === undefined) { info = new ShaderPassInfo(name, this.nextIndex, options); this.passesNamed.set(info.name, info); this.passesIndexed[info.index] = info; this.nextIndex++; } return info; } getByIndex(index) { const info = this.passesIndexed[index]; return info; } getByName(name) { return this.passesNamed.get(name); } constructor(){ this.passesNamed = new Map(); this.passesIndexed = []; this.nextIndex = 0; const add = (name, index, options)=>{ this.allocate(name, options); }; add('forward', SHADER_FORWARD, { isForward: true }); add('prepass'); add('shadow'); add('pick'); add('depth_pick'); } } class ShaderChunkMap extends Map { set(name, code) { if (!this.has(name) || this.get(name) !== code) { this.markDirty(); } return super.set(name, code); } add(object, override = true) { for (const [key, value] of Object.entries(object)){ if (override || !this.has(key)) { this.set(key, value); } } return this; } delete(name) { const existed = this.has(name); const result = super.delete(name); if (existed && result) { this.markDirty(); } return result; } clear() { if (this.size > 0) { this.markDirty(); } super.clear(); } markDirty() { this._dirty = true; this._keyDirty = true; } isDirty() { return this._dirty; } resetDirty() { this._dirty = false; } get key() { if (this._keyDirty) { this._keyDirty = false; this._key = Array.from(this.entries()).sort(([a], [b])=>a < b ? -1 : a > b ? 1 : 0).map(([k, v])=>`${k}=${hashCode(v)}`).join(','); } return this._key; } copy(source) { this.clear(); for (const [key, value] of source){ this.set(key, value); } return this; } constructor(validations){ super(), this._keyDirty = false, this._key = ''; this._validations = validations; } } const _chunksCache = new DeviceCache(); class ShaderChunks { static get(device, shaderLanguage = SHADERLANGUAGE_GLSL) { const cache = _chunksCache.get(device, ()=>{ return new ShaderChunks(); }); return shaderLanguage === SHADERLANGUAGE_GLSL ? cache.glsl : cache.wgsl; } static registerValidation(name, options) {} get useWGSL() { return this.glsl.size === 0 || this.wgsl.size > 0; } get key() { return `GLSL:${this.glsl.key}|WGSL:${this.wgsl.key}|API:${this.version}`; } isDirty() { return this.glsl.isDirty() || this.wgsl.isDirty(); } resetDirty() { this.glsl.resetDirty(); this.wgsl.resetDirty(); } copy(source) { this.version = source.version; this.glsl.copy(source.glsl); this.wgsl.copy(source.wgsl); return this; } constructor(){ this.glsl = new ShaderChunkMap(ShaderChunks._validations); this.wgsl = new ShaderChunkMap(ShaderChunks._validations); this.version = ''; } } ShaderChunks._validations = new Map(); class MapUtils { static merge(...maps) { const result = new Map(maps[0] ?? []); for(let i = 1; i < maps.length; i++){ const map = maps[i]; if (map) { for (const [key, value] of map){ result.set(key, value); } } } return result; } } class ShaderGeneratorPassThrough extends ShaderGenerator { generateKey(options) { return this.key; } createShaderDefinition(device, options) { return this.shaderDefinition; } constructor(key, shaderDefinition){ super(); this.key = key; this.shaderDefinition = shaderDefinition; } } class ShaderUtils { static createShader(device, options) { const programLibrary = getProgramLibrary(device); let shader = programLibrary.getCachedShader(options.uniqueName); if (!shader) { const wgsl = device.isWebGPU && (!!options.vertexWGSL || !!options.vertexChunk) && (!!options.fragmentWGSL || !!options.fragmentChunk); const chunksMap = ShaderChunks.get(device, wgsl ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL); const vertexCode = options.vertexChunk ? chunksMap.get(options.vertexChunk) : wgsl ? options.vertexWGSL : options.vertexGLSL; const fragmentCode = options.fragmentChunk ? chunksMap.get(options.fragmentChunk) : wgsl ? options.fragmentWGSL : options.fragmentGLSL; const fragmentIncludes = MapUtils.merge(chunksMap, options.fragmentIncludes); const vertexIncludes = MapUtils.merge(chunksMap, options.vertexIncludes); shader = new Shader(device, ShaderDefinitionUtils.createDefinition(device, { name: options.uniqueName, shaderLanguage: wgsl ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL, attributes: options.attributes, vertexCode: vertexCode, fragmentCode: fragmentCode, useTransformFeedback: options.useTransformFeedback, vertexIncludes: vertexIncludes, vertexDefines: options.vertexDefines, fragmentIncludes: fragmentIncludes, fragmentDefines: options.fragmentDefines, fragmentOutputTypes: options.fragmentOutputTypes })); programLibrary.setCachedShader(options.uniqueName, shader); } return shader; } static getCoreDefines(material, params) { const defines = new Map(material.defines); params.cameraShaderParams.defines.forEach((value, key)=>defines.set(key, value)); const shaderPassInfo = ShaderPass.get(params.device).getByIndex(params.pass); shaderPassInfo.defines.forEach((value, key)=>defines.set(key, value)); return defines; } static processShader(shader, processingOptions) { const shaderDefinition = shader.definition; const name = shaderDefinition.name ?? 'shader'; const key = `${name}-id-${shader.id}`; const materialGenerator = new ShaderGeneratorPassThrough(key, shaderDefinition); const libraryModuleName = 'shader'; const library = getProgramLibrary(shader.device); library.register(libraryModuleName, materialGenerator); const variant = library.getProgram(libraryModuleName, {}, processingOptions); library.unregister(libraryModuleName); return variant; } static addScreenDepthChunkDefines(device, cameraShaderParams, defines) { if (cameraShaderParams.sceneDepthMapLinear) { defines.set('SCENE_DEPTHMAP_LINEAR', ''); } if (device.textureFloatRenderable) { defines.set('SCENE_DEPTHMAP_FLOAT', ''); } } } const _quadPrimitive = { type: PRIMITIVE_TRISTRIP, base: 0, baseVertex: 0, count: 4, indexed: false }; const _tempViewport = new Vec4(); const _tempScissor = new Vec4(); const _dynamicBindGroup$1 = new DynamicBindGroup(); class QuadRender { destroy() { this.uniformBuffer?.destroy(); this.uniformBuffer = null; this.bindGroup?.destroy(); this.bindGroup = null; } render(viewport, scissor) { const device = this.shader.device; if (viewport) { _tempViewport.set(device.vx, device.vy, device.vw, device.vh); _tempScissor.set(device.sx, device.sy, device.sw, device.sh); scissor = scissor ?? viewport; device.setViewport(viewport.x, viewport.y, viewport.z, viewport.w); device.setScissor(scissor.x, scissor.y, scissor.z, scissor.w); } device.setVertexBuffer(device.quadVertexBuffer); const shader = this.shader; device.setShader(shader); if (device.supportsUniformBuffers) { device.setBindGroup(BINDGROUP_VIEW, device.emptyBindGroup); const bindGroup = this.bindGroup; bindGroup.update(); device.setBindGroup(BINDGROUP_MESH, bindGroup); const uniformBuffer = this.uniformBuffer; if (uniformBuffer) { uniformBuffer.update(_dynamicBindGroup$1); device.setBindGroup(BINDGROUP_MESH_UB, _dynamicBindGroup$1.bindGroup, _dynamicBindGroup$1.offsets); } else { device.setBindGroup(BINDGROUP_MESH_UB, device.emptyBindGroup); } } device.draw(_quadPrimitive); if (viewport) { device.setViewport(_tempViewport.x, _tempViewport.y, _tempViewport.z, _tempViewport.w); device.setScissor(_tempScissor.x, _tempScissor.y, _tempScissor.z, _tempScissor.w); } } constructor(shader){ const device = shader.device; this.shader = shader; if (device.supportsUniformBuffers) { const processingOptions = new ShaderProcessorOptions(); this.shader = ShaderUtils.processShader(shader, processingOptions); const ubFormat = this.shader.meshUniformBufferFormat; if (ubFormat) { this.uniformBuffer = new UniformBuffer(device, ubFormat, false); } const bindGroupFormat = this.shader.meshBindGroupFormat; this.bindGroup = new BindGroup(device, bindGroupFormat); } } } class RenderPassQuad extends RenderPass { execute() { const { device } = this; device.setCullMode(CULLFACE_NONE); device.setDepthState(DepthState.NODEPTH); device.setStencilState(null, null); this.quad.render(this.rect, this.scissorRect); } constructor(device, quad, rect, scissorRect){ super(device); this.quad = quad; this.rect = rect; this.scissorRect = scissorRect; } } const _tempRect = new Vec4(); function drawQuadWithShader(device, target, shader, rect, scissorRect) { const quad = new QuadRender(shader); if (!rect) { rect = _tempRect; rect.x = 0; rect.y = 0; rect.z = target ? target.width : device.width; rect.w = target ? target.height : device.height; } const renderPass = new RenderPassQuad(device, quad, rect, scissorRect); renderPass.init(target); renderPass.colorOps.clear = false; renderPass.depthStencilOps.clearDepth = false; if (device.isWebGPU && target === null && device.samples > 1) { renderPass.colorOps.store = true; } renderPass.render(); quad.destroy(); } class Batch { destroy(scene, layers) { if (this.meshInstance) { this.removeFromLayers(scene, layers); this.meshInstance.destroy(); this.meshInstance = null; } } addToLayers(scene, layers) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); if (layer) { layer.addMeshInstances([ this.meshInstance ]); } } } removeFromLayers(scene, layers) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); if (layer) { layer.removeMeshInstances([ this.meshInstance ]); } } } updateBoundingBox() { this._aabb.copy(this.origMeshInstances[0].aabb); for(let i = 1; i < this.origMeshInstances.length; i++){ this._aabb.add(this.origMeshInstances[i].aabb); } this.meshInstance.aabb = this._aabb; this.meshInstance._aabbVer = 0; } get model() { return undefined; } constructor(meshInstances, dynamic, batchGroupId){ this._aabb = new BoundingBox(); this.meshInstance = null; this.origMeshInstances = meshInstances; this.dynamic = dynamic; this.batchGroupId = batchGroupId; } } class BatchGroup { constructor(id, name, dynamic, maxAabbSize, layers = [ LAYERID_WORLD ]){ this._ui = false; this._sprite = false; this._obj = { model: [], element: [], sprite: [], render: [] }; this.id = id; this.name = name; this.dynamic = dynamic; this.maxAabbSize = maxAabbSize; this.layers = layers; } } BatchGroup.MODEL = 'model'; BatchGroup.ELEMENT = 'element'; BatchGroup.SPRITE = 'sprite'; BatchGroup.RENDER = 'render'; const _invMatrix = new Mat4(); class SkinInstance { set rootBone(rootBone) { this._rootBone = rootBone; } get rootBone() { return this._rootBone; } init(device, numBones) { const numPixels = numBones * 3; let width = Math.ceil(Math.sqrt(numPixels)); width = math.roundUp(width, 3); const height = Math.ceil(numPixels / width); this.boneTexture = new Texture(device, { width: width, height: height, format: PIXELFORMAT_RGBA32F, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, name: 'skin' }); this.matrixPalette = this.boneTexture.lock({ mode: TEXTURELOCK_READ }); this.boneTexture.unlock(); } destroy() { if (this.boneTexture) { this.boneTexture.destroy(); this.boneTexture = null; } } resolve(rootBone, entity) { this.rootBone = rootBone; const skin = this.skin; const bones = []; for(let j = 0; j < skin.boneNames.length; j++){ const boneName = skin.boneNames[j]; let bone = rootBone.findByName(boneName); if (!bone) { bone = entity; } bones.push(bone); } this.bones = bones; } initSkin(skin) { this.skin = skin; this.bones = []; const numBones = skin.inverseBindPose.length; this.init(skin.device, numBones); this.matrices = []; for(let i = 0; i < numBones; i++){ this.matrices[i] = new Mat4(); } } uploadBones(device) { this.boneTexture.upload(); } _updateMatrices(rootNode, skinUpdateIndex) { if (this._skinUpdateIndex !== skinUpdateIndex) { this._skinUpdateIndex = skinUpdateIndex; _invMatrix.copy(rootNode.getWorldTransform()).invert(); for(let i = this.bones.length - 1; i >= 0; i--){ this.matrices[i].mulAffine2(_invMatrix, this.bones[i].getWorldTransform()); this.matrices[i].mulAffine2(this.matrices[i], this.skin.inverseBindPose[i]); } } } updateMatrices(rootNode, skinUpdateIndex) { if (this._updateBeforeCull) { this._updateMatrices(rootNode, skinUpdateIndex); } } updateMatrixPalette(rootNode, skinUpdateIndex) { this._updateMatrices(rootNode, skinUpdateIndex); const mp = this.matrixPalette; const count = this.bones.length; for(let i = 0; i < count; i++){ const pe = this.matrices[i].data; const base = i * 12; mp[base] = pe[0]; mp[base + 1] = pe[4]; mp[base + 2] = pe[8]; mp[base + 3] = pe[12]; mp[base + 4] = pe[1]; mp[base + 5] = pe[5]; mp[base + 6] = pe[9]; mp[base + 7] = pe[13]; mp[base + 8] = pe[2]; mp[base + 9] = pe[6]; mp[base + 10] = pe[10]; mp[base + 11] = pe[14]; } this.uploadBones(this.skin.device); } constructor(skin){ this._dirty = true; this._rootBone = null; this._skinUpdateIndex = -1; this._updateBeforeCull = true; if (skin) { this.initSkin(skin); } } } class SkinBatchInstance extends SkinInstance { updateMatrices(rootNode, skinUpdateIndex) {} updateMatrixPalette(rootNode, skinUpdateIndex) { const mp = this.matrixPalette; const count = this.bones.length; for(let i = 0; i < count; i++){ const pe = this.bones[i].getWorldTransform().data; const base = i * 12; mp[base] = pe[0]; mp[base + 1] = pe[4]; mp[base + 2] = pe[8]; mp[base + 3] = pe[12]; mp[base + 4] = pe[1]; mp[base + 5] = pe[5]; mp[base + 6] = pe[9]; mp[base + 7] = pe[13]; mp[base + 8] = pe[2]; mp[base + 9] = pe[6]; mp[base + 10] = pe[10]; mp[base + 11] = pe[14]; } this.uploadBones(this.device); } constructor(device, nodes, rootNode){ super(); const numBones = nodes.length; this.init(device, numBones); this.device = device; this.rootNode = rootNode; this.bones = nodes; } } let id$5 = 0; class GeometryData { initDefaults() { this.recreate = false; this.verticesUsage = BUFFER_STATIC; this.indicesUsage = BUFFER_STATIC; this.maxVertices = 0; this.maxIndices = 0; this.vertexCount = 0; this.indexCount = 0; this.vertexStreamsUpdated = false; this.indexStreamUpdated = false; this.vertexStreamDictionary = {}; this.indices = null; } _changeVertexCount(count, semantic) { if (!this.vertexCount) { this.vertexCount = count; } } constructor(){ this.initDefaults(); } } GeometryData.DEFAULT_COMPONENTS_POSITION = 3; GeometryData.DEFAULT_COMPONENTS_NORMAL = 3; GeometryData.DEFAULT_COMPONENTS_UV = 2; GeometryData.DEFAULT_COMPONENTS_COLORS = 4; class GeometryVertexStream { constructor(data, componentCount, dataType, dataTypeNormalize, asInt){ this.data = data; this.componentCount = componentCount; this.dataType = dataType; this.dataTypeNormalize = dataTypeNormalize; this.asInt = asInt; } } class Mesh extends RefCountedObject { static fromGeometry(graphicsDevice, geometry, options = {}) { const mesh = new Mesh(graphicsDevice, options); const { positions, normals, tangents, colors, uvs, uvs1, blendIndices, blendWeights, indices } = geometry; if (positions) { mesh.setPositions(positions); } if (normals) { mesh.setNormals(normals); } if (tangents) { mesh.setVertexStream(SEMANTIC_TANGENT, tangents, 4); } if (colors) { mesh.setColors32(colors); } if (uvs) { mesh.setUvs(0, uvs); } if (uvs1) { mesh.setUvs(1, uvs1); } if (blendIndices) { mesh.setVertexStream(SEMANTIC_BLENDINDICES, blendIndices, 4, blendIndices.length / 4, TYPE_UINT8); } if (blendWeights) { mesh.setVertexStream(SEMANTIC_BLENDWEIGHT, blendWeights, 4); } if (indices) { mesh.setIndices(indices); } mesh.update(); return mesh; } set morph(morph) { if (morph !== this._morph) { if (this._morph) { this._morph.decRefCount(); } this._morph = morph; if (morph) { morph.incRefCount(); } } } get morph() { return this._morph; } set aabb(aabb) { this._aabb = aabb; this._aabbVer++; } get aabb() { return this._aabb; } destroy() { const morph = this.morph; if (morph) { this.morph = null; if (morph.refCount < 1) { morph.destroy(); } } if (this.vertexBuffer) { this.vertexBuffer.destroy(); this.vertexBuffer = null; } for(let j = 0; j < this.indexBuffer.length; j++){ this._destroyIndexBuffer(j); } this.indexBuffer.length = 0; this._geometryData = null; } _destroyIndexBuffer(index) { if (this.indexBuffer[index]) { this.indexBuffer[index].destroy(); this.indexBuffer[index] = null; } } _initBoneAabbs(morphTargets) { this.boneAabb = []; this.boneUsed = []; let x, y, z; let bMax, bMin; const boneMin = []; const boneMax = []; const boneUsed = this.boneUsed; const numBones = this.skin.boneNames.length; let maxMorphX, maxMorphY, maxMorphZ; for(let i = 0; i < numBones; i++){ boneMin[i] = new Vec3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); boneMax[i] = new Vec3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE); } const iterator = new VertexIterator(this.vertexBuffer); const posElement = iterator.element[SEMANTIC_POSITION]; const weightsElement = iterator.element[SEMANTIC_BLENDWEIGHT]; const indicesElement = iterator.element[SEMANTIC_BLENDINDICES]; const numVerts = this.vertexBuffer.numVertices; for(let j = 0; j < numVerts; j++){ for(let k = 0; k < 4; k++){ const boneWeight = weightsElement.array[weightsElement.index + k]; if (boneWeight > 0) { const boneIndex = indicesElement.array[indicesElement.index + k]; boneUsed[boneIndex] = true; x = posElement.array[posElement.index]; y = posElement.array[posElement.index + 1]; z = posElement.array[posElement.index + 2]; bMax = boneMax[boneIndex]; bMin = boneMin[boneIndex]; if (bMin.x > x) bMin.x = x; if (bMin.y > y) bMin.y = y; if (bMin.z > z) bMin.z = z; if (bMax.x < x) bMax.x = x; if (bMax.y < y) bMax.y = y; if (bMax.z < z) bMax.z = z; if (morphTargets) { let minMorphX = maxMorphX = x; let minMorphY = maxMorphY = y; let minMorphZ = maxMorphZ = z; for(let l = 0; l < morphTargets.length; l++){ const target = morphTargets[l]; const dx = target.deltaPositions[j * 3]; const dy = target.deltaPositions[j * 3 + 1]; const dz = target.deltaPositions[j * 3 + 2]; if (dx < 0) { minMorphX += dx; } else { maxMorphX += dx; } if (dy < 0) { minMorphY += dy; } else { maxMorphY += dy; } if (dz < 0) { minMorphZ += dz; } else { maxMorphZ += dz; } } if (bMin.x > minMorphX) bMin.x = minMorphX; if (bMin.y > minMorphY) bMin.y = minMorphY; if (bMin.z > minMorphZ) bMin.z = minMorphZ; if (bMax.x < maxMorphX) bMax.x = maxMorphX; if (bMax.y < maxMorphY) bMax.y = maxMorphY; if (bMax.z < maxMorphZ) bMax.z = maxMorphZ; } } } iterator.next(); } const positionElement = this.vertexBuffer.getFormat().elements.find((e)=>e.name === SEMANTIC_POSITION); if (positionElement && positionElement.normalize) { const func = (()=>{ switch(positionElement.dataType){ case TYPE_INT8: return (x)=>Math.max(x / 127.0, -1); case TYPE_UINT8: return (x)=>x / 255.0; case TYPE_INT16: return (x)=>Math.max(x / 32767.0, -1); case TYPE_UINT16: return (x)=>x / 65535.0; default: return (x)=>x; } })(); for(let i = 0; i < numBones; i++){ if (boneUsed[i]) { const min = boneMin[i]; const max = boneMax[i]; min.set(func(min.x), func(min.y), func(min.z)); max.set(func(max.x), func(max.y), func(max.z)); } } } for(let i = 0; i < numBones; i++){ const aabb = new BoundingBox(); aabb.setMinMax(boneMin[i], boneMax[i]); this.boneAabb.push(aabb); } } _initGeometryData() { if (!this._geometryData) { this._geometryData = new GeometryData(); if (this.vertexBuffer) { this._geometryData.vertexCount = this.vertexBuffer.numVertices; this._geometryData.maxVertices = this.vertexBuffer.numVertices; } if (this.indexBuffer.length > 0 && this.indexBuffer[0]) { this._geometryData.indexCount = this.indexBuffer[0].numIndices; this._geometryData.maxIndices = this.indexBuffer[0].numIndices; } } } clear(verticesDynamic, indicesDynamic, maxVertices = 0, maxIndices = 0) { this._initGeometryData(); this._geometryData.initDefaults(); this._geometryData.recreate = true; this._geometryData.maxVertices = maxVertices; this._geometryData.maxIndices = maxIndices; this._geometryData.verticesUsage = verticesDynamic ? BUFFER_STATIC : BUFFER_DYNAMIC; this._geometryData.indicesUsage = indicesDynamic ? BUFFER_STATIC : BUFFER_DYNAMIC; } setVertexStream(semantic, data, componentCount, numVertices, dataType = TYPE_FLOAT32, dataTypeNormalize = false, asInt = false) { this._initGeometryData(); const vertexCount = numVertices || data.length / componentCount; this._geometryData._changeVertexCount(vertexCount, semantic); this._geometryData.vertexStreamsUpdated = true; this._geometryData.vertexStreamDictionary[semantic] = new GeometryVertexStream(data, componentCount, dataType, dataTypeNormalize, asInt); } getVertexStream(semantic, data) { let count = 0; let done = false; if (this._geometryData) { const stream = this._geometryData.vertexStreamDictionary[semantic]; if (stream) { done = true; count = this._geometryData.vertexCount; if (ArrayBuffer.isView(data)) { data.set(stream.data); } else { data.length = 0; data.push(stream.data); } } } if (!done) { if (this.vertexBuffer) { const iterator = new VertexIterator(this.vertexBuffer); count = iterator.readData(semantic, data); } } return count; } setPositions(positions, componentCount = GeometryData.DEFAULT_COMPONENTS_POSITION, numVertices) { this.setVertexStream(SEMANTIC_POSITION, positions, componentCount, numVertices, TYPE_FLOAT32, false); } setNormals(normals, componentCount = GeometryData.DEFAULT_COMPONENTS_NORMAL, numVertices) { this.setVertexStream(SEMANTIC_NORMAL, normals, componentCount, numVertices, TYPE_FLOAT32, false); } setUvs(channel, uvs, componentCount = GeometryData.DEFAULT_COMPONENTS_UV, numVertices) { this.setVertexStream(SEMANTIC_TEXCOORD + channel, uvs, componentCount, numVertices, TYPE_FLOAT32, false); } setColors(colors, componentCount = GeometryData.DEFAULT_COMPONENTS_COLORS, numVertices) { this.setVertexStream(SEMANTIC_COLOR, colors, componentCount, numVertices, TYPE_FLOAT32, false); } setColors32(colors, numVertices) { this.setVertexStream(SEMANTIC_COLOR, colors, GeometryData.DEFAULT_COMPONENTS_COLORS, numVertices, TYPE_UINT8, true); } setIndices(indices, numIndices) { this._initGeometryData(); this._geometryData.indexStreamUpdated = true; this._geometryData.indices = indices; this._geometryData.indexCount = numIndices || indices.length; } getPositions(positions) { return this.getVertexStream(SEMANTIC_POSITION, positions); } getNormals(normals) { return this.getVertexStream(SEMANTIC_NORMAL, normals); } getUvs(channel, uvs) { return this.getVertexStream(SEMANTIC_TEXCOORD + channel, uvs); } getColors(colors) { return this.getVertexStream(SEMANTIC_COLOR, colors); } getIndices(indices) { let count = 0; if (this._geometryData && this._geometryData.indices) { const streamIndices = this._geometryData.indices; count = this._geometryData.indexCount; if (ArrayBuffer.isView(indices)) { indices.set(streamIndices); } else { indices.length = 0; for(let i = 0, il = streamIndices.length; i < il; i++){ indices.push(streamIndices[i]); } } } else { if (this.indexBuffer.length > 0 && this.indexBuffer[0]) { const indexBuffer = this.indexBuffer[0]; count = indexBuffer.readData(indices); } } return count; } update(primitiveType = PRIMITIVE_TRIANGLES, updateBoundingBox = true) { if (this._geometryData) { if (updateBoundingBox) { const stream = this._geometryData.vertexStreamDictionary[SEMANTIC_POSITION]; if (stream) { if (stream.componentCount === 3) { this._aabb.compute(stream.data, this._geometryData.vertexCount); this._aabbVer++; } } } let destroyVB = this._geometryData.recreate; if (this._geometryData.vertexCount > this._geometryData.maxVertices) { destroyVB = true; this._geometryData.maxVertices = this._geometryData.vertexCount; } if (destroyVB) { if (this.vertexBuffer) { this.vertexBuffer.destroy(); this.vertexBuffer = null; } } let destroyIB = this._geometryData.recreate; if (this._geometryData.indexCount > this._geometryData.maxIndices) { destroyIB = true; this._geometryData.maxIndices = this._geometryData.indexCount; } if (destroyIB) { if (this.indexBuffer.length > 0 && this.indexBuffer[0]) { this.indexBuffer[0].destroy(); this.indexBuffer[0] = null; } } if (this._geometryData.vertexStreamsUpdated) { this._updateVertexBuffer(); } if (this._geometryData.indexStreamUpdated) { this._updateIndexBuffer(); } this.primitive[0].type = primitiveType; if (this.indexBuffer.length > 0 && this.indexBuffer[0]) { if (this._geometryData.indexStreamUpdated) { this.primitive[0].count = this._geometryData.indexCount; this.primitive[0].indexed = true; } } else { if (this._geometryData.vertexStreamsUpdated) { this.primitive[0].count = this._geometryData.vertexCount; this.primitive[0].indexed = false; } } this._geometryData.vertexCount = 0; this._geometryData.indexCount = 0; this._geometryData.vertexStreamsUpdated = false; this._geometryData.indexStreamUpdated = false; this._geometryData.recreate = false; this.updateRenderStates(); } } _buildVertexFormat(vertexCount) { const vertexDesc = []; for(const semantic in this._geometryData.vertexStreamDictionary){ const stream = this._geometryData.vertexStreamDictionary[semantic]; vertexDesc.push({ semantic: semantic, components: stream.componentCount, type: stream.dataType, normalize: stream.dataTypeNormalize, asInt: stream.asInt }); } return new VertexFormat(this.device, vertexDesc, vertexCount); } _updateVertexBuffer() { if (!this.vertexBuffer) { const allocateVertexCount = this._geometryData.maxVertices; const format = this._buildVertexFormat(allocateVertexCount); this.vertexBuffer = new VertexBuffer(this.device, format, allocateVertexCount, { usage: this._geometryData.verticesUsage, storage: this._storageVertex }); } const iterator = new VertexIterator(this.vertexBuffer); const numVertices = this._geometryData.vertexCount; for(const semantic in this._geometryData.vertexStreamDictionary){ const stream = this._geometryData.vertexStreamDictionary[semantic]; iterator.writeData(semantic, stream.data, numVertices); delete this._geometryData.vertexStreamDictionary[semantic]; } iterator.end(); } _updateIndexBuffer() { if (this.indexBuffer.length <= 0 || !this.indexBuffer[0]) { const maxVertices = this._geometryData.maxVertices; const createFormat = maxVertices > 0xffff || maxVertices === 0 ? INDEXFORMAT_UINT32 : INDEXFORMAT_UINT16; const options = this._storageIndex ? { storage: true } : undefined; this.indexBuffer[0] = new IndexBuffer(this.device, createFormat, this._geometryData.maxIndices, this._geometryData.indicesUsage, undefined, options); } const srcIndices = this._geometryData.indices; if (srcIndices) { const indexBuffer = this.indexBuffer[0]; indexBuffer.writeData(srcIndices, this._geometryData.indexCount); this._geometryData.indices = null; } } prepareRenderState(renderStyle) { if (renderStyle === RENDERSTYLE_WIREFRAME) { this.generateWireframe(); } else if (renderStyle === RENDERSTYLE_POINTS) { this.primitive[RENDERSTYLE_POINTS] = { type: PRIMITIVE_POINTS, base: 0, baseVertex: 0, count: this.vertexBuffer ? this.vertexBuffer.numVertices : 0, indexed: false }; } } updateRenderStates() { if (this.primitive[RENDERSTYLE_POINTS]) { this.prepareRenderState(RENDERSTYLE_POINTS); } if (this.primitive[RENDERSTYLE_WIREFRAME]) { this.prepareRenderState(RENDERSTYLE_WIREFRAME); } } generateWireframe() { this._destroyIndexBuffer(RENDERSTYLE_WIREFRAME); const numVertices = this.vertexBuffer.numVertices; let lines; let format; if (this.indexBuffer.length > 0 && this.indexBuffer[0]) { const offsets = [ [ 0, 1 ], [ 1, 2 ], [ 2, 0 ] ]; const base = this.primitive[RENDERSTYLE_SOLID].base; const count = this.primitive[RENDERSTYLE_SOLID].count; const baseVertex = this.primitive[RENDERSTYLE_SOLID].baseVertex || 0; const indexBuffer = this.indexBuffer[RENDERSTYLE_SOLID]; const indicesArrayType = typedArrayIndexFormats[indexBuffer.format]; const srcIndices = new indicesArrayType(indexBuffer.storage); const tmpIndices = new indicesArrayType(count * 2); const seen = new Set(); let len = 0; for(let j = base; j < base + count; j += 3){ for(let k = 0; k < 3; k++){ const i1 = srcIndices[j + offsets[k][0]] + baseVertex; const i2 = srcIndices[j + offsets[k][1]] + baseVertex; const hash = i1 > i2 ? i2 * numVertices + i1 : i1 * numVertices + i2; if (!seen.has(hash)) { seen.add(hash); tmpIndices[len++] = i1; tmpIndices[len++] = i2; } } } seen.clear(); format = indexBuffer.format; lines = tmpIndices.slice(0, len); } else { const safeNumVertices = numVertices - numVertices % 3; const count = safeNumVertices / 3 * 6; format = count > 65535 ? INDEXFORMAT_UINT32 : INDEXFORMAT_UINT16; lines = count > 65535 ? new Uint32Array(count) : new Uint16Array(count); let idx = 0; for(let i = 0; i < safeNumVertices; i += 3){ lines[idx++] = i; lines[idx++] = i + 1; lines[idx++] = i + 1; lines[idx++] = i + 2; lines[idx++] = i + 2; lines[idx++] = i; } } const wireBuffer = new IndexBuffer(this.vertexBuffer.device, format, lines.length, BUFFER_STATIC, lines.buffer); this.primitive[RENDERSTYLE_WIREFRAME] = { type: PRIMITIVE_LINES, base: 0, baseVertex: 0, count: lines.length, indexed: true }; this.indexBuffer[RENDERSTYLE_WIREFRAME] = wireBuffer; } constructor(graphicsDevice, options){ super(), this.indexBuffer = [ null ], this.vertexBuffer = null, this.primitive = [ { type: 0, base: 0, baseVertex: 0, count: 0 } ], this.skin = null, this.boneAabb = null, this._aabbVer = 0, this._aabb = new BoundingBox(), this._geometryData = null, this._morph = null, this._storageIndex = false, this._storageVertex = false; this.id = id$5++; this.device = graphicsDevice; this._storageIndex = options?.storageIndex || false; this._storageVertex = options?.storageVertex || false; } } const defaultMaterialDeviceCache = new DeviceCache(); function getDefaultMaterial(device) { const material = defaultMaterialDeviceCache.get(device); return material; } function setDefaultMaterial(device, material) { defaultMaterialDeviceCache.get(device, ()=>{ return material; }); } class RefCountedCache { destroy() { this.cache.forEach((refCount, object)=>{ object.destroy(); }); this.cache.clear(); } incRef(object) { const refCount = (this.cache.get(object) || 0) + 1; this.cache.set(object, refCount); } decRef(object) { if (object) { let refCount = this.cache.get(object); if (refCount) { refCount--; if (refCount === 0) { this.cache.delete(object); object.destroy(); } else { this.cache.set(object, refCount); } } } } constructor(){ this.cache = new Map(); } } class LightmapCache { static incRef(texture) { this.cache.incRef(texture); } static decRef(texture) { this.cache.decRef(texture); } static destroy() { this.cache.destroy(); } } LightmapCache.cache = new RefCountedCache(); let id$4 = 0; const _tmpAabb = new BoundingBox(); const _tempBoneAabb = new BoundingBox(); const _tempSphere = new BoundingSphere(); const _meshSet = new Set(); const lookupHashes = new Uint32Array(4); class InstancingData { destroy() { if (this._destroyVertexBuffer) { this.vertexBuffer?.destroy(); } this.vertexBuffer = null; } constructor(numObjects){ this.vertexBuffer = null; this._destroyVertexBuffer = false; this.count = numObjects; } } class ShaderInstance { getBindGroup(device) { if (!this.bindGroup) { const shader = this.shader; const bindGroupFormat = shader.meshBindGroupFormat; this.bindGroup = new BindGroup(device, bindGroupFormat); } return this.bindGroup; } getUniformBuffer(device) { if (!this.uniformBuffer) { const shader = this.shader; const ubFormat = shader.meshUniformBufferFormat; this.uniformBuffer = new UniformBuffer(device, ubFormat, false); } return this.uniformBuffer; } destroy() { this.bindGroup?.destroy(); this.bindGroup = null; this.uniformBuffer?.destroy(); this.uniformBuffer = null; } constructor(){ this.bindGroup = null; this.uniformBuffer = null; } } class MeshInstance { set drawBucket(bucket) { this._drawBucket = Math.floor(bucket) & 0xff; this.updateKey(); } get drawBucket() { return this._drawBucket; } set renderStyle(renderStyle) { this._renderStyle = renderStyle; this.mesh.prepareRenderState(renderStyle); } get renderStyle() { return this._renderStyle; } set mesh(mesh) { if (mesh === this._mesh) { return; } if (this._mesh) { this._mesh.decRefCount(); } this._mesh = mesh; if (mesh) { mesh.incRefCount(); } } get mesh() { return this._mesh; } set aabb(aabb) { this._aabb = aabb; } get aabb() { if (!this._updateAabb) { return this._aabb; } if (this._updateAabbFunc) { return this._updateAabbFunc(this._aabb); } let localAabb = this._customAabb; let toWorldSpace = !!localAabb; if (!localAabb) { localAabb = _tmpAabb; if (this.skinInstance) { if (!this.mesh.boneAabb) { const morphTargets = this._morphInstance ? this._morphInstance.morph._targets : null; this.mesh._initBoneAabbs(morphTargets); } const boneUsed = this.mesh.boneUsed; let first = true; for(let i = 0; i < this.mesh.boneAabb.length; i++){ if (boneUsed[i]) { _tempBoneAabb.setFromTransformedAabb(this.mesh.boneAabb[i], this.skinInstance.matrices[i]); if (first) { first = false; localAabb.center.copy(_tempBoneAabb.center); localAabb.halfExtents.copy(_tempBoneAabb.halfExtents); } else { localAabb.add(_tempBoneAabb); } } } toWorldSpace = true; } else if (this.node._aabbVer !== this._aabbVer || this.mesh._aabbVer !== this._aabbMeshVer) { if (this.mesh) { localAabb.center.copy(this.mesh.aabb.center); localAabb.halfExtents.copy(this.mesh.aabb.halfExtents); } else { localAabb.center.set(0, 0, 0); localAabb.halfExtents.set(0, 0, 0); } if (this.mesh && this.mesh.morph) { const morphAabb = this.mesh.morph.aabb; localAabb._expand(morphAabb.getMin(), morphAabb.getMax()); } toWorldSpace = true; this._aabbVer = this.node._aabbVer; this._aabbMeshVer = this.mesh._aabbVer; } } if (toWorldSpace) { this._aabb.setFromTransformedAabb(localAabb, this.node.getWorldTransform()); } return this._aabb; } clearShaders() { this._shaderCache.forEach((shaderInstance)=>{ shaderInstance.destroy(); }); this._shaderCache.clear(); } getShaderInstance(shaderPass, lightHash, scene, cameraShaderParams, viewUniformFormat, viewBindGroupFormat, sortedLights) { const shaderDefs = this._shaderDefs; lookupHashes[0] = shaderPass; lookupHashes[1] = lightHash; lookupHashes[2] = shaderDefs; lookupHashes[3] = cameraShaderParams.hash; const hash = hash32Fnv1a(lookupHashes); let shaderInstance = this._shaderCache.get(hash); if (!shaderInstance) { const mat = this._material; shaderInstance = new ShaderInstance(); shaderInstance.shader = mat.variants.get(hash); shaderInstance.hashes = new Uint32Array(lookupHashes); if (!shaderInstance.shader) { const shader = mat.getShaderVariant({ device: this.mesh.device, scene: scene, objDefs: shaderDefs, cameraShaderParams: cameraShaderParams, pass: shaderPass, sortedLights: sortedLights, viewUniformFormat: viewUniformFormat, viewBindGroupFormat: viewBindGroupFormat, vertexFormat: this.mesh.vertexBuffer?.format }); mat.variants.set(hash, shader); shaderInstance.shader = shader; } this._shaderCache.set(hash, shaderInstance); } return shaderInstance; } set material(material) { this.clearShaders(); const prevMat = this._material; if (prevMat) { prevMat.removeMeshInstanceRef(this); } this._material = material; if (material) { material.addMeshInstanceRef(this); this.transparent = material.transparent; this.updateKey(); } } get material() { return this._material; } _updateShaderDefs(shaderDefs) { if (shaderDefs !== this._shaderDefs) { this._shaderDefs = shaderDefs; this.clearShaders(); } } set calculateSortDistance(calculateSortDistance) { this._calculateSortDistance = calculateSortDistance; } get calculateSortDistance() { return this._calculateSortDistance; } set receiveShadow(val) { if (this._receiveShadow !== val) { this._receiveShadow = val; this._updateShaderDefs(val ? this._shaderDefs & ~SHADERDEF_NOSHADOW : this._shaderDefs | SHADERDEF_NOSHADOW); } } get receiveShadow() { return this._receiveShadow; } set batching(val) { this._updateShaderDefs(val ? this._shaderDefs | SHADERDEF_BATCH : this._shaderDefs & ~SHADERDEF_BATCH); } get batching() { return (this._shaderDefs & SHADERDEF_BATCH) !== 0; } set skinInstance(val) { this._skinInstance = val; this._updateShaderDefs(val ? this._shaderDefs | SHADERDEF_SKIN : this._shaderDefs & ~SHADERDEF_SKIN); this._setupSkinUpdate(); } get skinInstance() { return this._skinInstance; } set morphInstance(val) { this._morphInstance?.destroy(); this._morphInstance = val; let shaderDefs = this._shaderDefs; shaderDefs = val && val.morph.morphPositions ? shaderDefs | SHADERDEF_MORPH_POSITION : shaderDefs & ~SHADERDEF_MORPH_POSITION; shaderDefs = val && val.morph.morphNormals ? shaderDefs | SHADERDEF_MORPH_NORMAL : shaderDefs & ~SHADERDEF_MORPH_NORMAL; shaderDefs = val && val.morph.intRenderFormat ? shaderDefs | SHADERDEF_MORPH_TEXTURE_BASED_INT : shaderDefs & ~SHADERDEF_MORPH_TEXTURE_BASED_INT; this._updateShaderDefs(shaderDefs); } get morphInstance() { return this._morphInstance; } set screenSpace(val) { if (this._screenSpace !== val) { this._screenSpace = val; this._updateShaderDefs(val ? this._shaderDefs | SHADERDEF_SCREENSPACE : this._shaderDefs & ~SHADERDEF_SCREENSPACE); } } get screenSpace() { return this._screenSpace; } set key(val) { this._sortKeyForward = val; } get key() { return this._sortKeyForward; } set mask(val) { const toggles = this._shaderDefs & 0x0000FFFF; this._updateShaderDefs(toggles | val << 16); } get mask() { return this._shaderDefs >> 16; } set instancingCount(value) { if (this.instancingData) { this.instancingData.count = value; } } get instancingCount() { return this.instancingData ? this.instancingData.count : 0; } destroy() { const mesh = this.mesh; if (mesh) { this.mesh = null; if (mesh.refCount < 1) { mesh.destroy(); } } this.setRealtimeLightmap(MeshInstance.lightmapParamNames[0], null); this.setRealtimeLightmap(MeshInstance.lightmapParamNames[1], null); this._skinInstance?.destroy(); this._skinInstance = null; this.morphInstance?.destroy(); this.morphInstance = null; this.clearShaders(); this.material = null; this.instancingData?.destroy(); this.destroyDrawCommands(); } destroyDrawCommands() { if (this.drawCommands) { for (const cmd of this.drawCommands.values()){ cmd?.destroy(); } this.drawCommands = null; } } static _prepareRenderStyleForArray(meshInstances, renderStyle) { if (meshInstances) { for(let i = 0; i < meshInstances.length; i++){ meshInstances[i]._renderStyle = renderStyle; const mesh = meshInstances[i].mesh; if (!_meshSet.has(mesh)) { _meshSet.add(mesh); mesh.prepareRenderState(renderStyle); } } _meshSet.clear(); } } _isVisible(camera) { if (this.visible) { if (this.isVisibleFunc) { return this.isVisibleFunc(camera); } _tempSphere.center = this.aabb.center; _tempSphere.radius = this._aabb.halfExtents.length(); return camera.frustum.containsSphere(_tempSphere) > 0; } return false; } updateKey() { const { material } = this; this._sortKeyForward = this._drawBucket << 23 | (material.alphaToCoverage || material.alphaTest ? 0x400000 : 0) | material.id & 0x3fffff; } setInstancing(vertexBuffer, cull = false) { if (vertexBuffer) { this.instancingData = new InstancingData(vertexBuffer.numVertices); this.instancingData.vertexBuffer = vertexBuffer; vertexBuffer.format.instancing = true; this.cull = cull; } else { this.instancingData = null; this.cull = true; } this._updateShaderDefs(vertexBuffer ? this._shaderDefs | SHADERDEF_INSTANCING : this._shaderDefs & ~SHADERDEF_INSTANCING); } setIndirect(camera, slot, count = 1) { const key = camera?.camera ?? null; if (slot === -1) { this._deleteDrawCommandsKey(key); } else { this.drawCommands ?? (this.drawCommands = new Map()); const cmd = this.drawCommands.get(key) ?? new DrawCommands(this.mesh.device); cmd.slotIndex = slot; cmd.update(count); this.drawCommands.set(key, cmd); const device = this.mesh.device; device.mapsToClear.add(this.drawCommands); } } setMultiDraw(camera, maxCount = 1) { const key = camera?.camera ?? null; let cmd; if (maxCount === 0) { this._deleteDrawCommandsKey(key); } else { this.drawCommands ?? (this.drawCommands = new Map()); cmd = this.drawCommands.get(key); if (!cmd) { const indexBuffer = this.mesh.indexBuffer?.[0]; const indexFormat = indexBuffer?.format; const indexSizeBytes = indexFormat !== undefined ? indexFormatByteSize[indexFormat] : 0; cmd = new DrawCommands(this.mesh.device, indexSizeBytes); this.drawCommands.set(key, cmd); } cmd.allocate(maxCount); } return cmd; } _deleteDrawCommandsKey(key) { const cmds = this.drawCommands; if (cmds) { const cmd = cmds.get(key); cmd?.destroy(); cmds.delete(key); if (cmds.size === 0) { this.destroyDrawCommands(); } } } getDrawCommands(camera) { const cmds = this.drawCommands; if (!cmds) return undefined; return cmds.get(camera) ?? cmds.get(null); } getIndirectMetaData() { const prim = this.mesh?.primitive[this.renderStyle]; const data = this.meshMetaData ?? (this.meshMetaData = new Int32Array(4)); data[0] = prim.count; data[1] = prim.base; data[2] = prim.baseVertex; return data; } ensureMaterial(device) { if (!this.material) { this.material = getDefaultMaterial(device); } } clearParameters() { this.parameters = {}; } getParameters() { return this.parameters; } getParameter(name) { return this.parameters[name]; } setParameter(name, data, passFlags = 0xFFFFFFFF) { const param = this.parameters[name]; if (param) { param.data = data; param.passFlags = passFlags; } else { this.parameters[name] = { scopeId: null, data: data, passFlags: passFlags }; } } setRealtimeLightmap(name, texture) { const old = this.getParameter(name); if (old === texture) { return; } if (old) { LightmapCache.decRef(old.data); } if (texture) { LightmapCache.incRef(texture); this.setParameter(name, texture); } else { this.deleteParameter(name); } } deleteParameter(name) { if (this.parameters[name]) { delete this.parameters[name]; } } setParameters(device, passFlag) { const parameters = this.parameters; for(const paramName in parameters){ const parameter = parameters[paramName]; if (parameter.passFlags & passFlag) { if (!parameter.scopeId) { parameter.scopeId = device.scope.resolve(paramName); } parameter.scopeId.setValue(parameter.data); } } } setLightmapped(value) { if (value) { this.mask = (this.mask | MASK_AFFECT_LIGHTMAPPED) & -6; } else { this.setRealtimeLightmap(MeshInstance.lightmapParamNames[0], null); this.setRealtimeLightmap(MeshInstance.lightmapParamNames[1], null); this._shaderDefs &= -4289; this.mask = (this.mask | MASK_AFFECT_DYNAMIC) & -7; } } setCustomAabb(aabb) { if (aabb) { if (this._customAabb) { this._customAabb.copy(aabb); } else { this._customAabb = aabb.clone(); } } else { this._customAabb = null; this._aabbVer = -1; } this._setupSkinUpdate(); } _setupSkinUpdate() { if (this._skinInstance) { this._skinInstance._updateBeforeCull = !this._customAabb; } } constructor(mesh, material, node = null){ this.castShadow = false; this.shadowCascadeMask = SHADOW_CASCADE_ALL; this.cull = true; this.drawOrder = 0; this._drawBucket = 127; this.visible = true; this.visibleThisFrame = false; this.flipFacesFactor = 1; this.gsplatInstance = null; this.id = id$4++; this.isVisibleFunc = null; this.instancingData = null; this.indirectData = null; this.drawCommands = null; this.meshMetaData = null; this.parameters = {}; this.pick = true; this.stencilFront = null; this.stencilBack = null; this.transparent = false; this._aabb = new BoundingBox(); this._aabbVer = -1; this._aabbMeshVer = -1; this._customAabb = null; this._updateAabb = true; this._updateAabbFunc = null; this._sortKeyShadow = 0; this._sortKeyForward = 0; this._sortKeyDynamic = 0; this._layer = LAYER_WORLD; this._material = null; this._skinInstance = null; this._morphInstance = null; this._receiveShadow = true; this._renderStyle = RENDERSTYLE_SOLID; this._screenSpace = false; this._shaderCache = new Map(); this._shaderDefs = MASK_AFFECT_DYNAMIC << 16; this._calculateSortDistance = null; this.node = node; this._mesh = mesh; mesh.incRefCount(); this.material = material; if (mesh.vertexBuffer) { const format = mesh.vertexBuffer.format; this._shaderDefs |= format.hasUv0 ? SHADERDEF_UV0 : 0; this._shaderDefs |= format.hasUv1 ? SHADERDEF_UV1 : 0; this._shaderDefs |= format.hasColor ? SHADERDEF_VCOLOR : 0; this._shaderDefs |= format.hasTangents ? SHADERDEF_TANGENTS : 0; } this.updateKey(); } } MeshInstance.lightmapParamNames = [ 'texture_lightMap', 'texture_dirLightMap' ]; const _triFanIndices = [ 0, 1, 3, 2, 3, 1 ]; const _triStripIndices = [ 0, 1, 3, 0, 3, 2 ]; const mat3 = new Mat3(); function paramsIdentical(a, b) { if (a && !b) return false; if (!a && b) return false; a = a.data; b = b.data; if (a === b) return true; if (a instanceof Float32Array && b instanceof Float32Array) { if (a.length !== b.length) return false; for(let i = 0; i < a.length; i++){ if (a[i] !== b[i]) return false; } return true; } return false; } function equalParamSets(params1, params2) { for(const param in params1){ if (params1.hasOwnProperty(param) && !paramsIdentical(params1[param], params2[param])) { return false; } } for(const param in params2){ if (params2.hasOwnProperty(param) && !paramsIdentical(params2[param], params1[param])) { return false; } } return true; } function getScaleSign(mi) { return mi.node.worldTransform.scaleSign; } class BatchManager { destroy() { this.device = null; this.rootNode = null; this.scene = null; this._batchGroups = {}; this._batchList = []; this._dirtyGroups = []; } addGroup(name, dynamic, maxAabbSize, id, layers) { if (id === undefined) { id = this._batchGroupCounter; this._batchGroupCounter++; } if (this._batchGroups[id]) { return undefined; } const group = new BatchGroup(id, name, dynamic, maxAabbSize, layers); this._batchGroups[id] = group; return group; } removeGroup(id) { if (!this._batchGroups[id]) { return; } const newBatchList = []; for(let i = 0; i < this._batchList.length; i++){ if (this._batchList[i].batchGroupId === id) { this.destroyBatch(this._batchList[i]); } else { newBatchList.push(this._batchList[i]); } } this._batchList = newBatchList; this._removeModelsFromBatchGroup(this.rootNode, id); delete this._batchGroups[id]; } markGroupDirty(id) { if (this._dirtyGroups.indexOf(id) < 0) { this._dirtyGroups.push(id); } } getGroupByName(name) { const groups = this._batchGroups; for(const group in groups){ if (!groups.hasOwnProperty(group)) continue; if (groups[group].name === name) { return groups[group]; } } return null; } getBatches(batchGroupId) { const results = []; const len = this._batchList.length; for(let i = 0; i < len; i++){ const batch = this._batchList[i]; if (batch.batchGroupId === batchGroupId) { results.push(batch); } } return results; } _removeModelsFromBatchGroup(node, id) { if (!node.enabled) return; if (node.model && node.model.batchGroupId === id) { node.model.batchGroupId = -1; } if (node.render && node.render.batchGroupId === id) { node.render.batchGroupId = -1; } if (node.element && node.element.batchGroupId === id) { node.element.batchGroupId = -1; } if (node.sprite && node.sprite.batchGroupId === id) { node.sprite.batchGroupId = -1; } for(let i = 0; i < node._children.length; i++){ this._removeModelsFromBatchGroup(node._children[i], id); } } insert(type, groupId, node) { const group = this._batchGroups[groupId]; if (group) { if (group._obj[type].indexOf(node) < 0) { group._obj[type].push(node); this.markGroupDirty(groupId); } } } remove(type, groupId, node) { const group = this._batchGroups[groupId]; if (group) { const idx = group._obj[type].indexOf(node); if (idx >= 0) { group._obj[type].splice(idx, 1); this.markGroupDirty(groupId); } } } _extractRender(node, arr, group, groupMeshInstances) { if (node.render) { arr = groupMeshInstances[node.render.batchGroupId] = arr.concat(node.render.meshInstances); node.render.removeFromLayers(); } return arr; } _extractModel(node, arr, group, groupMeshInstances) { if (node.model && node.model.model) { arr = groupMeshInstances[node.model.batchGroupId] = arr.concat(node.model.meshInstances); node.model.removeModelFromLayers(); } return arr; } _extractElement(node, arr, group) { if (!node.element) return; let valid = false; if (node.element._text && node.element._text._model.meshInstances.length > 0) { arr.push(node.element._text._model.meshInstances[0]); node.element.removeModelFromLayers(node.element._text._model); valid = true; } else if (node.element._image) { arr.push(node.element._image._renderable.meshInstance); node.element.removeModelFromLayers(node.element._image._renderable.model); if (node.element._image._renderable.unmaskMeshInstance) { arr.push(node.element._image._renderable.unmaskMeshInstance); if (!node.element._image._renderable.unmaskMeshInstance.stencilFront || !node.element._image._renderable.unmaskMeshInstance.stencilBack) { node.element._dirtifyMask(); node.element._onPrerender(); } } valid = true; } if (valid) { group._ui = true; } } _collectAndRemoveMeshInstances(groupMeshInstances, groupIds) { for(let g = 0; g < groupIds.length; g++){ const id = groupIds[g]; const group = this._batchGroups[id]; if (!group) continue; let arr = groupMeshInstances[id]; if (!arr) arr = groupMeshInstances[id] = []; for(let m = 0; m < group._obj.model.length; m++){ arr = this._extractModel(group._obj.model[m], arr, group, groupMeshInstances); } for(let r = 0; r < group._obj.render.length; r++){ arr = this._extractRender(group._obj.render[r], arr, group, groupMeshInstances); } for(let e = 0; e < group._obj.element.length; e++){ this._extractElement(group._obj.element[e], arr, group); } for(let s = 0; s < group._obj.sprite.length; s++){ const node = group._obj.sprite[s]; if (node.sprite && node.sprite._meshInstance && (group.dynamic || node.sprite.sprite._renderMode === SPRITE_RENDERMODE_SIMPLE)) { arr.push(node.sprite._meshInstance); node.sprite.removeModelFromLayers(); group._sprite = true; node.sprite._batchGroup = group; } } } } generate(groupIds) { const groupMeshInstances = {}; if (!groupIds) { groupIds = Object.keys(this._batchGroups); } const newBatchList = []; for(let i = 0; i < this._batchList.length; i++){ if (groupIds.indexOf(this._batchList[i].batchGroupId) < 0) { newBatchList.push(this._batchList[i]); continue; } this.destroyBatch(this._batchList[i]); } this._batchList = newBatchList; this._collectAndRemoveMeshInstances(groupMeshInstances, groupIds); if (groupIds === this._dirtyGroups) { this._dirtyGroups.length = 0; } else { const newDirtyGroups = []; for(let i = 0; i < this._dirtyGroups.length; i++){ if (groupIds.indexOf(this._dirtyGroups[i]) < 0) newDirtyGroups.push(this._dirtyGroups[i]); } this._dirtyGroups = newDirtyGroups; } let group, lists, groupData, batch; for(const groupId in groupMeshInstances){ if (!groupMeshInstances.hasOwnProperty(groupId)) continue; group = groupMeshInstances[groupId]; groupData = this._batchGroups[groupId]; if (!groupData) { continue; } lists = this.prepare(group, groupData.dynamic, groupData.maxAabbSize, groupData._ui || groupData._sprite); for(let i = 0; i < lists.length; i++){ batch = this.create(lists[i], groupData.dynamic, parseInt(groupId, 10)); if (batch) { batch.addToLayers(this.scene, groupData.layers); } } } } prepare(meshInstances, dynamic, maxAabbSize = Number.POSITIVE_INFINITY, translucent) { if (meshInstances.length === 0) return []; const halfMaxAabbSize = maxAabbSize * 0.5; const maxInstanceCount = 1024; const maxNumVertices = 0xffffffff; const aabb = new BoundingBox(); const testAabb = new BoundingBox(); let skipTranslucentAabb = null; let sf; const lists = []; let j = 0; if (translucent) { meshInstances.sort((a, b)=>{ return a.drawOrder - b.drawOrder; }); } let meshInstancesLeftA = meshInstances; let meshInstancesLeftB; const skipMesh = translucent ? function(mi) { if (skipTranslucentAabb) { skipTranslucentAabb.add(mi.aabb); } else { skipTranslucentAabb = mi.aabb.clone(); } meshInstancesLeftB.push(mi); } : function(mi) { meshInstancesLeftB.push(mi); }; while(meshInstancesLeftA.length > 0){ lists[j] = [ meshInstancesLeftA[0] ]; meshInstancesLeftB = []; const material = meshInstancesLeftA[0].material; const layer = meshInstancesLeftA[0].layer; const defs = meshInstancesLeftA[0]._shaderDefs; const params = meshInstancesLeftA[0].parameters; const stencil = meshInstancesLeftA[0].stencilFront; let vertCount = meshInstancesLeftA[0].mesh.vertexBuffer.getNumVertices(); const drawOrder = meshInstancesLeftA[0].drawOrder; aabb.copy(meshInstancesLeftA[0].aabb); const scaleSign = getScaleSign(meshInstancesLeftA[0]); const vertexFormatBatchingHash = meshInstancesLeftA[0].mesh.vertexBuffer.format.batchingHash; const indexed = meshInstancesLeftA[0].mesh.primitive[0].indexed; skipTranslucentAabb = null; for(let i = 1; i < meshInstancesLeftA.length; i++){ const mi = meshInstancesLeftA[i]; if (dynamic && lists[j].length >= maxInstanceCount) { meshInstancesLeftB = meshInstancesLeftB.concat(meshInstancesLeftA.slice(i)); break; } if (material !== mi.material || layer !== mi.layer || vertexFormatBatchingHash !== mi.mesh.vertexBuffer.format.batchingHash || indexed !== mi.mesh.primitive[0].indexed || defs !== mi._shaderDefs || vertCount + mi.mesh.vertexBuffer.getNumVertices() > maxNumVertices) { skipMesh(mi); continue; } testAabb.copy(aabb); testAabb.add(mi.aabb); if (testAabb.halfExtents.x > halfMaxAabbSize || testAabb.halfExtents.y > halfMaxAabbSize || testAabb.halfExtents.z > halfMaxAabbSize) { skipMesh(mi); continue; } if (stencil) { if (!(sf = mi.stencilFront) || stencil.func !== sf.func || stencil.zpass !== sf.zpass) { skipMesh(mi); continue; } } if (scaleSign !== getScaleSign(mi)) { skipMesh(mi); continue; } if (!equalParamSets(params, mi.parameters)) { skipMesh(mi); continue; } if (translucent && skipTranslucentAabb && skipTranslucentAabb.intersects(mi.aabb) && mi.drawOrder !== drawOrder) { skipMesh(mi); continue; } aabb.add(mi.aabb); vertCount += mi.mesh.vertexBuffer.getNumVertices(); lists[j].push(mi); } j++; meshInstancesLeftA = meshInstancesLeftB; } return lists; } collectBatchedMeshData(meshInstances, dynamic) { let streams = null; let batchNumVerts = 0; let batchNumIndices = 0; let material = null; for(let i = 0; i < meshInstances.length; i++){ if (meshInstances[i].visible) { const mesh = meshInstances[i].mesh; const numVerts = mesh.vertexBuffer.numVertices; batchNumVerts += numVerts; if (mesh.primitive[0].indexed) { batchNumIndices += mesh.primitive[0].count; } else { const primitiveType = mesh.primitive[0].type; if (primitiveType === PRIMITIVE_TRIFAN || primitiveType === PRIMITIVE_TRISTRIP) { if (mesh.primitive[0].count === 4) { batchNumIndices += 6; } } } if (!streams) { material = meshInstances[i].material; streams = {}; const elems = mesh.vertexBuffer.format.elements; for(let j = 0; j < elems.length; j++){ const semantic = elems[j].name; streams[semantic] = { numComponents: elems[j].numComponents, dataType: elems[j].dataType, normalize: elems[j].normalize, count: 0 }; } if (dynamic) { streams[SEMANTIC_BLENDINDICES] = { numComponents: 1, dataType: TYPE_FLOAT32, normalize: false, count: 0 }; } } } } return { streams: streams, batchNumVerts: batchNumVerts, batchNumIndices: batchNumIndices, material: material }; } create(meshInstances, dynamic, batchGroupId) { if (!this._init) { this.vertexFormats = {}; this._init = true; } let stream = null; let semantic; let mesh, numVerts; let batch = null; const batchData = this.collectBatchedMeshData(meshInstances, dynamic); if (batchData.streams) { const streams = batchData.streams; let material = batchData.material; const batchNumVerts = batchData.batchNumVerts; const batchNumIndices = batchData.batchNumIndices; batch = new Batch(meshInstances, dynamic, batchGroupId); this._batchList.push(batch); let indexBase, indexBaseVertex, numIndices, indexData; let verticesOffset = 0; let indexOffset = 0; let transform; const indexArrayType = batchNumVerts <= 0xffff ? Uint16Array : Uint32Array; const indices = new indexArrayType(batchNumIndices); for(semantic in streams){ stream = streams[semantic]; stream.typeArrayType = typedArrayTypes[stream.dataType]; stream.elementByteSize = typedArrayTypesByteSize[stream.dataType]; stream.buffer = new stream.typeArrayType(batchNumVerts * stream.numComponents); } for(let i = 0; i < meshInstances.length; i++){ if (!meshInstances[i].visible) { continue; } mesh = meshInstances[i].mesh; numVerts = mesh.vertexBuffer.numVertices; if (!dynamic) { transform = meshInstances[i].node.getWorldTransform(); } for(semantic in streams){ if (semantic !== SEMANTIC_BLENDINDICES) { stream = streams[semantic]; const subarray = new stream.typeArrayType(stream.buffer.buffer, stream.elementByteSize * stream.count); const totalComponents = mesh.getVertexStream(semantic, subarray) * stream.numComponents; stream.count += totalComponents; if (!dynamic && stream.numComponents >= 3) { if (semantic === SEMANTIC_POSITION) { const m = transform.data; const m0 = m[0]; const m1 = m[1]; const m2 = m[2]; const m4 = m[4]; const m5 = m[5]; const m6 = m[6]; const m8 = m[8]; const m9 = m[9]; const m10 = m[10]; const m12 = m[12]; const m13 = m[13]; const m14 = m[14]; let x, y, z; for(let j = 0; j < totalComponents; j += stream.numComponents){ x = subarray[j]; y = subarray[j + 1]; z = subarray[j + 2]; subarray[j] = x * m0 + y * m4 + z * m8 + m12; subarray[j + 1] = x * m1 + y * m5 + z * m9 + m13; subarray[j + 2] = x * m2 + y * m6 + z * m10 + m14; } } else if (semantic === SEMANTIC_NORMAL || semantic === SEMANTIC_TANGENT) { mat3.invertMat4(transform).transpose(); const [m0, m1, m2, m3, m4, m5, m6, m7, m8] = mat3.data; let x, y, z; for(let j = 0; j < totalComponents; j += stream.numComponents){ x = subarray[j]; y = subarray[j + 1]; z = subarray[j + 2]; subarray[j] = x * m0 + y * m3 + z * m6; subarray[j + 1] = x * m1 + y * m4 + z * m7; subarray[j + 2] = x * m2 + y * m5 + z * m8; } } } } } if (dynamic) { stream = streams[SEMANTIC_BLENDINDICES]; for(let j = 0; j < numVerts; j++){ stream.buffer[stream.count++] = i; } } if (mesh.primitive[0].indexed) { indexBase = mesh.primitive[0].base; indexBaseVertex = mesh.primitive[0].baseVertex || 0; numIndices = mesh.primitive[0].count; const srcFormat = mesh.indexBuffer[0].getFormat(); indexData = new typedArrayIndexFormats[srcFormat](mesh.indexBuffer[0].storage); } else { indexBaseVertex = 0; const primitiveType = mesh.primitive[0].type; if (primitiveType === PRIMITIVE_TRIFAN || primitiveType === PRIMITIVE_TRISTRIP) { if (mesh.primitive[0].count === 4) { indexBase = 0; numIndices = 6; indexData = primitiveType === PRIMITIVE_TRIFAN ? _triFanIndices : _triStripIndices; } else { numIndices = 0; continue; } } } for(let j = 0; j < numIndices; j++){ indices[j + indexOffset] = indexData[indexBase + j] + indexBaseVertex + verticesOffset; } indexOffset += numIndices; verticesOffset += numVerts; } mesh = new Mesh(this.device); for(semantic in streams){ stream = streams[semantic]; mesh.setVertexStream(semantic, stream.buffer, stream.numComponents, undefined, stream.dataType, stream.normalize); } if (indices.length > 0) { mesh.setIndices(indices); } mesh.update(PRIMITIVE_TRIANGLES, false); if (dynamic) { material = material.clone(); material.update(); } const meshInstance = new MeshInstance(mesh, material, this.rootNode); meshInstance.castShadow = batch.origMeshInstances[0].castShadow; meshInstance.parameters = batch.origMeshInstances[0].parameters; meshInstance.layer = batch.origMeshInstances[0].layer; meshInstance._shaderDefs = batch.origMeshInstances[0]._shaderDefs; meshInstance.batching = true; meshInstance.cull = batch.origMeshInstances[0].cull; const batchGroup = this._batchGroups[batchGroupId]; if (batchGroup && batchGroup._ui) { meshInstance.cull = false; } if (dynamic) { const nodes = []; for(let i = 0; i < batch.origMeshInstances.length; i++){ nodes.push(batch.origMeshInstances[i].node); } meshInstance.skinInstance = new SkinBatchInstance(this.device, nodes, this.rootNode); } meshInstance._updateAabb = false; meshInstance.drawOrder = batch.origMeshInstances[0].drawOrder; meshInstance.stencilFront = batch.origMeshInstances[0].stencilFront; meshInstance.stencilBack = batch.origMeshInstances[0].stencilBack; meshInstance.flipFacesFactor = getScaleSign(batch.origMeshInstances[0]); meshInstance.castShadow = batch.origMeshInstances[0].castShadow; batch.meshInstance = meshInstance; batch.updateBoundingBox(); } return batch; } updateAll() { if (this._dirtyGroups.length > 0) { this.generate(this._dirtyGroups); } for(let i = 0; i < this._batchList.length; i++){ if (!this._batchList[i].dynamic) continue; this._batchList[i].updateBoundingBox(); } } clone(batch, clonedMeshInstances) { const batch2 = new Batch(clonedMeshInstances, batch.dynamic, batch.batchGroupId); this._batchList.push(batch2); const nodes = []; for(let i = 0; i < clonedMeshInstances.length; i++){ nodes.push(clonedMeshInstances[i].node); } batch2.meshInstance = new MeshInstance(batch.meshInstance.mesh, batch.meshInstance.material, batch.meshInstance.node); batch2.meshInstance._updateAabb = false; batch2.meshInstance.parameters = clonedMeshInstances[0].parameters; batch2.meshInstance.cull = clonedMeshInstances[0].cull; batch2.meshInstance.layer = clonedMeshInstances[0].layer; if (batch.dynamic) { batch2.meshInstance.skinInstance = new SkinBatchInstance(this.device, nodes, this.rootNode); } batch2.meshInstance.castShadow = batch.meshInstance.castShadow; return batch2; } destroyBatch(batch) { batch.destroy(this.scene, this._batchGroups[batch.batchGroupId].layers); } constructor(device, root, scene){ this.device = device; this.rootNode = root; this.scene = scene; this._init = false; this._batchGroups = {}; this._batchGroupCounter = 0; this._batchList = []; this._dirtyGroups = []; } } const _colorUniformName = 'uSceneColorMap'; class RenderPassColorGrab extends RenderPass { destroy() { super.destroy(); this.releaseRenderTarget(this.colorRenderTarget); } shouldReallocate(targetRT, sourceTexture, sourceFormat) { const targetFormat = targetRT?.colorBuffer.format; if (targetFormat !== sourceFormat) { return true; } const width = sourceTexture?.width || this.device.width; const height = sourceTexture?.height || this.device.height; return !targetRT || width !== targetRT.width || height !== targetRT.height; } allocateRenderTarget(renderTarget, sourceRenderTarget, device, format) { const texture = new Texture(device, { name: _colorUniformName, format, width: sourceRenderTarget ? sourceRenderTarget.colorBuffer.width : device.width, height: sourceRenderTarget ? sourceRenderTarget.colorBuffer.height : device.height, mipmaps: true, minFilter: FILTER_LINEAR_MIPMAP_LINEAR, magFilter: FILTER_LINEAR, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); if (renderTarget) { renderTarget.destroyFrameBuffers(); renderTarget._colorBuffer = texture; renderTarget._colorBuffers = [ texture ]; renderTarget.evaluateDimensions(); } else { renderTarget = new RenderTarget({ name: 'ColorGrabRT', colorBuffer: texture, depth: false, stencil: false, autoResolve: false }); } return renderTarget; } releaseRenderTarget(rt) { if (rt) { rt.destroyTextureBuffers(); rt.destroy(); } } frameUpdate() { const device = this.device; const sourceRt = this.source; const sourceFormat = sourceRt?.colorBuffer.format ?? this.device.backBufferFormat; if (this.shouldReallocate(this.colorRenderTarget, sourceRt?.colorBuffer, sourceFormat)) { this.releaseRenderTarget(this.colorRenderTarget); this.colorRenderTarget = this.allocateRenderTarget(this.colorRenderTarget, sourceRt, device, sourceFormat); } const colorBuffer = this.colorRenderTarget.colorBuffer; device.scope.resolve(_colorUniformName).setValue(colorBuffer); } execute() { const device = this.device; const sourceRt = this.source; const colorBuffer = this.colorRenderTarget.colorBuffer; if (device.isWebGPU) { device.copyRenderTarget(sourceRt, this.colorRenderTarget, true, false); device.mipmapRenderer.generate(this.colorRenderTarget.colorBuffer.impl); } else { device.copyRenderTarget(sourceRt, this.colorRenderTarget, true, false); device.activeTexture(device.maxCombinedTextures - 1); device.bindTexture(colorBuffer); device.gl.generateMipmap(colorBuffer.impl._glTarget); } } constructor(...args){ super(...args), this.colorRenderTarget = null, this.source = null; } } const _depthUniformName = 'uSceneDepthMap'; class RenderPassDepthGrab extends RenderPass { destroy() { super.destroy(); this.releaseRenderTarget(this.depthRenderTarget); } shouldReallocate(targetRT, sourceTexture) { const width = sourceTexture?.width || this.device.width; const height = sourceTexture?.height || this.device.height; return !targetRT || width !== targetRT.width || height !== targetRT.height; } allocateRenderTarget(renderTarget, sourceRenderTarget, device, format, isDepth) { const texture = new Texture(device, { name: _depthUniformName, format, width: sourceRenderTarget ? sourceRenderTarget.colorBuffer.width : device.width, height: sourceRenderTarget ? sourceRenderTarget.colorBuffer.height : device.height, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); if (renderTarget) { renderTarget.destroyFrameBuffers(); if (isDepth) { renderTarget._depthBuffer = texture; } else { renderTarget._colorBuffer = texture; renderTarget._colorBuffers = [ texture ]; } renderTarget.evaluateDimensions(); } else { renderTarget = new RenderTarget({ name: 'DepthGrabRT', colorBuffer: isDepth ? null : texture, depthBuffer: isDepth ? texture : null, depth: !isDepth, stencil: device.supportsStencil, autoResolve: false }); } return renderTarget; } releaseRenderTarget(rt) { if (rt) { rt.destroyTextureBuffers(); rt.destroy(); } } before() { const camera = this.camera; const device = this.device; const destinationRt = camera?.renderTarget ?? device.backBuffer; let useDepthBuffer = true; let format = destinationRt.stencil ? PIXELFORMAT_DEPTHSTENCIL : PIXELFORMAT_DEPTH; if (device.isWebGPU) { const numSamples = destinationRt.samples; if (numSamples > 1) { format = PIXELFORMAT_R32F; useDepthBuffer = false; } } const sourceTexture = camera.renderTarget?.depthBuffer ?? camera.renderTarget?.colorBuffer; if (this.shouldReallocate(this.depthRenderTarget, sourceTexture)) { this.releaseRenderTarget(this.depthRenderTarget); this.depthRenderTarget = this.allocateRenderTarget(this.depthRenderTarget, camera.renderTarget, device, format, useDepthBuffer); } const colorBuffer = useDepthBuffer ? this.depthRenderTarget.depthBuffer : this.depthRenderTarget.colorBuffer; device.scope.resolve(_depthUniformName).setValue(colorBuffer); } execute() { const device = this.device; if (device.isWebGL2 && device.renderTarget.samples > 1) { const src = device.renderTarget.impl._glFrameBuffer; const dest = this.depthRenderTarget; device.renderTarget = dest; device.updateBegin(); this.depthRenderTarget.impl.internalResolve(device, src, dest.impl._glFrameBuffer, this.depthRenderTarget, device.gl.DEPTH_BUFFER_BIT); } else { device.copyRenderTarget(device.renderTarget, this.depthRenderTarget, false, true); } } constructor(device, camera){ super(device), this.depthRenderTarget = null, this.camera = null; this.camera = camera; } } class CameraShaderParams { get hash() { if (this._hash === undefined) { const key = `${this.gammaCorrection}_${this.toneMapping}_${this.srgbRenderTarget}_${this.fog}_${this.ssaoEnabled}_${this.sceneDepthMapLinear}`; this._hash = hashCode(key); } return this._hash; } get defines() { const defines = this._defines; if (this._definesDirty) { this._definesDirty = false; defines.clear(); if (this._sceneDepthMapLinear) defines.set('SCENE_DEPTHMAP_LINEAR', ''); if (this.shaderOutputGamma === GAMMA_SRGB) defines.set('SCENE_COLORMAP_GAMMA', ''); defines.set('FOG', this._fog.toUpperCase()); defines.set('TONEMAP', tonemapNames[this._toneMapping]); defines.set('GAMMA', gammaNames[this.shaderOutputGamma]); } return defines; } markDirty() { this._hash = undefined; this._definesDirty = true; } set fog(type) { if (this._fog !== type) { this._fog = type; this.markDirty(); } } get fog() { return this._fog; } set ssaoEnabled(value) { if (this._ssaoEnabled !== value) { this._ssaoEnabled = value; this.markDirty(); } } get ssaoEnabled() { return this._ssaoEnabled; } set gammaCorrection(value) { this._gammaCorrectionAssigned = true; if (this._gammaCorrection !== value) { this._gammaCorrection = value; this.markDirty(); } } get gammaCorrection() { return this._gammaCorrection; } set toneMapping(value) { if (this._toneMapping !== value) { this._toneMapping = value; this.markDirty(); } } get toneMapping() { return this._toneMapping; } set srgbRenderTarget(value) { if (this._srgbRenderTarget !== value) { this._srgbRenderTarget = value; this.markDirty(); } } get srgbRenderTarget() { return this._srgbRenderTarget; } set sceneDepthMapLinear(value) { if (this._sceneDepthMapLinear !== value) { this._sceneDepthMapLinear = value; this.markDirty(); } } get sceneDepthMapLinear() { return this._sceneDepthMapLinear; } get shaderOutputGamma() { const gammaOutput = this._gammaCorrection === GAMMA_SRGB && !this._srgbRenderTarget; return gammaOutput ? GAMMA_SRGB : GAMMA_NONE; } constructor(){ this._gammaCorrection = GAMMA_SRGB; this._toneMapping = TONEMAP_LINEAR; this._srgbRenderTarget = false; this._ssaoEnabled = false; this._fog = FOG_NONE; this._sceneDepthMapLinear = false; this._defines = new Map(); this._definesDirty = true; } } const _deviceCoord = new Vec3(); const _halfSize = new Vec3(); const _point$1 = new Vec3(); const _invViewProjMat = new Mat4(); const _frustumPoints = [ new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3() ]; let Camera$1 = class Camera { destroy() { this.renderPassColorGrab?.destroy(); this.renderPassColorGrab = null; this.renderPassDepthGrab?.destroy(); this.renderPassDepthGrab = null; this.renderPasses.length = 0; } _storeShaderMatrices(viewProjMat, jitterX, jitterY, renderVersion) { if (this._shaderMatricesVersion !== renderVersion) { this._shaderMatricesVersion = renderVersion; this._viewProjPrevious.copy(this._viewProjCurrent ?? viewProjMat); this._viewProjCurrent ?? (this._viewProjCurrent = new Mat4()); this._viewProjCurrent.copy(viewProjMat); this._viewProjInverse.invert(viewProjMat); this._jitters[2] = this._jitters[0]; this._jitters[3] = this._jitters[1]; this._jitters[0] = jitterX; this._jitters[1] = jitterY; } } get fullSizeClearRect() { const rect = this._scissorRectClear ? this.scissorRect : this._rect; return rect.x === 0 && rect.y === 0 && rect.z === 1 && rect.w === 1; } set aspectRatio(newValue) { if (this._aspectRatio !== newValue) { this._aspectRatio = newValue; this._projMatDirty = true; } } get aspectRatio() { return this.xr?.active ? this._xrProperties.aspectRatio : this._aspectRatio; } set aspectRatioMode(newValue) { if (this._aspectRatioMode !== newValue) { this._aspectRatioMode = newValue; this._projMatDirty = true; } } get aspectRatioMode() { return this._aspectRatioMode; } set calculateProjection(newValue) { this._calculateProjection = newValue; this._projMatDirty = true; } get calculateProjection() { return this._calculateProjection; } set calculateTransform(newValue) { this._calculateTransform = newValue; } get calculateTransform() { return this._calculateTransform; } set clearColor(newValue) { this._clearColor.copy(newValue); } get clearColor() { return this._clearColor; } set clearColorBuffer(newValue) { this._clearColorBuffer = newValue; } get clearColorBuffer() { return this._clearColorBuffer; } set clearDepth(newValue) { this._clearDepth = newValue; } get clearDepth() { return this._clearDepth; } set clearDepthBuffer(newValue) { this._clearDepthBuffer = newValue; } get clearDepthBuffer() { return this._clearDepthBuffer; } set clearStencil(newValue) { this._clearStencil = newValue; } get clearStencil() { return this._clearStencil; } set clearStencilBuffer(newValue) { this._clearStencilBuffer = newValue; } get clearStencilBuffer() { return this._clearStencilBuffer; } set cullFaces(newValue) { this._cullFaces = newValue; } get cullFaces() { return this._cullFaces; } set farClip(newValue) { if (this._farClip !== newValue) { this._farClip = newValue; this._projMatDirty = true; } } get farClip() { return this.xr?.active ? this._xrProperties.farClip : this._farClip; } set flipFaces(newValue) { this._flipFaces = newValue; } get flipFaces() { return this._flipFaces; } set fov(newValue) { if (this._fov !== newValue) { this._fov = newValue; this._projMatDirty = true; } } get fov() { return this.xr?.active ? this._xrProperties.fov : this._fov; } set frustumCulling(newValue) { this._frustumCulling = newValue; } get frustumCulling() { return this._frustumCulling; } set horizontalFov(newValue) { if (this._horizontalFov !== newValue) { this._horizontalFov = newValue; this._projMatDirty = true; } } get horizontalFov() { return this.xr?.active ? this._xrProperties.horizontalFov : this._horizontalFov; } set layers(newValue) { this._layers = newValue.slice(0); this._layersSet = new Set(this._layers); } get layers() { return this._layers; } get layersSet() { return this._layersSet; } set nearClip(newValue) { if (this._nearClip !== newValue) { this._nearClip = newValue; this._projMatDirty = true; } } get nearClip() { return this.xr?.active ? this._xrProperties.nearClip : this._nearClip; } set node(newValue) { this._node = newValue; } get node() { return this._node; } set orthoHeight(newValue) { if (this._orthoHeight !== newValue) { this._orthoHeight = newValue; this._projMatDirty = true; } } get orthoHeight() { return this._orthoHeight; } set projection(newValue) { if (this._projection !== newValue) { this._projection = newValue; this._projMatDirty = true; } } get projection() { return this._projection; } get projectionMatrix() { this._evaluateProjectionMatrix(); return this._projMat; } set rect(newValue) { this._rect.copy(newValue); } get rect() { return this._rect; } set renderTarget(newValue) { this._renderTarget = newValue; } get renderTarget() { return this._renderTarget; } set scissorRect(newValue) { this._scissorRect.copy(newValue); } get scissorRect() { return this._scissorRect; } get viewMatrix() { if (this._viewMatDirty) { const wtm = this._node.getWorldTransform(); this._viewMat.copy(wtm).invert(); this._viewMatDirty = false; } return this._viewMat; } set aperture(newValue) { this._aperture = newValue; } get aperture() { return this._aperture; } set sensitivity(newValue) { this._sensitivity = newValue; } get sensitivity() { return this._sensitivity; } set shutter(newValue) { this._shutter = newValue; } get shutter() { return this._shutter; } set xr(newValue) { if (this._xr !== newValue) { this._xr = newValue; this._projMatDirty = true; } } get xr() { return this._xr; } clone() { return new Camera().copy(this); } copy(other) { this._aspectRatio = other._aspectRatio; this._farClip = other._farClip; this._fov = other._fov; this._horizontalFov = other._horizontalFov; this._nearClip = other._nearClip; this._xrProperties.aspectRatio = other._xrProperties.aspectRatio; this._xrProperties.farClip = other._xrProperties.farClip; this._xrProperties.fov = other._xrProperties.fov; this._xrProperties.horizontalFov = other._xrProperties.horizontalFov; this._xrProperties.nearClip = other._xrProperties.nearClip; this.aspectRatioMode = other.aspectRatioMode; this.calculateProjection = other.calculateProjection; this.calculateTransform = other.calculateTransform; this.clearColor = other.clearColor; this.clearColorBuffer = other.clearColorBuffer; this.clearDepth = other.clearDepth; this.clearDepthBuffer = other.clearDepthBuffer; this.clearStencil = other.clearStencil; this.clearStencilBuffer = other.clearStencilBuffer; this.cullFaces = other.cullFaces; this.flipFaces = other.flipFaces; this.frustumCulling = other.frustumCulling; this.layers = other.layers; this.orthoHeight = other.orthoHeight; this.projection = other.projection; this.rect = other.rect; this.renderTarget = other.renderTarget; this.scissorRect = other.scissorRect; this.aperture = other.aperture; this.shutter = other.shutter; this.sensitivity = other.sensitivity; this.shaderPassInfo = other.shaderPassInfo; this.jitter = other.jitter; this._projMatDirty = true; return this; } _enableRenderPassColorGrab(device, enable) { if (enable) { if (!this.renderPassColorGrab) { this.renderPassColorGrab = new RenderPassColorGrab(device); } } else { this.renderPassColorGrab?.destroy(); this.renderPassColorGrab = null; } } _enableRenderPassDepthGrab(device, renderer, enable) { if (enable) { if (!this.renderPassDepthGrab) { this.renderPassDepthGrab = new RenderPassDepthGrab(device, this); } } else { this.renderPassDepthGrab?.destroy(); this.renderPassDepthGrab = null; } } _updateViewProjMat() { if (this._projMatDirty || this._viewMatDirty || this._viewProjMatDirty) { this._viewProjMat.mul2(this.projectionMatrix, this.viewMatrix); this._viewProjMatDirty = false; } } worldToScreen(worldCoord, cw, ch, screenCoord = new Vec3()) { this._updateViewProjMat(); this._viewProjMat.transformPoint(worldCoord, screenCoord); const vpm = this._viewProjMat.data; const w = worldCoord.x * vpm[3] + worldCoord.y * vpm[7] + worldCoord.z * vpm[11] + 1 * vpm[15]; screenCoord.x = (screenCoord.x / w + 1) * 0.5; screenCoord.y = (1 - screenCoord.y / w) * 0.5; const { x: rx, y: ry, z: rw, w: rh } = this._rect; screenCoord.x = screenCoord.x * rw * cw + rx * cw; screenCoord.y = screenCoord.y * rh * ch + (1 - ry - rh) * ch; return screenCoord; } screenToWorld(x, y, z, cw, ch, worldCoord = new Vec3()) { const { x: rx, y: ry, z: rw, w: rh } = this._rect; const range = this.farClip - this.nearClip; _deviceCoord.set((x - rx * cw) / (rw * cw), 1 - (y - (1 - ry - rh) * ch) / (rh * ch), z / range); _deviceCoord.mulScalar(2); _deviceCoord.sub(Vec3.ONE); if (this._projection === PROJECTION_PERSPECTIVE) { Mat4._getPerspectiveHalfSize(_halfSize, this.fov, this.aspectRatio, this.nearClip, this.horizontalFov); _halfSize.x *= _deviceCoord.x; _halfSize.y *= _deviceCoord.y; const invView = this._node.getWorldTransform(); _halfSize.z = -this.nearClip; invView.transformPoint(_halfSize, _point$1); const cameraPos = this._node.getPosition(); worldCoord.sub2(_point$1, cameraPos); worldCoord.normalize(); worldCoord.mulScalar(z); worldCoord.add(cameraPos); } else { this._updateViewProjMat(); _invViewProjMat.copy(this._viewProjMat).invert(); _invViewProjMat.transformPoint(_deviceCoord, worldCoord); } return worldCoord; } _evaluateProjectionMatrix() { if (this._projMatDirty) { if (this._projection === PROJECTION_PERSPECTIVE) { this._projMat.setPerspective(this.fov, this.aspectRatio, this.nearClip, this.farClip, this.horizontalFov); this._projMatSkybox.copy(this._projMat); } else { const y = this._orthoHeight; const x = y * this.aspectRatio; this._projMat.setOrtho(-x, x, -y, y, this.nearClip, this.farClip); this._projMatSkybox.setPerspective(this.fov, this.aspectRatio, this.nearClip, this.farClip); } this._projMatDirty = false; } } getProjectionMatrixSkybox() { this._evaluateProjectionMatrix(); return this._projMatSkybox; } getExposure() { const ev100 = Math.log2(this._aperture * this._aperture / this._shutter * 100.0 / this._sensitivity); return 1.0 / (Math.pow(2.0, ev100) * 1.2); } getScreenSize(sphere) { if (this._projection === PROJECTION_PERSPECTIVE) { const distance = this._node.getPosition().distance(sphere.center); if (distance < sphere.radius) { return 1; } const viewAngle = Math.asin(sphere.radius / distance); const sphereViewHeight = Math.tan(viewAngle); const screenViewHeight = Math.tan(this.fov / 2 * math.DEG_TO_RAD); return Math.min(sphereViewHeight / screenViewHeight, 1); } return math.clamp(sphere.radius / this._orthoHeight, 0, 1); } getFrustumCorners(near = this.nearClip, far = this.farClip) { const fov = this.fov * math.DEG_TO_RAD; let x, y; if (this.projection === PROJECTION_PERSPECTIVE) { if (this.horizontalFov) { x = near * Math.tan(fov / 2.0); y = x / this.aspectRatio; } else { y = near * Math.tan(fov / 2.0); x = y * this.aspectRatio; } } else { y = this._orthoHeight; x = y * this.aspectRatio; } const points = _frustumPoints; points[0].x = x; points[0].y = -y; points[0].z = -near; points[1].x = x; points[1].y = y; points[1].z = -near; points[2].x = -x; points[2].y = y; points[2].z = -near; points[3].x = -x; points[3].y = -y; points[3].z = -near; if (this._projection === PROJECTION_PERSPECTIVE) { if (this.horizontalFov) { x = far * Math.tan(fov / 2.0); y = x / this.aspectRatio; } else { y = far * Math.tan(fov / 2.0); x = y * this.aspectRatio; } } points[4].x = x; points[4].y = -y; points[4].z = -far; points[5].x = x; points[5].y = y; points[5].z = -far; points[6].x = -x; points[6].y = y; points[6].z = -far; points[7].x = -x; points[7].y = -y; points[7].z = -far; return points; } setXrProperties(properties) { Object.assign(this._xrProperties, properties); this._projMatDirty = true; } fillShaderParams(output) { const f = this._farClip; output[0] = 1 / f; output[1] = f; output[2] = this._nearClip; output[3] = this._projection === PROJECTION_ORTHOGRAPHIC ? 1 : 0; return output; } constructor(){ this.shaderPassInfo = null; this.renderPassColorGrab = null; this.renderPassDepthGrab = null; this.fogParams = null; this.shaderParams = new CameraShaderParams(); this.renderPasses = []; this.jitter = 0; this._aspectRatio = 16 / 9; this._aspectRatioMode = ASPECT_AUTO; this._calculateProjection = null; this._calculateTransform = null; this._clearColor = new Color(0.75, 0.75, 0.75, 1); this._clearColorBuffer = true; this._clearDepth = 1; this._clearDepthBuffer = true; this._clearStencil = 0; this._clearStencilBuffer = true; this._cullFaces = true; this._farClip = 1000; this._flipFaces = false; this._fov = 45; this._frustumCulling = true; this._horizontalFov = false; this._layers = [ LAYERID_WORLD, LAYERID_DEPTH, LAYERID_SKYBOX, LAYERID_UI, LAYERID_IMMEDIATE ]; this._layersSet = new Set(this._layers); this._nearClip = 0.1; this._node = null; this._orthoHeight = 10; this._projection = PROJECTION_PERSPECTIVE; this._rect = new Vec4(0, 0, 1, 1); this._renderTarget = null; this._scissorRect = new Vec4(0, 0, 1, 1); this._scissorRectClear = false; this._aperture = 16.0; this._shutter = 1.0 / 1000.0; this._sensitivity = 1000; this._projMat = new Mat4(); this._projMatDirty = true; this._projMatSkybox = new Mat4(); this._viewMat = new Mat4(); this._viewMatDirty = true; this._viewProjMat = new Mat4(); this._viewProjMatDirty = true; this._shaderMatricesVersion = 0; this._viewProjInverse = new Mat4(); this._viewProjCurrent = null; this._viewProjPrevious = new Mat4(); this._jitters = [ 0, 0, 0, 0 ]; this.frustum = new Frustum(); this._xr = null; this._xrProperties = { horizontalFov: this._horizontalFov, fov: this._fov, aspectRatio: this._aspectRatio, farClip: this._farClip, nearClip: this._nearClip }; } }; const scaleCompensatePosTransform = new Mat4(); const scaleCompensatePos = new Vec3(); const scaleCompensateRot = new Quat(); const scaleCompensateRot2 = new Quat(); const scaleCompensateScale = new Vec3(); const scaleCompensateScaleForParent = new Vec3(); const tmpMat4 = new Mat4(); const tmpQuat = new Quat(); const position$1 = new Vec3(); const invParentWtm$1 = new Mat4(); const rotation$4 = new Quat(); const invParentRot = new Quat(); const matrix = new Mat4(); const target = new Vec3(); const up$1 = new Vec3(); function createTest(attr, value) { if (attr instanceof Function) { return attr; } return (node)=>{ let x = node[attr]; if (x instanceof Function) { x = x(); } return x === value; }; } function findNode(node, test) { if (test(node)) { return node; } const children = node._children; const len = children.length; for(let i = 0; i < len; ++i){ const result = findNode(children[i], test); if (result) { return result; } } return null; } class GraphNode extends EventHandler { get right() { if (!this._right) { this._right = new Vec3(); } return this.getWorldTransform().getX(this._right).normalize(); } get up() { if (!this._up) { this._up = new Vec3(); } return this.getWorldTransform().getY(this._up).normalize(); } get forward() { if (!this._forward) { this._forward = new Vec3(); } return this.getWorldTransform().getZ(this._forward).normalize().mulScalar(-1); } get normalMatrix() { const normalMat = this._normalMatrix; if (this._dirtyNormal) { normalMat.invertMat4(this.getWorldTransform()).transpose(); this._dirtyNormal = false; } return normalMat; } set enabled(enabled) { if (this._enabled !== enabled) { this._enabled = enabled; if (enabled && this._parent?.enabled || !enabled) { this._notifyHierarchyStateChanged(this, enabled); } } } get enabled() { return this._enabled && this._enabledInHierarchy; } get parent() { return this._parent; } get path() { let node = this._parent; if (!node) { return ''; } let result = this.name; while(node && node._parent){ result = `${node.name}/${result}`; node = node._parent; } return result; } get root() { let result = this; while(result._parent){ result = result._parent; } return result; } get children() { return this._children; } get graphDepth() { return this._graphDepth; } _notifyHierarchyStateChanged(node, enabled) { node._onHierarchyStateChanged(enabled); const c = node._children; for(let i = 0, len = c.length; i < len; i++){ if (c[i]._enabled) { this._notifyHierarchyStateChanged(c[i], enabled); } } } _onHierarchyStateChanged(enabled) { this._enabledInHierarchy = enabled; if (enabled && !this._frozen) { this._unfreezeParentToRoot(); } } _cloneInternal(clone) { clone.name = this.name; const tags = this.tags._list; clone.tags.clear(); for(let i = 0; i < tags.length; i++){ clone.tags.add(tags[i]); } clone.localPosition.copy(this.localPosition); clone.localRotation.copy(this.localRotation); clone.localScale.copy(this.localScale); clone.localEulerAngles.copy(this.localEulerAngles); clone.position.copy(this.position); clone.rotation.copy(this.rotation); clone.eulerAngles.copy(this.eulerAngles); clone.localTransform.copy(this.localTransform); clone._dirtyLocal = this._dirtyLocal; clone.worldTransform.copy(this.worldTransform); clone._dirtyWorld = this._dirtyWorld; clone._dirtyNormal = this._dirtyNormal; clone._aabbVer = this._aabbVer + 1; clone._enabled = this._enabled; clone.scaleCompensation = this.scaleCompensation; clone._enabledInHierarchy = false; } clone() { const clone = new this.constructor(); this._cloneInternal(clone); return clone; } copy(source) { source._cloneInternal(this); return this; } destroy() { this.remove(); const children = this._children; while(children.length){ const child = children.pop(); child._parent = null; child.destroy(); } this.fire('destroy', this); this.off(); } find(attr, value) { const results = []; const test = createTest(attr, value); this.forEach((node)=>{ if (test(node)) { results.push(node); } }); return results; } findOne(attr, value) { const test = createTest(attr, value); return findNode(this, test); } findByTag(...query) { const results = []; const queryNode = (node, checkNode)=>{ if (checkNode && node.tags.has(...query)) { results.push(node); } for(let i = 0; i < node._children.length; i++){ queryNode(node._children[i], true); } }; queryNode(this, false); return results; } findByName(name) { return this.findOne('name', name); } findByPath(path) { const parts = Array.isArray(path) ? path : path.split('/'); let result = this; for(let i = 0, imax = parts.length; i < imax; ++i){ result = result.children.find((c)=>c.name === parts[i]); if (!result) { return null; } } return result; } forEach(callback, thisArg) { callback.call(thisArg, this); const children = this._children; const len = children.length; for(let i = 0; i < len; ++i){ children[i].forEach(callback, thisArg); } } isDescendantOf(node) { let parent = this._parent; while(parent){ if (parent === node) { return true; } parent = parent._parent; } return false; } isAncestorOf(node) { return node.isDescendantOf(this); } getEulerAngles() { this.getWorldTransform().getEulerAngles(this.eulerAngles); return this.eulerAngles; } getLocalEulerAngles() { this.localRotation.getEulerAngles(this.localEulerAngles); return this.localEulerAngles; } getLocalPosition() { return this.localPosition; } getLocalRotation() { return this.localRotation; } getLocalScale() { return this.localScale; } getLocalTransform() { if (this._dirtyLocal) { this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale); this._dirtyLocal = false; } return this.localTransform; } getPosition() { this.getWorldTransform().getTranslation(this.position); return this.position; } getRotation() { this.rotation.setFromMat4(this.getWorldTransform()); return this.rotation; } getScale() { if (!this._scale) { this._scale = new Vec3(); } return this.getWorldTransform().getScale(this._scale); } getWorldTransform() { if (!this._dirtyLocal && !this._dirtyWorld) { return this.worldTransform; } if (this._parent) { this._parent.getWorldTransform(); } this._sync(); return this.worldTransform; } get worldScaleSign() { if (this._worldScaleSign === 0) { this._worldScaleSign = this.getWorldTransform().scaleSign; } return this._worldScaleSign; } remove() { this._parent?.removeChild(this); } reparent(parent, index) { this.remove(); if (parent) { if (index >= 0) { parent.insertChild(this, index); } else { parent.addChild(this); } } } setLocalEulerAngles(x, y, z) { this.localRotation.setFromEulerAngles(x, y, z); if (!this._dirtyLocal) { this._dirtifyLocal(); } } setLocalPosition(x, y, z) { if (x instanceof Vec3) { this.localPosition.copy(x); } else { this.localPosition.set(x, y, z); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } setLocalRotation(x, y, z, w) { if (x instanceof Quat) { this.localRotation.copy(x); } else { this.localRotation.set(x, y, z, w); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } setLocalScale(x, y, z) { if (x instanceof Vec3) { this.localScale.copy(x); } else { this.localScale.set(x, y, z); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } _dirtifyLocal() { if (!this._dirtyLocal) { this._dirtyLocal = true; if (!this._dirtyWorld) { this._dirtifyWorld(); } } } _unfreezeParentToRoot() { let p = this._parent; while(p){ p._frozen = false; p = p._parent; } } _dirtifyWorld() { if (!this._dirtyWorld) { this._unfreezeParentToRoot(); } this._dirtifyWorldInternal(); } _dirtifyWorldInternal() { if (!this._dirtyWorld) { this._frozen = false; this._dirtyWorld = true; for(let i = 0; i < this._children.length; i++){ if (!this._children[i]._dirtyWorld) { this._children[i]._dirtifyWorldInternal(); } } } this._dirtyNormal = true; this._worldScaleSign = 0; this._aabbVer++; } setPosition(x, y, z) { if (x instanceof Vec3) { position$1.copy(x); } else { position$1.set(x, y, z); } if (this._parent === null) { this.localPosition.copy(position$1); } else { invParentWtm$1.copy(this._parent.getWorldTransform()).invert(); invParentWtm$1.transformPoint(position$1, this.localPosition); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } setRotation(x, y, z, w) { if (x instanceof Quat) { rotation$4.copy(x); } else { rotation$4.set(x, y, z, w); } if (this._parent === null) { this.localRotation.copy(rotation$4); } else { const parentRot = this._parent.getRotation(); invParentRot.copy(parentRot).invert(); this.localRotation.copy(invParentRot).mul(rotation$4); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } setPositionAndRotation(position, rotation) { if (this._parent === null) { this.localPosition.copy(position); this.localRotation.copy(rotation); } else { const parentWtm = this._parent.getWorldTransform(); invParentWtm$1.copy(parentWtm).invert(); invParentWtm$1.transformPoint(position, this.localPosition); this.localRotation.setFromMat4(invParentWtm$1).mul(rotation); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } setEulerAngles(x, y, z) { this.localRotation.setFromEulerAngles(x, y, z); if (this._parent !== null) { const parentRot = this._parent.getRotation(); invParentRot.copy(parentRot).invert(); this.localRotation.mul2(invParentRot, this.localRotation); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } addChild(node) { this._prepareInsertChild(node); this._children.push(node); this._onInsertChild(node); } addChildAndSaveTransform(node) { const wPos = node.getPosition(); const wRot = node.getRotation(); this._prepareInsertChild(node); node.setPosition(tmpMat4.copy(this.worldTransform).invert().transformPoint(wPos)); node.setRotation(tmpQuat.copy(this.getRotation()).invert().mul(wRot)); this._children.push(node); this._onInsertChild(node); } insertChild(node, index) { this._prepareInsertChild(node); this._children.splice(index, 0, node); this._onInsertChild(node); } _prepareInsertChild(node) { node.remove(); } _fireOnHierarchy(name, nameHierarchy, parent) { this.fire(name, parent); for(let i = 0; i < this._children.length; i++){ this._children[i]._fireOnHierarchy(nameHierarchy, nameHierarchy, parent); } } _onInsertChild(node) { node._parent = this; const enabledInHierarchy = node._enabled && this.enabled; if (node._enabledInHierarchy !== enabledInHierarchy) { node._enabledInHierarchy = enabledInHierarchy; node._notifyHierarchyStateChanged(node, enabledInHierarchy); } node._updateGraphDepth(); node._dirtifyWorld(); if (this._frozen) { node._unfreezeParentToRoot(); } node._fireOnHierarchy('insert', 'inserthierarchy', this); if (this.fire) this.fire('childinsert', node); } _updateGraphDepth() { this._graphDepth = this._parent ? this._parent._graphDepth + 1 : 0; for(let i = 0, len = this._children.length; i < len; i++){ this._children[i]._updateGraphDepth(); } } removeChild(child) { const index = this._children.indexOf(child); if (index === -1) { return; } this._children.splice(index, 1); child._parent = null; child._fireOnHierarchy('remove', 'removehierarchy', this); this.fire('childremove', child); } _sync() { if (this._dirtyLocal) { this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale); this._dirtyLocal = false; } if (this._dirtyWorld) { if (this._parent === null) { this.worldTransform.copy(this.localTransform); } else { if (this.scaleCompensation) { let parentWorldScale; const parent = this._parent; let scale = this.localScale; let parentToUseScaleFrom = parent; if (parentToUseScaleFrom) { while(parentToUseScaleFrom && parentToUseScaleFrom.scaleCompensation){ parentToUseScaleFrom = parentToUseScaleFrom._parent; } if (parentToUseScaleFrom) { parentToUseScaleFrom = parentToUseScaleFrom._parent; if (parentToUseScaleFrom) { parentWorldScale = parentToUseScaleFrom.worldTransform.getScale(); scaleCompensateScale.mul2(parentWorldScale, this.localScale); scale = scaleCompensateScale; } } } scaleCompensateRot2.setFromMat4(parent.worldTransform); scaleCompensateRot.mul2(scaleCompensateRot2, this.localRotation); let tmatrix = parent.worldTransform; if (parent.scaleCompensation) { scaleCompensateScaleForParent.mul2(parentWorldScale, parent.getLocalScale()); scaleCompensatePosTransform.setTRS(parent.worldTransform.getTranslation(scaleCompensatePos), scaleCompensateRot2, scaleCompensateScaleForParent); tmatrix = scaleCompensatePosTransform; } tmatrix.transformPoint(this.localPosition, scaleCompensatePos); this.worldTransform.setTRS(scaleCompensatePos, scaleCompensateRot, scale); } else { this.worldTransform.mulAffine2(this._parent.worldTransform, this.localTransform); } } this._dirtyWorld = false; } } syncHierarchy() { if (!this._enabled) { return; } if (this._frozen) { return; } this._frozen = true; if (this._dirtyLocal || this._dirtyWorld) { this._sync(); } const children = this._children; for(let i = 0, len = children.length; i < len; i++){ children[i].syncHierarchy(); } } lookAt(x, y, z, ux = 0, uy = 1, uz = 0) { if (x instanceof Vec3) { target.copy(x); if (y instanceof Vec3) { up$1.copy(y); } else { up$1.copy(Vec3.UP); } } else if (z === undefined) { return; } else { target.set(x, y, z); up$1.set(ux, uy, uz); } matrix.setLookAt(this.getPosition(), target, up$1); rotation$4.setFromMat4(matrix); this.setRotation(rotation$4); } translate(x, y, z) { if (x instanceof Vec3) { position$1.copy(x); } else { position$1.set(x, y, z); } position$1.add(this.getPosition()); this.setPosition(position$1); } translateLocal(x, y, z) { if (x instanceof Vec3) { position$1.copy(x); } else { position$1.set(x, y, z); } this.localRotation.transformVector(position$1, position$1); this.localPosition.add(position$1); if (!this._dirtyLocal) { this._dirtifyLocal(); } } rotate(x, y, z) { rotation$4.setFromEulerAngles(x, y, z); if (this._parent === null) { this.localRotation.mul2(rotation$4, this.localRotation); } else { const rot = this.getRotation(); const parentRot = this._parent.getRotation(); invParentRot.copy(parentRot).invert(); rotation$4.mul2(invParentRot, rotation$4); this.localRotation.mul2(rotation$4, rot); } if (!this._dirtyLocal) { this._dirtifyLocal(); } } rotateLocal(x, y, z) { rotation$4.setFromEulerAngles(x, y, z); this.localRotation.mul(rotation$4); if (!this._dirtyLocal) { this._dirtifyLocal(); } } constructor(name = 'Untitled'){ super(), this.tags = new Tags(this), this.localPosition = new Vec3(), this.localRotation = new Quat(), this.localScale = new Vec3(1, 1, 1), this.localEulerAngles = new Vec3(), this.position = new Vec3(), this.rotation = new Quat(), this.eulerAngles = new Vec3(), this._scale = null, this.localTransform = new Mat4(), this._dirtyLocal = false, this._aabbVer = 0, this._frozen = false, this.worldTransform = new Mat4(), this._dirtyWorld = false, this._worldScaleSign = 0, this._normalMatrix = new Mat3(), this._dirtyNormal = true, this._right = null, this._up = null, this._forward = null, this._parent = null, this._children = [], this._graphDepth = 0, this._enabled = true, this._enabledInHierarchy = false, this.scaleCompensation = false; this.name = name; } } const _viewMat$1 = new Mat4(); const _viewProjMat = new Mat4(); const _viewportMatrix = new Mat4(); class LightCamera { static create(name, lightType, face) { const camera = new Camera$1(); camera.node = new GraphNode(name); camera.aspectRatio = 1; camera.aspectRatioMode = ASPECT_MANUAL; camera._scissorRectClear = true; switch(lightType){ case LIGHTTYPE_OMNI: camera.node.setRotation(LightCamera.pointLightRotations[face]); camera.fov = 90; camera.projection = PROJECTION_PERSPECTIVE; break; case LIGHTTYPE_SPOT: camera.projection = PROJECTION_PERSPECTIVE; break; case LIGHTTYPE_DIRECTIONAL: camera.projection = PROJECTION_ORTHOGRAPHIC; break; } return camera; } static evalSpotCookieMatrix(light) { let cookieCamera = LightCamera._spotCookieCamera; if (!cookieCamera) { cookieCamera = LightCamera.create('SpotCookieCamera', LIGHTTYPE_SPOT); LightCamera._spotCookieCamera = cookieCamera; } cookieCamera.fov = light._outerConeAngle * 2; const cookieNode = cookieCamera._node; cookieNode.setPosition(light._node.getPosition()); cookieNode.setRotation(light._node.getRotation()); cookieNode.rotateLocal(-90, 0, 0); _viewMat$1.setTRS(cookieNode.getPosition(), cookieNode.getRotation(), Vec3.ONE).invert(); _viewProjMat.mul2(cookieCamera.projectionMatrix, _viewMat$1); const cookieMatrix = light.cookieMatrix; const rectViewport = light.atlasViewport; _viewportMatrix.setViewport(rectViewport.x, rectViewport.y, rectViewport.z, rectViewport.w); cookieMatrix.mul2(_viewportMatrix, _viewProjMat); return cookieMatrix; } } LightCamera.pointLightRotations = [ new Quat().setFromEulerAngles(0, 90, 180), new Quat().setFromEulerAngles(0, -90, 180), new Quat().setFromEulerAngles(90, 0, 0), new Quat().setFromEulerAngles(-90, 0, 0), new Quat().setFromEulerAngles(0, 180, 180), new Quat().setFromEulerAngles(0, 0, 180) ]; LightCamera._spotCookieCamera = null; const tempVec3$1 = new Vec3(); const tempAreaLightSizes = new Float32Array(6); const areaHalfAxisWidth = new Vec3(-0.5, 0, 0); const areaHalfAxisHeight = new Vec3(0, 0, 0.5); const TextureIndexFloat = { POSITION_RANGE: 0, DIRECTION_FLAGS: 1, COLOR_ANGLES_BIAS: 2, PROJ_MAT_0: 3, ATLAS_VIEWPORT: 3, PROJ_MAT_1: 4, PROJ_MAT_2: 5, PROJ_MAT_3: 6, AREA_DATA_WIDTH: 7, AREA_DATA_HEIGHT: 8, COUNT: 9 }; const enums = { 'LIGHTSHAPE_PUNCTUAL': `${LIGHTSHAPE_PUNCTUAL}u`, 'LIGHTSHAPE_RECT': `${LIGHTSHAPE_RECT}u`, 'LIGHTSHAPE_DISK': `${LIGHTSHAPE_DISK}u`, 'LIGHTSHAPE_SPHERE': `${LIGHTSHAPE_SPHERE}u`, 'LIGHT_COLOR_DIVIDER': `${LIGHT_COLOR_DIVIDER}.0` }; const buildShaderDefines = (object, prefix)=>{ return Object.keys(object).map((key)=>`#define {${prefix}${key}} ${object[key]}`).join('\n'); }; const lightBufferDefines = `\n ${buildShaderDefines(TextureIndexFloat, 'CLUSTER_TEXTURE_')} ${buildShaderDefines(enums, '')} `; class LightsBuffer { destroy() { this.lightsTexture?.destroy(); this.lightsTexture = null; } createTexture(device, width, height, format, name) { const tex = new Texture(device, { name: name, width: width, height: height, mipmaps: false, format: format, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, type: TEXTURETYPE_DEFAULT, magFilter: FILTER_NEAREST, minFilter: FILTER_NEAREST, anisotropy: 1 }); return tex; } setBounds(min, delta) { this.boundsMin.copy(min); this.boundsDelta.copy(delta); } uploadTextures() { this.lightsTexture.lock().set(this.lightsFloat); this.lightsTexture.unlock(); } updateUniforms() { this._lightsTextureId.setValue(this.lightsTexture); } getSpotDirection(direction, spot) { const mat = spot._node.getWorldTransform(); mat.getY(direction).mulScalar(-1); direction.normalize(); } getLightAreaSizes(light) { const mat = light._node.getWorldTransform(); mat.transformVector(areaHalfAxisWidth, tempVec3$1); tempAreaLightSizes[0] = tempVec3$1.x; tempAreaLightSizes[1] = tempVec3$1.y; tempAreaLightSizes[2] = tempVec3$1.z; mat.transformVector(areaHalfAxisHeight, tempVec3$1); tempAreaLightSizes[3] = tempVec3$1.x; tempAreaLightSizes[4] = tempVec3$1.y; tempAreaLightSizes[5] = tempVec3$1.z; return tempAreaLightSizes; } addLightData(light, lightIndex) { const isSpot = light._type === LIGHTTYPE_SPOT; const hasAtlasViewport = light.atlasViewportAllocated; const isCookie = this.cookiesEnabled && !!light._cookie && hasAtlasViewport; const isArea = this.areaLightsEnabled && light.shape !== LIGHTSHAPE_PUNCTUAL; const castShadows = this.shadowsEnabled && light.castShadows && hasAtlasViewport; const pos = light._node.getPosition(); let lightProjectionMatrix = null; let atlasViewport = null; if (isSpot) { if (castShadows) { const lightRenderData = light.getRenderData(null, 0); lightProjectionMatrix = lightRenderData.shadowMatrix; } else if (isCookie) { lightProjectionMatrix = LightCamera.evalSpotCookieMatrix(light); } } else { if (castShadows || isCookie) { atlasViewport = light.atlasViewport; } } const dataFloat = this.lightsFloat; const dataUint = this.lightsUint; const dataFloatStart = lightIndex * this.lightsTexture.width * 4; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 0] = pos.x; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 1] = pos.y; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 2] = pos.z; dataFloat[dataFloatStart + 4 * TextureIndexFloat.POSITION_RANGE + 3] = light.attenuationEnd; const clusteredData = light.clusteredData; dataUint[dataFloatStart + 4 * TextureIndexFloat.COLOR_ANGLES_BIAS + 0] = clusteredData[0]; dataUint[dataFloatStart + 4 * TextureIndexFloat.COLOR_ANGLES_BIAS + 1] = clusteredData[1]; dataUint[dataFloatStart + 4 * TextureIndexFloat.COLOR_ANGLES_BIAS + 2] = clusteredData[2]; if (light.castShadows) { const lightRenderData = light.getRenderData(null, 0); const biases = light._getUniformBiasValues(lightRenderData); const biasHalf = FloatPacking.float2Half(biases.bias); const normalBiasHalf = FloatPacking.float2Half(biases.normalBias); dataUint[dataFloatStart + 4 * TextureIndexFloat.COLOR_ANGLES_BIAS + 3] = biasHalf | normalBiasHalf << 16; } if (isSpot) { this.getSpotDirection(tempVec3$1, light); dataFloat[dataFloatStart + 4 * TextureIndexFloat.DIRECTION_FLAGS + 0] = tempVec3$1.x; dataFloat[dataFloatStart + 4 * TextureIndexFloat.DIRECTION_FLAGS + 1] = tempVec3$1.y; dataFloat[dataFloatStart + 4 * TextureIndexFloat.DIRECTION_FLAGS + 2] = tempVec3$1.z; } dataUint[dataFloatStart + 4 * TextureIndexFloat.DIRECTION_FLAGS + 3] = light.getClusteredFlags(castShadows, isCookie); if (lightProjectionMatrix) { const matData = lightProjectionMatrix.data; for(let m = 0; m < 16; m++){ dataFloat[dataFloatStart + 4 * TextureIndexFloat.PROJ_MAT_0 + m] = matData[m]; } } if (atlasViewport) { dataFloat[dataFloatStart + 4 * TextureIndexFloat.ATLAS_VIEWPORT + 0] = atlasViewport.x; dataFloat[dataFloatStart + 4 * TextureIndexFloat.ATLAS_VIEWPORT + 1] = atlasViewport.y; dataFloat[dataFloatStart + 4 * TextureIndexFloat.ATLAS_VIEWPORT + 2] = atlasViewport.z / 3; } if (isArea) { const areaSizes = this.getLightAreaSizes(light); dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_WIDTH + 0] = areaSizes[0]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_WIDTH + 1] = areaSizes[1]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_WIDTH + 2] = areaSizes[2]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_HEIGHT + 0] = areaSizes[3]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_HEIGHT + 1] = areaSizes[4]; dataFloat[dataFloatStart + 4 * TextureIndexFloat.AREA_DATA_HEIGHT + 2] = areaSizes[5]; } } constructor(device){ this.areaLightsEnabled = false; this.device = device; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('lightBufferDefinesPS', lightBufferDefines); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('lightBufferDefinesPS', lightBufferDefines); this.cookiesEnabled = false; this.shadowsEnabled = false; this.areaLightsEnabled = false; this.maxLights = 255; const pixelsPerLightFloat = TextureIndexFloat.COUNT; this.lightsFloat = new Float32Array(4 * pixelsPerLightFloat * this.maxLights); this.lightsUint = new Uint32Array(this.lightsFloat.buffer); this.lightsTexture = this.createTexture(this.device, pixelsPerLightFloat, this.maxLights, PIXELFORMAT_RGBA32F, 'LightsTexture'); this._lightsTextureId = this.device.scope.resolve('lightsTexture'); this.invMaxColorValue = 0; this.invMaxAttenuation = 0; this.boundsMin = new Vec3(); this.boundsDelta = new Vec3(); } } const tempVec3 = new Vec3(); const tempMin3 = new Vec3(); const tempMax3 = new Vec3(); const tempBox = new BoundingBox(); class ClusterLight { constructor(){ this.light = null; this.min = new Vec3(); this.max = new Vec3(); } } class WorldClusters { set maxCellLightCount(count) { if (count !== this._maxCellLightCount) { this._maxCellLightCount = count; this._cellsDirty = true; } } get maxCellLightCount() { return this._maxCellLightCount; } set cells(value) { tempVec3.copy(value).floor(); if (!this._cells.equals(tempVec3)) { this._cells.copy(tempVec3); this._cellsLimit.copy(tempVec3).sub(Vec3.ONE); this._cellsDirty = true; } } get cells() { return this._cells; } destroy() { this.lightsBuffer.destroy(); this.releaseClusterTexture(); } releaseClusterTexture() { if (this.clusterTexture) { this.clusterTexture.destroy(); this.clusterTexture = null; } } registerUniforms(device) { this._numClusteredLightsId = device.scope.resolve('numClusteredLights'); this._clusterMaxCellsId = device.scope.resolve('clusterMaxCells'); this._clusterWorldTextureId = device.scope.resolve('clusterWorldTexture'); this._clusterBoundsMinId = device.scope.resolve('clusterBoundsMin'); this._clusterBoundsMinData = new Float32Array(3); this._clusterBoundsDeltaId = device.scope.resolve('clusterBoundsDelta'); this._clusterBoundsDeltaData = new Float32Array(3); this._clusterCellsCountByBoundsSizeId = device.scope.resolve('clusterCellsCountByBoundsSize'); this._clusterCellsCountByBoundsSizeData = new Float32Array(3); this._clusterCellsDotId = device.scope.resolve('clusterCellsDot'); this._clusterCellsDotData = new Int32Array(3); this._clusterCellsMaxId = device.scope.resolve('clusterCellsMax'); this._clusterCellsMaxData = new Int32Array(3); this._clusterTextureWidthId = device.scope.resolve('clusterTextureWidth'); } updateParams(lightingParams) { if (lightingParams) { this.cells = lightingParams.cells; this.maxCellLightCount = lightingParams.maxLightsPerCell; this.lightsBuffer.cookiesEnabled = lightingParams.cookiesEnabled; this.lightsBuffer.shadowsEnabled = lightingParams.shadowsEnabled; this.lightsBuffer.areaLightsEnabled = lightingParams.areaLightsEnabled; } } updateCells() { if (this._cellsDirty) { this._cellsDirty = false; const cx = this._cells.x; const cy = this._cells.y; const cz = this._cells.z; const numCells = cx * cy * cz; const totalPixels = this.maxCellLightCount * numCells; let width = Math.ceil(Math.sqrt(totalPixels)); width = math.roundUp(width, this.maxCellLightCount); const height = Math.ceil(totalPixels / width); this._clusterCellsMaxData[0] = cx; this._clusterCellsMaxData[1] = cy; this._clusterCellsMaxData[2] = cz; this._clusterCellsDotData[0] = this.maxCellLightCount; this._clusterCellsDotData[1] = cx * cz * this.maxCellLightCount; this._clusterCellsDotData[2] = cx * this.maxCellLightCount; this.clusters = new Uint8ClampedArray(totalPixels); this.counts = new Int32Array(numCells); this.releaseClusterTexture(); this.clusterTexture = this.lightsBuffer.createTexture(this.device, width, height, PIXELFORMAT_R8U, 'ClusterTexture'); } } uploadTextures() { this.clusterTexture.lock().set(this.clusters); this.clusterTexture.unlock(); this.lightsBuffer.uploadTextures(); } updateUniforms() { this._numClusteredLightsId.setValue(this._usedLights.length); this.lightsBuffer.updateUniforms(); this._clusterWorldTextureId.setValue(this.clusterTexture); this._clusterMaxCellsId.setValue(this.maxCellLightCount); const boundsDelta = this.boundsDelta; this._clusterCellsCountByBoundsSizeData[0] = this._cells.x / boundsDelta.x; this._clusterCellsCountByBoundsSizeData[1] = this._cells.y / boundsDelta.y; this._clusterCellsCountByBoundsSizeData[2] = this._cells.z / boundsDelta.z; this._clusterCellsCountByBoundsSizeId.setValue(this._clusterCellsCountByBoundsSizeData); this._clusterBoundsMinData[0] = this.boundsMin.x; this._clusterBoundsMinData[1] = this.boundsMin.y; this._clusterBoundsMinData[2] = this.boundsMin.z; this._clusterBoundsDeltaData[0] = boundsDelta.x; this._clusterBoundsDeltaData[1] = boundsDelta.y; this._clusterBoundsDeltaData[2] = boundsDelta.z; this._clusterBoundsMinId.setValue(this._clusterBoundsMinData); this._clusterBoundsDeltaId.setValue(this._clusterBoundsDeltaData); this._clusterCellsDotId.setValue(this._clusterCellsDotData); this._clusterCellsMaxId.setValue(this._clusterCellsMaxData); this._clusterTextureWidthId.setValue(this.clusterTexture.width); } evalLightCellMinMax(clusteredLight, min, max) { min.copy(clusteredLight.min); min.sub(this.boundsMin); min.div(this.boundsDelta); min.mul2(min, this.cells); min.floor(); max.copy(clusteredLight.max); max.sub(this.boundsMin); max.div(this.boundsDelta); max.mul2(max, this.cells); max.ceil(); min.max(Vec3.ZERO); max.min(this._cellsLimit); } collectLights(lights) { const maxLights = this.lightsBuffer.maxLights; const usedLights = this._usedLights; let lightIndex = 1; lights.forEach((light)=>{ const runtimeLight = !!(light.mask & (MASK_AFFECT_DYNAMIC | MASK_AFFECT_LIGHTMAPPED)); const zeroAngleSpotlight = light.type === LIGHTTYPE_SPOT && light._outerConeAngle === 0; if (light.enabled && light.type !== LIGHTTYPE_DIRECTIONAL && light.visibleThisFrame && light.intensity > 0 && runtimeLight && !zeroAngleSpotlight) { if (lightIndex < maxLights) { let clusteredLight; if (lightIndex < usedLights.length) { clusteredLight = usedLights[lightIndex]; } else { clusteredLight = new ClusterLight(); usedLights.push(clusteredLight); } clusteredLight.light = light; light.getBoundingBox(tempBox); clusteredLight.min.copy(tempBox.getMin()); clusteredLight.max.copy(tempBox.getMax()); lightIndex++; } } }); usedLights.length = lightIndex; } evaluateBounds() { const usedLights = this._usedLights; const min = this.boundsMin; const max = this.boundsMax; if (usedLights.length > 1) { min.copy(usedLights[1].min); max.copy(usedLights[1].max); for(let i = 2; i < usedLights.length; i++){ min.min(usedLights[i].min); max.max(usedLights[i].max); } } else { min.set(0, 0, 0); max.set(1, 1, 1); } this.boundsDelta.sub2(max, min); this.lightsBuffer.setBounds(min, this.boundsDelta); } updateClusters(lightingParams) { this.counts.fill(0); this.clusters.fill(0); this.lightsBuffer.areaLightsEnabled = lightingParams ? lightingParams.areaLightsEnabled : false; const divX = this._cells.x; const divZ = this._cells.z; const counts = this.counts; const limit = this._maxCellLightCount; const clusters = this.clusters; const pixelsPerCellCount = this.maxCellLightCount; const usedLights = this._usedLights; for(let i = 1; i < usedLights.length; i++){ const clusteredLight = usedLights[i]; const light = clusteredLight.light; this.lightsBuffer.addLightData(light, i); this.evalLightCellMinMax(clusteredLight, tempMin3, tempMax3); const xStart = tempMin3.x; const xEnd = tempMax3.x; const yStart = tempMin3.y; const yEnd = tempMax3.y; const zStart = tempMin3.z; const zEnd = tempMax3.z; for(let x = xStart; x <= xEnd; x++){ for(let z = zStart; z <= zEnd; z++){ for(let y = yStart; y <= yEnd; y++){ const clusterIndex = x + divX * (z + y * divZ); const count = counts[clusterIndex]; if (count < limit) { clusters[pixelsPerCellCount * clusterIndex + count] = i; counts[clusterIndex] = count + 1; } } } } } } update(lights, lightingParams = null) { this.updateParams(lightingParams); this.updateCells(); this.collectLights(lights); this.evaluateBounds(); this.updateClusters(lightingParams); this.uploadTextures(); } activate() { this.updateUniforms(); } constructor(device){ this.device = device; this.name = 'Untitled'; this.reportCount = 0; this.boundsMin = new Vec3(); this.boundsMax = new Vec3(); this.boundsDelta = new Vec3(); this._cells = new Vec3(1, 1, 1); this._cellsLimit = new Vec3(); this.cells = this._cells; this.maxCellLightCount = 4; this._usedLights = []; this._usedLights.push(new ClusterLight()); this.lightsBuffer = new LightsBuffer(device); this.registerUniforms(device); } } const base64String = 'muPIHORMLNDCz4DxVR/ZvYfAUVEFR47KRIC4nwAAAAAP7WxlhD6Ci+2HCe7BF8jRAPZwdH2UPpI5PdLCJdkvG4UTaNDJ/0crAzne71GCrb4kbdMjjCEGzdX6fNxDMLJq5xkeoIVTdfiZkodEeArmZmp/FQzFjD4x8iOW7Dg64n+3mWqyEwLxXT8zoJXfbw8QJKDCaarUYyTlMzNFHbgUe9IQV7g4YOgtSKpIFZJ0qERm7u4PpmiF89ktHWCywaGmD6h+hfh2/Zd8KYlKqqo4Cem4T42bT/Z9FpCQF1hhSjfBzZ5XFn/y3jegWC6u86KuELRundQS/1Rp+XuKKGIgRv3CvP5y749yqLlFO495JOT3+f2CXgd71npU0/KjjpkZucbJ5m78IVyuSrSozc9jgBUhDrz0hFsyb7LFUH9//wJbBgLdNWJZObfKxrNt8TliLA9w9sXFv6g26iXpf6r/BqcAusj/QzGBZuoUGeEtw8BCXCZ3jUiw4hvM18ZVqlUD3C40LAFXW6FRjuAZGRNstb0/qVk4skwyT+MHrvRorI4rKHVMWZmKyAkzL/78u/9pMQuX14pZN50b2PHn6fRxeaCQLsfT4dpvIkWWFuFVENZIh+8xgR6lU+85W0PPdAu1j99kcCG40JBQa4JMyRzq6qriOBLtqF87vpCJan0WEduVr/mOYkS00urVA0mA6M3031+GmGmW48PaJDYOEIb3bIXWPaLoAOEinX1TN3+/vwhG6nqJu0TdHpedS7QsGZIoxH3nQYYjQP1jmbahlbNngw5ogsGk1y50XZyUmQBY+/JBJ3Unu4dApm+WmPwHPU9gLb+4mHh4BiY6M86pq+WeTyWdI3s0CXPEtHGXZ8zMZgUoyRomBi1VdazzuN+WOmQ9Pa0Z0tlNopUi8AJ4x2Xn4mmOKEbXLxlbVsWu8XhuDGYFOGCRVdSqDPXrHU5SDdUlti3k5///SBwzTMwK3L4a1H7w4lnpEas6////AfX8asyIBfeFXVJ3tgvxQ/blZuUKyIODIfr/UzdWNu7pciLBpdZRZ4pIfZ1R6szq+XNxkGG///8EZFpu7VHAhFWqHEOrB9unw+YQa5o8/9IR/V5/zq+986rJSyfgJKt2u9hxU1wzyQWPjJGvzG9+eWWxGFOHVKqI4jBQALwZZswesnvZ2UmmkEXdiRpz8B+oWE7PY70ZTMndisYSXg2TqoI+3y9BxbnY2Y4EfbdcRhAvG59NqDENNYbxKvK5HJfPG5M+Wi2AcpLVJrD6caiEOzgSoVNSgQK8fm2M3zGcF4xtClv/8Hs9oD7C3jitTATYNQxmKqKf1LhIxzf1bmfiNn7UKFmcJu4sLqVLwxGSue3taBEyknkw5hXTsUCvqmmL/f8n/w0giR7Hu/9EHvpkz3yuu64TioMkzdTJ30i0+hFnQqW1+v9mMwq+z9qGX0UFu9MomvVG2xod6vc12AAAAACq7sGa5qptFR0jF3nQt/D+7PibKYahaxP3hEixPbGi9nwNf2LAa7LkEZRKxzXeCD64Xpii5n+8Kpg8eHIv7AWXZltgMoGltmoJ0XGdOCL8WkzphvR9N2o3ARSZ42l5e5Pe4B58MCRlP3EKv+mcloknH+fto5BWsmEutW6KvjOVsznFCktkSczVk4aGvj9VXlRcLeDoKG8RkBgdcNG2bf8HUL4MT2DM+ar7NImJhKpxakX4Vk0CnP+/XNhl5UsP0lXgeZXPoDBMSW5An+DXlTCO5FQGwSPYwHLKYVIimEdAoVe49rQLaaNcye5LxU2/c5TijTgJtD5eQQIe1snxauj5jZsxJBUJdoP/zqpjqv8qBruoPsVsP8N44PCUW5Dd0DzqjSS/Dl5mI9cn1w2ndN/0KAEm1QAAAACwu6KM/083IBbH5bPa/9oHUwcU8I9v3j6/v18QYammrf+P6VL///8BrpuM3fOLCxaLNOFNF1zPbPYTP65ni6njft4eVcyrVXRQFrs52tr35StiSp55edVDCBC0H5rIfac6nzUwxQSt7y15QoKb+5zebEQUmVbrPjXuUa19Ey7sqXMiSUKHaw72PJKDdrutJoQr3u6lEYJ8K0MakWKj9zjTFi4X94TsKYco0GrLeB60M6D8M/80rhXUW8iMequg8y5F838WI0+gp3GBN5Kj/xIOxTWQuUaPV/LwvARr1VH93BFgGZR1MFW0Ua30GbYmdnAgo9VWy8SQtpDUgGE2r2zq2eTEMCL7sMKmE1hchVhuF/TCq9iXKEm86kzOf3Rp9ZnCxbpDUj+FKNxVyXe6pVZkRXv/m95SnB/EB8aME29N85MtAcDoXWlor8De2Q5Dg1tar+8wgiZufbMam81j//ASUohoR/zSh2KG4bvT6mkIPz6C5/98DC3LaWlaEZ1zA5JORZRu6J/a0GY285sEYzw71YqOT1ihAG0z5SDt1xNiDQWZdFpndArp6xWhqSDkRb4kSJEHb9liPvw7uLV/6i5MVf//A9Qjr8xkAEUh+KDI+zdtJ68d6MBOktg1iyp/SCq8O9f5pbamn1VVVQPRTWqNBvhQKa07s6P0lc9Luu/3gw4HeyOUfz8MxMwV4UQhua+t9cr4bz/nIB2wnDSK1K7I94M+s6C84htaX/CNlMQUSs2KJO+yaebfTbkNX5yWcqEJevo0vbKUiETuFXiL019A3E+lmsyZMwXrXLLiQAZ5t9+jI3JobhJTMiDH5ZOQ+8Jau5555NMjHSscP9qCVaa40doh+1a3Ukf6jqBmLddgh79/fwTfCyqiuldNkUoy+nUp+4nerwg0OjtGv2x485PJOJvUEokNhYIdWjpx7BWk0VZGWOp3jSFTJ2bnu6KCduZtG/UcBC9RZ3W/jMSfSMw4Etr/DoD/XYP2V5Ovw+YoM3F5g2dGLdvuG6ZkVGLE6Dk5Zr+sdSyGliJP1y2OFf/KFO0RWO+3gsGhesTnfZVpTd8/HwgO216gwaqo+vY3TljfJWowY+i0p0Os4SLn/1wLqDHMlszggmT/D8MRFzs+pLv6LNJSsNZ/r41mWi/rF6ZcKp/yzJdK0VU44hskq3RGpgO6mIpJDsf/mZkFrz0yYOMLbuaj/wp1v7JMFM5eqvBhmTd7U8frQAtHtys4zgpjZmzUhOVTfNNLifElGXADlqHGKrkBT/nYwX8ZRm3RjvyPvjKyEqEGKUpVnvOGx+NKPHiWM//ZDpDVGvvrjmk8RPF/wiYZD3+Us8YCXjrVOfjdd1UPAfjLp8jgSn4me7DPTpz1Ggy9XL80guFO7ECT10AvILKfD18Qx+KY/f8aRqu0oOO8hfKRFZa9PUJwCsp6VdZz6LFkm2b9Pl2LIifCwzRy7TpdG2uAtOxP2OemY26bJMa9ZGSLIRlMsgpDpnDJwd0oa5pQ13x1hrHf52HpulUWonGWsfXZbSQYKu9bnEN76ciQih0opN3deDVrbrxorfVlnCmL1R9zq3ePGWIv21c7pW8kEiFTM5JX8dAw867s/60cf79/BH+MDFCZBHlz1L+qGOJf/1txhhmrf3//As+RIJwevDb+fgNXVeHw67QptZegayhrEwr5Gy+EPo1RLaMtPbqOZYoVzXzwzjMFWZxyUG9YUIf6////AQWy84iAygLk9COtXt92+0mT/xg0zMzMBeLkb8y9SL2TDXgSX422hDgpGNLJyuPioA+YJ91G8znrpNqHkwYyscaJDEc9Vc+j4cXle3hvcd2JqDQH2lBZxDn6mUTs0b75raMvbs727codX01Anj8f3wir9P2xQaQ22v/TxCMglKDFoTjaP01XTLgxnTvPv02JgEUrW6UDgOnobFpLdvKdlypgIzPcq14fgXU5tvVW0FEs7VRlsG1IyA69fN4n+awHhT34cE+xUvdj86C8LgAsFheTjI9Ht9EyYAAAAAAVBVKRx2wLgUTI0/2QfyJo2riRw3JDqzEShmx/Lifo6mRkQVbS7X53t+EvKxcXogtdts31e9MRHdcHgsA8rt4/mt2unlzQ/wsU8Gu7+W6Oj7eD8EQdDp5XlCsVaS/AV/t5ZpPOHR3rGpyAJe9IPV+xMrBL1Oz/8MQhFs31h0N1cVnq371uqIJYHyafKH1jteAK3VpMXBcuC+yt0ZeKyRUY4QhdrJJ4tJ1wg3Hu6kDsbovxupTMkGdRrm8oZSoYPbJ+PwH/xotgTdkA1205vUEfnqkI04T/fnnd1fiZW5AwNcggd7fi4j5zasmcntZexIxqFZQMzMJpfndmI5jn17cgn5EV5t9XN0C///8Q9wlJpMGXdoiaMTG2sVyHQsn8mWRISCLNG777S0OuDRP2GlLcJ2UeOg7Fo8hTNPeJ//iTJhyqxhKRUntdXOihq2wfKfH///8B0GGrwT+fSOQRdctKxjjGCSS11d6BlQ9BDfE0J6Z25FaNTKGpFKNCMr2G/041KpWwBLVe1k08vncseQbKZdXi8x1t9XA45U/Wd43D9wAh3Tal0aiLVzGPusOZ1F+W3TWoqlX/A95+dNef11TsuGful+ctGssldk3fqpfqh+43XTxL42+leSHoF/dWHYGX6maqUEuLX7UB+r/6Llr4LKocbVIeu+hB9QTPfz9fCP8RyWmX4SmbhMFsNtCijV7lVcwejLKlvl0GfCndnWV7/39VBrtTRuUx92oke3GBgKkC5fdGK0YvNK+xenKaDmsHDjNFUM3NMz3ZiXXFuLgojosPVCDEl2W5BjX3Ms+j0GSqACHmh0+RPWyuNm/Qe8vFf9AW7N1uRaxWirrUytqEJnJ4/Flm8hSoiZ2NQBsS6w/yQlC4gCaFo8q4nyY6AFdo4hiwhBXzbNKKvZvktCjSCukRR/BbYVbNwZi2Yh3hGodEacLW8qijiWJODf0P2bhfaiPspPT4lYJBgi/KfcFwCfvyUIgkJOv///8CG/JEepRBLaMFE+2TgrqsJXOVOWHt6g/bFwVLLMVBsMR50dis/39/AlBX+/rMTJkUQrnlxpR2iu0Tp8tATkRYGmDIrcAiRP8PjoWIlb7/0ecTdSCE9Y58+a+n/FovJQTVF4F2jAxMZhTgrM/KVS5BQu6bVbkWY5HXnxRshks3urDdW4RkWp4M4TeLmFK5KF/uHkkiO5Kv96RioH984v/CSDBnG+BwlnU9B+o7Y+0X0Nob+0pLsStxjvPXMy2eCpzhOWV4XbObBHN4UE2sLQ/DIqXhOzxVf38GlTi6aG7EnePO7TRJm9yOfUUcqq1I2iQHrVDqn3TUNRi/lMw8KbMW/3/nqCz/Ef8PoW5Qxcz2yHR/f78EPB2Stbd+ZFmfNTUYILzsb9YNhpaHcaymYrBiNHmFE3Y4ccYJ25Prqm7zHobGHED8/93ZNlWro9vcKivGZs31UiK1k5zjUhexUgbqJb+fUTjxce/7Zly8a5KMC1fX5nfjPgibdvzbXV1jRT2asXvmSAusaLdq1TSIJ8fXINk5AtT34EWPAsfP9IFQqM5K11O6saoHJA=='; let data = null; const initData = ()=>{ if (!data) { const binaryString = atob(base64String); data = Uint8Array.from(binaryString, (char)=>char.charCodeAt(0)); } }; const blueNoiseData = ()=>{ initData(); return data; }; class BlueNoise { _next() { this.seed = (this.seed + 4) % data.length; } value() { this._next(); return data[this.seed] / 255; } vec4(dest = new Vec4()) { this._next(); return dest.set(data[this.seed], data[this.seed + 1], data[this.seed + 2], data[this.seed + 3]).mulScalar(1 / 255); } constructor(seed = 0){ this.seed = 0; this.seed = seed * 4; initData(); } } const lightCubeDir = [ new Vec3(-1, 0, 0), new Vec3(1, 0, 0), new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1) ]; class LightCube { update(ambientLight, lights) { const colors = this.colors; const { r, g, b } = ambientLight; for(let j = 0; j < 6; j++){ colors[j * 3] = r; colors[j * 3 + 1] = g; colors[j * 3 + 2] = b; } for(let j = 0; j < lights.length; j++){ const light = lights[j]; if (light._type === LIGHTTYPE_DIRECTIONAL) { for(let c = 0; c < 6; c++){ const weight = Math.max(lightCubeDir[c].dot(light._direction), 0) * light._intensity; const lightColor = light._color; colors[c * 3] += lightColor.r * weight; colors[c * 3 + 1] += lightColor.g * weight; colors[c * 3 + 2] += lightColor.b * weight; } } } } constructor(){ this.colors = new Float32Array(6 * 3); } } const createTexture = (device, namePrefix, size, data)=>{ const texture = new Texture(device, { name: `${namePrefix}${size}`, width: size, height: size, format: PIXELFORMAT_RGBA8, addressU: ADDRESS_REPEAT, addressV: ADDRESS_REPEAT, type: TEXTURETYPE_DEFAULT, magFilter: FILTER_NEAREST, minFilter: FILTER_NEAREST, anisotropy: 1, mipmaps: false }); texture.lock().set(data); texture.unlock(); return texture; }; const deviceCacheBlueNoise = new DeviceCache(); const getBlueNoiseTexture = (device)=>{ return deviceCacheBlueNoise.get(device, ()=>{ const data = blueNoiseData(); const size = Math.sqrt(data.length / 4); return createTexture(device, 'BlueNoise', size, data); }); }; class ShadowMap { destroy() { if (this.texture) { this.texture.destroy(); this.texture = null; } const targets = this.renderTargets; for(let i = 0; i < targets.length; i++){ targets[i].destroy(); } this.renderTargets.length = 0; } static create(device, light) { let shadowMap = null; if (light._type === LIGHTTYPE_OMNI) { shadowMap = this.createCubemap(device, light._shadowResolution, light._shadowType); } else { shadowMap = this.create2dMap(device, light._shadowResolution, light._shadowType); } return shadowMap; } static createAtlas(device, resolution, shadowType) { const shadowMap = this.create2dMap(device, resolution, shadowType); const targets = shadowMap.renderTargets; const rt = targets[0]; for(let i = 0; i < 5; i++){ targets.push(rt); } return shadowMap; } static create2dMap(device, size, shadowType) { const shadowInfo = shadowTypeInfo.get(shadowType); let format = shadowInfo.format; if (format === PIXELFORMAT_R32F && !device.textureFloatRenderable && device.textureHalfFloatRenderable) { format = PIXELFORMAT_R16F; } const formatName = pixelFormatInfo.get(format)?.name; let filter = FILTER_LINEAR; if (shadowType === SHADOW_VSM_32F) { filter = device.extTextureFloatLinear ? FILTER_LINEAR : FILTER_NEAREST; } if (shadowType === SHADOW_PCSS_32F) { filter = FILTER_NEAREST; } const texture = new Texture(device, { format: format, width: size, height: size, mipmaps: false, minFilter: filter, magFilter: filter, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: `ShadowMap2D_${formatName}` }); let target = null; if (shadowInfo?.pcf) { texture.compareOnRead = true; texture.compareFunc = FUNC_LESS; target = new RenderTarget({ depthBuffer: texture }); } else { target = new RenderTarget({ colorBuffer: texture, depth: true }); } if (device.isWebGPU) { target.flipY = true; } return new ShadowMap(texture, [ target ]); } static createCubemap(device, size, shadowType) { const shadowInfo = shadowTypeInfo.get(shadowType); const formatName = pixelFormatInfo.get(shadowInfo.format)?.name; const isPcss = shadowType === SHADOW_PCSS_32F; const filter = isPcss ? FILTER_NEAREST : FILTER_LINEAR; const cubemap = new Texture(device, { format: shadowInfo?.format, width: size, height: size, cubemap: true, mipmaps: false, minFilter: filter, magFilter: filter, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: `ShadowMapCube_${formatName}` }); if (!isPcss) { cubemap.compareOnRead = true; cubemap.compareFunc = FUNC_LESS; } const targets = []; for(let i = 0; i < 6; i++){ if (isPcss) { targets.push(new RenderTarget({ colorBuffer: cubemap, face: i, depth: true })); } else { targets.push(new RenderTarget({ depthBuffer: cubemap, face: i })); } } return new ShadowMap(cubemap, targets); } constructor(texture, targets){ this.texture = texture; this.cached = false; this.renderTargets = targets; } } const _tempArray = []; const _tempArray2 = []; const _viewport$1 = new Vec4(); const _scissor = new Vec4(); class Slot { constructor(rect){ this.size = Math.floor(rect.w * 1024); this.used = false; this.lightId = -1; this.rect = rect; } } class LightTextureAtlas { destroy() { this.destroyShadowAtlas(); this.destroyCookieAtlas(); } destroyShadowAtlas() { this.shadowAtlas?.destroy(); this.shadowAtlas = null; } destroyCookieAtlas() { this.cookieAtlas?.destroy(); this.cookieAtlas = null; this.cookieRenderTarget?.destroy(); this.cookieRenderTarget = null; } allocateShadowAtlas(resolution, shadowType = SHADOW_PCF3_32F) { const existingFormat = this.shadowAtlas?.texture.format; const requiredFormat = shadowTypeInfo.get(shadowType).format; if (!this.shadowAtlas || this.shadowAtlas.texture.width !== resolution || existingFormat !== requiredFormat) { this.version++; this.destroyShadowAtlas(); this.shadowAtlas = ShadowMap.createAtlas(this.device, resolution, shadowType); this.shadowAtlas.cached = true; const scissorOffset = 4 / this.shadowAtlasResolution; this.scissorVec.set(scissorOffset, scissorOffset, -2 * scissorOffset, -2 * scissorOffset); } } allocateCookieAtlas(resolution) { if (this.cookieAtlas.width !== resolution) { this.cookieRenderTarget.resize(resolution, resolution); this.version++; } } allocateUniforms() { this._shadowAtlasTextureId = this.device.scope.resolve('shadowAtlasTexture'); this._shadowAtlasParamsId = this.device.scope.resolve('shadowAtlasParams'); this._shadowAtlasParams = new Float32Array(2); this._cookieAtlasTextureId = this.device.scope.resolve('cookieAtlasTexture'); } updateUniforms() { const rt = this.shadowAtlas.renderTargets[0]; const shadowBuffer = rt.depthBuffer; this._shadowAtlasTextureId.setValue(shadowBuffer); this._shadowAtlasParams[0] = this.shadowAtlasResolution; this._shadowAtlasParams[1] = this.shadowEdgePixels; this._shadowAtlasParamsId.setValue(this._shadowAtlasParams); this._cookieAtlasTextureId.setValue(this.cookieAtlas); } subdivide(numLights, lightingParams) { let atlasSplit = lightingParams.atlasSplit; if (!atlasSplit) { const gridSize = Math.ceil(Math.sqrt(numLights)); atlasSplit = _tempArray2; atlasSplit[0] = gridSize; atlasSplit.length = 1; } const arraysEqual = (a, b)=>a.length === b.length && a.every((v, i)=>v === b[i]); if (!arraysEqual(atlasSplit, this.atlasSplit)) { this.version++; this.slots.length = 0; this.atlasSplit.length = 0; this.atlasSplit.push(...atlasSplit); const splitCount = this.atlasSplit[0]; if (splitCount > 1) { const invSize = 1 / splitCount; for(let i = 0; i < splitCount; i++){ for(let j = 0; j < splitCount; j++){ const rect = new Vec4(i * invSize, j * invSize, invSize, invSize); const nextLevelSplit = this.atlasSplit[1 + i * splitCount + j]; if (nextLevelSplit > 1) { for(let x = 0; x < nextLevelSplit; x++){ for(let y = 0; y < nextLevelSplit; y++){ const invSizeNext = invSize / nextLevelSplit; const rectNext = new Vec4(rect.x + x * invSizeNext, rect.y + y * invSizeNext, invSizeNext, invSizeNext); this.slots.push(new Slot(rectNext)); } } } else { this.slots.push(new Slot(rect)); } } } } else { this.slots.push(new Slot(new Vec4(0, 0, 1, 1))); } this.slots.sort((a, b)=>{ return b.size - a.size; }); } } collectLights(localLights, lightingParams) { const cookiesEnabled = lightingParams.cookiesEnabled; const shadowsEnabled = lightingParams.shadowsEnabled; let needsShadowAtlas = false; let needsCookieAtlas = false; const lights = _tempArray; lights.length = 0; const processLights = (list)=>{ for(let i = 0; i < list.length; i++){ const light = list[i]; if (light.visibleThisFrame) { const lightShadow = shadowsEnabled && light.castShadows; const lightCookie = cookiesEnabled && !!light.cookie; needsShadowAtlas || (needsShadowAtlas = lightShadow); needsCookieAtlas || (needsCookieAtlas = lightCookie); if (lightShadow || lightCookie) { lights.push(light); } } } }; if (cookiesEnabled || shadowsEnabled) { processLights(localLights); } lights.sort((a, b)=>{ return b.maxScreenSize - a.maxScreenSize; }); if (needsShadowAtlas) { this.allocateShadowAtlas(this.shadowAtlasResolution, lightingParams.shadowType); } if (needsCookieAtlas) { this.allocateCookieAtlas(this.cookieAtlasResolution); } if (needsShadowAtlas || needsCookieAtlas) { this.subdivide(lights.length, lightingParams); } return lights; } setupSlot(light, rect) { light.atlasViewport.copy(rect); const faceCount = light.numShadowFaces; for(let face = 0; face < faceCount; face++){ if (light.castShadows || light._cookie) { _viewport$1.copy(rect); _scissor.copy(rect); if (light._type === LIGHTTYPE_SPOT) { _viewport$1.add(this.scissorVec); } if (light._type === LIGHTTYPE_OMNI) { const smallSize = _viewport$1.z / 3; const offset = this.cubeSlotsOffsets[face]; _viewport$1.x += smallSize * offset.x; _viewport$1.y += smallSize * offset.y; _viewport$1.z = smallSize; _viewport$1.w = smallSize; _scissor.copy(_viewport$1); } if (light.castShadows) { const lightRenderData = light.getRenderData(null, face); lightRenderData.shadowViewport.copy(_viewport$1); lightRenderData.shadowScissor.copy(_scissor); } } } } assignSlot(light, slotIndex, slotReassigned) { light.atlasViewportAllocated = true; const slot = this.slots[slotIndex]; slot.lightId = light.id; slot.used = true; if (slotReassigned) { light.atlasSlotUpdated = true; light.atlasVersion = this.version; light.atlasSlotIndex = slotIndex; } } update(localLights, lightingParams) { this.shadowAtlasResolution = lightingParams.shadowAtlasResolution; this.cookieAtlasResolution = lightingParams.cookieAtlasResolution; const lights = this.collectLights(localLights, lightingParams); if (lights.length > 0) { const slots = this.slots; for(let i = 0; i < slots.length; i++){ slots[i].used = false; } const assignCount = Math.min(lights.length, slots.length); for(let i = 0; i < assignCount; i++){ const light = lights[i]; if (light.castShadows) { light._shadowMap = this.shadowAtlas; } const previousSlot = slots[light.atlasSlotIndex]; if (light.atlasVersion === this.version && light.id === previousSlot?.lightId) { const previousSlot = slots[light.atlasSlotIndex]; if (previousSlot.size === slots[i].size && !previousSlot.used) { this.assignSlot(light, light.atlasSlotIndex, false); } } } let usedCount = 0; for(let i = 0; i < assignCount; i++){ while(usedCount < slots.length && slots[usedCount].used){ usedCount++; } const light = lights[i]; if (!light.atlasViewportAllocated) { this.assignSlot(light, usedCount, true); } const slot = slots[light.atlasSlotIndex]; this.setupSlot(light, slot.rect); } } this.updateUniforms(); } constructor(device){ this.device = device; this.version = 1; this.shadowAtlasResolution = 2048; this.shadowAtlas = null; this.shadowEdgePixels = 3; this.cookieAtlasResolution = 4; this.cookieAtlas = new Texture(this.device, { name: 'CookieAtlas', width: this.cookieAtlasResolution, height: this.cookieAtlasResolution, format: PIXELFORMAT_SRGBA8, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); this.cookieRenderTarget = new RenderTarget({ colorBuffer: this.cookieAtlas, depth: false, flipY: true }); this.slots = []; this.atlasSplit = []; this.cubeSlotsOffsets = [ new Vec2(0, 0), new Vec2(0, 1), new Vec2(1, 0), new Vec2(1, 1), new Vec2(2, 0), new Vec2(2, 1) ]; this.scissorVec = new Vec4(); this.allocateShadowAtlas(1); this.allocateCookieAtlas(1); this.allocateUniforms(); } } const blendModes = []; blendModes[BLEND_SUBTRACTIVE] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_REVERSE_SUBTRACT }; blendModes[BLEND_NONE] = { src: BLENDMODE_ONE, dst: BLENDMODE_ZERO, op: BLENDEQUATION_ADD }; blendModes[BLEND_NORMAL] = { src: BLENDMODE_SRC_ALPHA, dst: BLENDMODE_ONE_MINUS_SRC_ALPHA, op: BLENDEQUATION_ADD, alphaSrc: BLENDMODE_ONE }; blendModes[BLEND_PREMULTIPLIED] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE_MINUS_SRC_ALPHA, op: BLENDEQUATION_ADD }; blendModes[BLEND_ADDITIVE] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_ADD }; blendModes[BLEND_ADDITIVEALPHA] = { src: BLENDMODE_SRC_ALPHA, dst: BLENDMODE_ONE, op: BLENDEQUATION_ADD }; blendModes[BLEND_MULTIPLICATIVE2X] = { src: BLENDMODE_DST_COLOR, dst: BLENDMODE_SRC_COLOR, op: BLENDEQUATION_ADD }; blendModes[BLEND_SCREEN] = { src: BLENDMODE_ONE_MINUS_DST_COLOR, dst: BLENDMODE_ONE, op: BLENDEQUATION_ADD }; blendModes[BLEND_MULTIPLICATIVE] = { src: BLENDMODE_DST_COLOR, dst: BLENDMODE_ZERO, op: BLENDEQUATION_ADD }; blendModes[BLEND_MIN] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_MIN }; blendModes[BLEND_MAX] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_MAX }; let id$3 = 0; class Material { get hasShaderChunks() { return this._shaderChunks != null; } get shaderChunks() { if (!this._shaderChunks) { this._shaderChunks = new ShaderChunks(); } return this._shaderChunks; } getShaderChunks(shaderLanguage = SHADERLANGUAGE_GLSL) { const chunks = this.shaderChunks; return shaderLanguage === SHADERLANGUAGE_GLSL ? chunks.glsl : chunks.wgsl; } set shaderChunksVersion(value) { this.shaderChunks.version = value; } get shaderChunksVersion() { return this.shaderChunks.version; } set chunks(value) { this._oldChunks = value; } get chunks() { Object.assign(this._oldChunks, Object.fromEntries(this.shaderChunks.glsl)); return this._oldChunks; } set depthBias(value) { this._depthState.depthBias = value; } get depthBias() { return this._depthState.depthBias; } set slopeDepthBias(value) { this._depthState.depthBiasSlope = value; } get slopeDepthBias() { return this._depthState.depthBiasSlope; } set redWrite(value) { this._blendState.redWrite = value; } get redWrite() { return this._blendState.redWrite; } set greenWrite(value) { this._blendState.greenWrite = value; } get greenWrite() { return this._blendState.greenWrite; } set blueWrite(value) { this._blendState.blueWrite = value; } get blueWrite() { return this._blendState.blueWrite; } set alphaWrite(value) { this._blendState.alphaWrite = value; } get alphaWrite() { return this._blendState.alphaWrite; } get transparent() { return this._blendState.blend; } _updateTransparency() { const transparent = this.transparent; const meshInstances = this.meshInstances; for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].transparent = transparent; } } set blendState(value) { this._blendState.copy(value); this._updateTransparency(); } get blendState() { return this._blendState; } set blendType(type) { const blendMode = blendModes[type]; this._blendState.setColorBlend(blendMode.op, blendMode.src, blendMode.dst); this._blendState.setAlphaBlend(blendMode.alphaOp ?? blendMode.op, blendMode.alphaSrc ?? blendMode.src, blendMode.alphaDst ?? blendMode.dst); const blend = type !== BLEND_NONE; if (this._blendState.blend !== blend) { this._blendState.blend = blend; this._updateTransparency(); } this._updateMeshInstanceKeys(); } get blendType() { if (!this.transparent) { return BLEND_NONE; } const { colorOp, colorSrcFactor, colorDstFactor, alphaOp, alphaSrcFactor, alphaDstFactor } = this._blendState; for(let i = 0; i < blendModes.length; i++){ const blendMode = blendModes[i]; if (blendMode.src === colorSrcFactor && blendMode.dst === colorDstFactor && blendMode.op === colorOp && blendMode.src === alphaSrcFactor && blendMode.dst === alphaDstFactor && blendMode.op === alphaOp) { return i; } } return BLEND_NORMAL; } set depthState(value) { this._depthState.copy(value); } get depthState() { return this._depthState; } set depthTest(value) { this._depthState.test = value; } get depthTest() { return this._depthState.test; } set depthFunc(value) { this._depthState.func = value; } get depthFunc() { return this._depthState.func; } set depthWrite(value) { this._depthState.write = value; } get depthWrite() { return this._depthState.write; } copy(source) { this.name = source.name; this.alphaTest = source.alphaTest; this.alphaToCoverage = source.alphaToCoverage; this._blendState.copy(source._blendState); this._depthState.copy(source._depthState); this.cull = source.cull; this.stencilFront = source.stencilFront?.clone(); if (source.stencilBack) { this.stencilBack = source.stencilFront === source.stencilBack ? this.stencilFront : source.stencilBack.clone(); } this.clearParameters(); for(const name in source.parameters){ if (source.parameters.hasOwnProperty(name)) { this._setParameterSimple(name, source.parameters[name].data); } } this.defines.clear(); source.defines.forEach((value, key)=>this.defines.set(key, value)); this._shaderChunks = source.hasShaderChunks ? new ShaderChunks() : null; this._shaderChunks?.copy(source._shaderChunks); return this; } clone() { const clone = new this.constructor(); return clone.copy(this); } _updateMeshInstanceKeys() { const meshInstances = this.meshInstances; for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].updateKey(); } } updateUniforms(device, scene) { if (this._dirtyShader) { this.clearVariants(); } } getShaderVariant(params) {} update() { if (Object.keys(this._oldChunks).length > 0) { for (const [key, value] of Object.entries(this._oldChunks)){ this.shaderChunks.glsl.set(key, value); delete this._oldChunks[key]; } } if (this._definesDirty || this._shaderChunks?.isDirty()) { this._definesDirty = false; this._shaderChunks?.resetDirty(); this.clearVariants(); } this.dirty = true; } clearParameters() { this.parameters = {}; } getParameters() { return this.parameters; } clearVariants() { this.variants.clear(); const meshInstances = this.meshInstances; const count = meshInstances.length; for(let i = 0; i < count; i++){ meshInstances[i].clearShaders(); } } getParameter(name) { return this.parameters[name]; } _setParameterSimple(name, data) { const param = this.parameters[name]; if (param) { param.data = data; } else { this.parameters[name] = { scopeId: null, data: data }; } } setParameter(name, data) { if (data === undefined && typeof name === 'object') { const uniformObject = name; if (uniformObject.length) { for(let i = 0; i < uniformObject.length; i++){ this.setParameter(uniformObject[i]); } return; } name = uniformObject.name; data = uniformObject.value; } this._setParameterSimple(name, data); } deleteParameter(name) { if (this.parameters[name]) { delete this.parameters[name]; } } setParameters(device, names) { const parameters = this.parameters; if (names === undefined) names = parameters; for(const paramName in names){ const parameter = parameters[paramName]; if (parameter) { if (!parameter.scopeId) { parameter.scopeId = device.scope.resolve(paramName); } parameter.scopeId.setValue(parameter.data); } } } setDefine(name, value) { let modified = false; const { defines } = this; if (value !== undefined && value !== false) { modified = !defines.has(name) || defines.get(name) !== value; defines.set(name, value); } else { modified = defines.has(name); defines.delete(name); } this._definesDirty || (this._definesDirty = modified); } getDefine(name) { return this.defines.has(name); } destroy() { this.variants.clear(); for(let i = 0; i < this.meshInstances.length; i++){ const meshInstance = this.meshInstances[i]; meshInstance.clearShaders(); meshInstance._material = null; if (meshInstance.mesh) { const defaultMaterial = getDefaultMaterial(meshInstance.mesh.device); if (this !== defaultMaterial) { meshInstance.material = defaultMaterial; } } } this.meshInstances.length = 0; } addMeshInstanceRef(meshInstance) { this.meshInstances.push(meshInstance); } removeMeshInstanceRef(meshInstance) { const meshInstances = this.meshInstances; const i = meshInstances.indexOf(meshInstance); if (i !== -1) { meshInstances.splice(i, 1); } } constructor(){ this.meshInstances = []; this.name = 'Untitled'; this.userId = ''; this.id = id$3++; this.variants = new Map(); this.defines = new Map(); this._definesDirty = false; this.parameters = {}; this.alphaTest = 0; this.alphaToCoverage = false; this._blendState = new BlendState(); this._depthState = new DepthState(); this.cull = CULLFACE_BACK; this.stencilFront = null; this.stencilBack = null; this._shaderChunks = null; this._oldChunks = {}; this._dirtyShader = true; this._shaderVersion = 0; this._scene = null; this.dirty = true; } } class ShadowMapCache { destroy() { this.clear(); this.cache = null; } clear() { this.cache.forEach((shadowMaps)=>{ shadowMaps.forEach((shadowMap)=>{ shadowMap.destroy(); }); }); this.cache.clear(); } getKey(light) { const isCubeMap = light._type === LIGHTTYPE_OMNI; const shadowType = light._shadowType; const resolution = light._shadowResolution; return `${isCubeMap}-${shadowType}-${resolution}`; } get(device, light) { const key = this.getKey(light); const shadowMaps = this.cache.get(key); if (shadowMaps && shadowMaps.length) { return shadowMaps.pop(); } const shadowMap = ShadowMap.create(device, light); shadowMap.cached = true; return shadowMap; } add(light, shadowMap) { const key = this.getKey(light); const shadowMaps = this.cache.get(key); if (shadowMaps) { shadowMaps.push(shadowMap); } else { this.cache.set(key, [ shadowMap ]); } } constructor(){ this.cache = new Map(); } } class RenderPassShadowLocalNonClustered extends RenderPass { execute() { this.shadowRenderer.renderFace(this.light, null, this.face, false); } after() { if (this.applyVsm) { this.shadowRenderer.renderVsm(this.light, this.shadowCamera); } } constructor(device, shadowRenderer, light, face, applyVsm){ super(device); this.requiresCubemaps = false; this.shadowRenderer = shadowRenderer; this.light = light; this.face = face; this.applyVsm = applyVsm; this.shadowCamera = shadowRenderer.prepareFace(light, null, face); shadowRenderer.setupRenderPass(this, this.shadowCamera, true); } } class ShadowRendererLocal { cull(light, comp, casters = null) { const isClustered = this.renderer.scene.clusteredLightingEnabled; light.visibleThisFrame = true; if (!isClustered) { if (!light._shadowMap) { light._shadowMap = ShadowMap.create(this.device, light); } } const type = light._type; const faceCount = type === LIGHTTYPE_SPOT ? 1 : 6; for(let face = 0; face < faceCount; face++){ const lightRenderData = light.getRenderData(null, face); const shadowCam = lightRenderData.shadowCamera; shadowCam.nearClip = light.attenuationEnd / 1000; shadowCam.farClip = light.attenuationEnd; const shadowCamNode = shadowCam._node; const lightNode = light._node; shadowCamNode.setPosition(lightNode.getPosition()); if (type === LIGHTTYPE_SPOT) { shadowCam.fov = light._outerConeAngle * 2; shadowCamNode.setRotation(lightNode.getRotation()); shadowCamNode.rotateLocal(-90, 0, 0); } else if (type === LIGHTTYPE_OMNI) { if (isClustered) { const tileSize = this.shadowRenderer.lightTextureAtlas.shadowAtlasResolution * light.atlasViewport.z / 3; const texelSize = 2 / tileSize; const filterSize = texelSize * this.shadowRenderer.lightTextureAtlas.shadowEdgePixels; shadowCam.fov = Math.atan(1 + filterSize) * math.RAD_TO_DEG * 2; } else { shadowCam.fov = 90; } } this.renderer.updateCameraFrustum(shadowCam); this.shadowRenderer.cullShadowCasters(comp, light, lightRenderData.visibleCasters, shadowCam, casters); } } prepareLights(shadowLights, lights) { let shadowCamera; for(let i = 0; i < lights.length; i++){ const light = lights[i]; if (this.shadowRenderer.needsShadowRendering(light) && light.atlasViewportAllocated) { shadowLights.push(light); for(let face = 0; face < light.numShadowFaces; face++){ shadowCamera = this.shadowRenderer.prepareFace(light, null, face); } } } return shadowCamera; } buildNonClusteredRenderPasses(frameGraph, localLights) { for(let i = 0; i < localLights.length; i++){ const light = localLights[i]; if (this.shadowRenderer.needsShadowRendering(light)) { const applyVsm = light._type === LIGHTTYPE_SPOT; const faceCount = light.numShadowFaces; for(let face = 0; face < faceCount; face++){ const renderPass = new RenderPassShadowLocalNonClustered(this.device, this.shadowRenderer, light, face, applyVsm); frameGraph.addRenderPass(renderPass); } } } } constructor(renderer, shadowRenderer){ this.shadowLights = []; this.renderer = renderer; this.shadowRenderer = shadowRenderer; this.device = renderer.device; } } class RenderPassShadowDirectional extends RenderPass { execute() { const { light, camera, shadowRenderer, allCascadesRendering } = this; const faceCount = light.numShadowFaces; const shadowUpdateOverrides = light.shadowUpdateOverrides; for(let face = 0; face < faceCount; face++){ if (shadowUpdateOverrides?.[face] !== SHADOWUPDATE_NONE) { shadowRenderer.renderFace(light, camera, face, !allCascadesRendering); } if (shadowUpdateOverrides?.[face] === SHADOWUPDATE_THISFRAME) { shadowUpdateOverrides[face] = SHADOWUPDATE_NONE; } } } after() { this.shadowRenderer.renderVsm(this.light, this.camera); } constructor(device, shadowRenderer, light, camera, allCascadesRendering){ super(device); this.shadowRenderer = shadowRenderer; this.light = light; this.camera = camera; this.allCascadesRendering = allCascadesRendering; } } const visibleSceneAabb = new BoundingBox(); const center = new Vec3(); const shadowCamView$1 = new Mat4(); const aabbPoints = [ new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3() ]; const _depthRange = { min: 0, max: 0 }; function getDepthRange(cameraViewMatrix, aabbMin, aabbMax) { aabbPoints[0].x = aabbPoints[1].x = aabbPoints[2].x = aabbPoints[3].x = aabbMin.x; aabbPoints[1].y = aabbPoints[3].y = aabbPoints[7].y = aabbPoints[5].y = aabbMin.y; aabbPoints[2].z = aabbPoints[3].z = aabbPoints[6].z = aabbPoints[7].z = aabbMin.z; aabbPoints[4].x = aabbPoints[5].x = aabbPoints[6].x = aabbPoints[7].x = aabbMax.x; aabbPoints[0].y = aabbPoints[2].y = aabbPoints[4].y = aabbPoints[6].y = aabbMax.y; aabbPoints[0].z = aabbPoints[1].z = aabbPoints[4].z = aabbPoints[5].z = aabbMax.z; let minz = 9999999999; let maxz = -9999999999; for(let i = 0; i < 8; ++i){ cameraViewMatrix.transformPoint(aabbPoints[i], aabbPoints[i]); const z = aabbPoints[i].z; if (z < minz) minz = z; if (z > maxz) maxz = z; } _depthRange.min = minz; _depthRange.max = maxz; return _depthRange; } class ShadowRendererDirectional { cull(light, comp, camera, casters = null) { light.visibleThisFrame = true; if (!light._shadowMap) { light._shadowMap = ShadowMap.create(this.device, light); } const nearDist = camera._nearClip; this.generateSplitDistances(light, nearDist, Math.min(camera._farClip, light.shadowDistance)); const shadowUpdateOverrides = light.shadowUpdateOverrides; for(let cascade = 0; cascade < light.numCascades; cascade++){ if (shadowUpdateOverrides?.[cascade] === SHADOWUPDATE_NONE) { break; } const lightRenderData = light.getRenderData(camera, cascade); const shadowCam = lightRenderData.shadowCamera; shadowCam.renderTarget = light._shadowMap.renderTargets[0]; lightRenderData.shadowViewport.copy(light.cascades[cascade]); lightRenderData.shadowScissor.copy(light.cascades[cascade]); const shadowCamNode = shadowCam._node; const lightNode = light._node; shadowCamNode.setPosition(lightNode.getPosition()); shadowCamNode.setRotation(lightNode.getRotation()); shadowCamNode.rotateLocal(-90, 0, 0); const frustumNearDist = cascade === 0 ? nearDist : light._shadowCascadeDistances[cascade - 1]; const frustumFarDist = light._shadowCascadeDistances[cascade]; const frustumPoints = camera.getFrustumCorners(frustumNearDist, frustumFarDist); center.set(0, 0, 0); const cameraWorldMat = camera.node.getWorldTransform(); for(let i = 0; i < 8; i++){ cameraWorldMat.transformPoint(frustumPoints[i], frustumPoints[i]); center.add(frustumPoints[i]); } center.mulScalar(1 / 8); let radius = 0; for(let i = 0; i < 8; i++){ const dist = frustumPoints[i].sub(center).length(); if (dist > radius) { radius = dist; } } const right = shadowCamNode.right; const up = shadowCamNode.up; const lightDir = shadowCamNode.forward; const sizeRatio = 0.25 * light._shadowResolution / radius; const x = Math.ceil(center.dot(up) * sizeRatio) / sizeRatio; const y = Math.ceil(center.dot(right) * sizeRatio) / sizeRatio; const scaledUp = up.mulScalar(x); const scaledRight = right.mulScalar(y); const dot = center.dot(lightDir); const scaledDir = lightDir.mulScalar(dot); center.add2(scaledUp, scaledRight).add(scaledDir); shadowCamNode.setPosition(center); shadowCamNode.translateLocal(0, 0, 1000000); shadowCam.nearClip = 0.01; shadowCam.farClip = 2000000; shadowCam.orthoHeight = radius; this.renderer.updateCameraFrustum(shadowCam); this.shadowRenderer.cullShadowCasters(comp, light, lightRenderData.visibleCasters, shadowCam, casters); const cascadeFlag = 1 << cascade; const visibleCasters = lightRenderData.visibleCasters; const origNumVisibleCasters = visibleCasters.length; let numVisibleCasters = 0; for(let i = 0; i < origNumVisibleCasters; i++){ const meshInstance = visibleCasters[i]; if (meshInstance.shadowCascadeMask & cascadeFlag) { visibleCasters[numVisibleCasters++] = meshInstance; if (numVisibleCasters === 1) { visibleSceneAabb.copy(meshInstance.aabb); } else { visibleSceneAabb.add(meshInstance.aabb); } } } if (origNumVisibleCasters !== numVisibleCasters) { visibleCasters.length = numVisibleCasters; } shadowCamView$1.copy(shadowCamNode.getWorldTransform()).invert(); const depthRange = getDepthRange(shadowCamView$1, visibleSceneAabb.getMin(), visibleSceneAabb.getMax()); shadowCamNode.translateLocal(0, 0, depthRange.max + 0.1); shadowCam.farClip = depthRange.max - depthRange.min + 0.2; lightRenderData.projectionCompensation = radius; } } generateSplitDistances(light, nearDist, farDist) { light._shadowCascadeDistances.fill(farDist); for(let i = 1; i < light.numCascades; i++){ const fraction = i / light.numCascades; const linearDist = nearDist + (farDist - nearDist) * fraction; const logDist = nearDist * (farDist / nearDist) ** fraction; const dist = math.lerp(linearDist, logDist, light.cascadeDistribution); light._shadowCascadeDistances[i - 1] = dist; } } getLightRenderPass(light, camera) { let renderPass = null; if (this.shadowRenderer.needsShadowRendering(light)) { const faceCount = light.numShadowFaces; const shadowUpdateOverrides = light.shadowUpdateOverrides; let allCascadesRendering = true; let shadowCamera; for(let face = 0; face < faceCount; face++){ if (shadowUpdateOverrides?.[face] === SHADOWUPDATE_NONE) { allCascadesRendering = false; } shadowCamera = this.shadowRenderer.prepareFace(light, camera, face); } renderPass = new RenderPassShadowDirectional(this.device, this.shadowRenderer, light, camera, allCascadesRendering); this.shadowRenderer.setupRenderPass(renderPass, shadowCamera, allCascadesRendering); } return renderPass; } constructor(renderer, shadowRenderer){ this.renderer = renderer; this.shadowRenderer = shadowRenderer; this.device = renderer.device; } } const tempSet$1 = new Set(); const shadowCamView = new Mat4(); const shadowCamViewProj = new Mat4(); const pixelOffset = new Float32Array(2); const blurScissorRect = new Vec4(1, 1, 0, 0); const viewportMatrix = new Mat4(); function gauss(x, sigma) { return Math.exp(-(x * x) / (2.0 * sigma * sigma)); } function gaussWeights(kernelSize) { const sigma = (kernelSize - 1) / (2 * 3); const halfWidth = (kernelSize - 1) * 0.5; const values = new Array(kernelSize); let sum = 0.0; for(let i = 0; i < kernelSize; ++i){ values[i] = gauss(i - halfWidth, sigma); sum += values[i]; } for(let i = 0; i < kernelSize; ++i){ values[i] /= sum; } return values; } class ShadowRenderer { static createShadowCamera(shadowType, type, face) { const shadowCam = LightCamera.create(SHADOWCAMERA_NAME, type, face); const shadowInfo = shadowTypeInfo.get(shadowType); const isVsm = shadowInfo?.vsm ?? false; const isPcf = shadowInfo?.pcf ?? false; if (isVsm) { shadowCam.clearColor = new Color(0, 0, 0, 0); } else { shadowCam.clearColor = new Color(1, 1, 1, 1); } shadowCam.clearDepthBuffer = true; shadowCam.clearStencilBuffer = false; shadowCam.clearColorBuffer = !isPcf; return shadowCam; } _cullShadowCastersInternal(meshInstances, visible, camera) { const numInstances = meshInstances.length; for(let i = 0; i < numInstances; i++){ const meshInstance = meshInstances[i]; if (meshInstance.castShadow) { if (!meshInstance.cull || meshInstance._isVisible(camera)) { meshInstance.visibleThisFrame = true; visible.push(meshInstance); } } } } cullShadowCasters(comp, light, visible, camera, casters) { this.renderer.scene?.fire(EVENT_PRECULL, camera); visible.length = 0; if (casters) { this._cullShadowCastersInternal(casters, visible, camera); } else { const layers = comp.layerList; const len = layers.length; for(let i = 0; i < len; i++){ const layer = layers[i]; if (layer._lightsSet.has(light)) { if (!tempSet$1.has(layer)) { tempSet$1.add(layer); this._cullShadowCastersInternal(layer.shadowCasters, visible, camera); } } } tempSet$1.clear(); } visible.sort(this.sortCompareShader); this.renderer.scene?.fire(EVENT_POSTCULL, camera); } sortCompareShader(drawCallA, drawCallB) { const keyA = drawCallA._sortKeyShadow; const keyB = drawCallB._sortKeyShadow; if (keyA === keyB) { return drawCallB.mesh.id - drawCallA.mesh.id; } return keyB - keyA; } setupRenderState(device, light) { const isClustered = this.renderer.scene.clusteredLightingEnabled; const useShadowSampler = isClustered ? light._isPcf : light._isPcf && light._type !== LIGHTTYPE_OMNI; device.setBlendState(useShadowSampler ? this.blendStateNoWrite : this.blendStateWrite); device.setDepthState(light.shadowDepthState); device.setStencilState(null, null); } dispatchUniforms(light, shadowCam, lightRenderData, face) { const shadowCamNode = shadowCam._node; if (light._type !== LIGHTTYPE_DIRECTIONAL) { this.renderer.dispatchViewPos(shadowCamNode.getPosition()); this.shadowMapLightRadiusId.setValue(light.attenuationEnd); } shadowCamView.setTRS(shadowCamNode.getPosition(), shadowCamNode.getRotation(), Vec3.ONE).invert(); shadowCamViewProj.mul2(shadowCam.projectionMatrix, shadowCamView); const rectViewport = lightRenderData.shadowViewport; shadowCam.rect = rectViewport; shadowCam.scissorRect = lightRenderData.shadowScissor; viewportMatrix.setViewport(rectViewport.x, rectViewport.y, rectViewport.z, rectViewport.w); lightRenderData.shadowMatrix.mul2(viewportMatrix, shadowCamViewProj); if (light._type === LIGHTTYPE_DIRECTIONAL) { light._shadowMatrixPalette.set(lightRenderData.shadowMatrix.data, face * 16); } } getShadowPass(light) { const lightType = light._type; const shadowType = light._shadowType; let shadowPassInfo = this.shadowPassCache[lightType]?.[shadowType]; if (!shadowPassInfo) { const shadowPassName = `ShadowPass_${lightType}_${shadowType}`; shadowPassInfo = ShaderPass.get(this.device).allocate(shadowPassName, { isShadow: true, lightType: lightType, shadowType: shadowType }); if (!this.shadowPassCache[lightType]) { this.shadowPassCache[lightType] = []; } this.shadowPassCache[lightType][shadowType] = shadowPassInfo; } return shadowPassInfo.index; } submitCasters(visibleCasters, light, camera) { const device = this.device; const renderer = this.renderer; const scene = renderer.scene; const passFlags = 1 << SHADER_SHADOW; const shadowPass = this.getShadowPass(light); const cameraShaderParams = camera.shaderParams; const flipFactor = camera.renderTarget.flipY ? -1 : 1; const count = visibleCasters.length; for(let i = 0; i < count; i++){ const meshInstance = visibleCasters[i]; const mesh = meshInstance.mesh; const instancingData = meshInstance.instancingData; if (instancingData && instancingData.count <= 0) { continue; } meshInstance.ensureMaterial(device); const material = meshInstance.material; renderer.setBaseConstants(device, material); renderer.setSkinning(device, meshInstance); if (material.dirty) { material.updateUniforms(device, scene); material.dirty = false; } renderer.setupCullMode(true, flipFactor, meshInstance); material.setParameters(device); meshInstance.setParameters(device, passFlags); const shaderInstance = meshInstance.getShaderInstance(shadowPass, 0, scene, cameraShaderParams, this.viewUniformFormat, this.viewBindGroupFormat); const shadowShader = shaderInstance.shader; if (shadowShader.failed) continue; meshInstance._sortKeyShadow = shadowShader.id; device.setShader(shadowShader); renderer.setVertexBuffers(device, mesh); renderer.setMorphing(device, meshInstance.morphInstance); if (instancingData) { device.setVertexBuffer(instancingData.vertexBuffer); } renderer.setMeshInstanceMatrices(meshInstance); renderer.setupMeshUniformBuffers(shaderInstance); const style = meshInstance.renderStyle; const indirectData = meshInstance.getDrawCommands(camera); device.draw(mesh.primitive[style], mesh.indexBuffer[style], instancingData?.count, indirectData); renderer._shadowDrawCalls++; if (instancingData) { renderer._instancedDrawCalls++; } } } needsShadowRendering(light) { const needs = light.enabled && light.castShadows && light.shadowUpdateMode !== SHADOWUPDATE_NONE && light.visibleThisFrame; if (light.shadowUpdateMode === SHADOWUPDATE_THISFRAME) { light.shadowUpdateMode = SHADOWUPDATE_NONE; } if (needs) { this.renderer._shadowMapUpdates += light.numShadowFaces; } return needs; } getLightRenderData(light, camera, face) { return light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, face); } setupRenderPass(renderPass, shadowCamera, clearRenderTarget) { const rt = shadowCamera.renderTarget; renderPass.init(rt); renderPass.depthStencilOps.clearDepthValue = 1; renderPass.depthStencilOps.clearDepth = clearRenderTarget; if (rt.depthBuffer) { renderPass.depthStencilOps.storeDepth = true; } else { renderPass.colorOps.clearValue.copy(shadowCamera.clearColor); renderPass.colorOps.clear = clearRenderTarget; renderPass.depthStencilOps.storeDepth = false; } renderPass.requiresCubemaps = false; } prepareFace(light, camera, face) { const type = light._type; const lightRenderData = this.getLightRenderData(light, camera, face); const shadowCam = lightRenderData.shadowCamera; const renderTargetIndex = type === LIGHTTYPE_DIRECTIONAL ? 0 : face; shadowCam.renderTarget = light._shadowMap.renderTargets[renderTargetIndex]; return shadowCam; } renderFace(light, camera, face, clear) { const device = this.device; const lightRenderData = this.getLightRenderData(light, camera, face); const shadowCam = lightRenderData.shadowCamera; this.dispatchUniforms(light, shadowCam, lightRenderData, face); const rt = shadowCam.renderTarget; const renderer = this.renderer; renderer.setCameraUniforms(shadowCam, rt); if (device.supportsUniformBuffers) { renderer.setupViewUniformBuffers(lightRenderData.viewBindGroups, this.viewUniformFormat, this.viewBindGroupFormat, null); } renderer.setupViewport(shadowCam, rt); if (clear) { renderer.clear(shadowCam); } this.setupRenderState(device, light); this.submitCasters(lightRenderData.visibleCasters, light, shadowCam); } renderVsm(light, camera) { if (light._isVsm && light._vsmBlurSize > 1) { const isClustered = this.renderer.scene.clusteredLightingEnabled; if (!isClustered || light._type === LIGHTTYPE_DIRECTIONAL) { this.applyVsmBlur(light, camera); } } } getVsmBlurShader(blurMode, filterSize) { const cache = this.blurVsmShader; let blurShader = cache[blurMode][filterSize]; if (!blurShader) { this.blurVsmWeights[filterSize] = gaussWeights(filterSize); const defines = new Map(); defines.set('{SAMPLES}', filterSize); if (blurMode === 1) defines.set('GAUSS', ''); blurShader = ShaderUtils.createShader(this.device, { uniqueName: `blurVsm${blurMode}${filterSize}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'fullscreenQuadVS', fragmentChunk: 'blurVSMPS', fragmentDefines: defines }); cache[blurMode][filterSize] = blurShader; } return blurShader; } applyVsmBlur(light, camera) { const device = this.device; device.setBlendState(BlendState.NOBLEND); const lightRenderData = light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, 0); const shadowCam = lightRenderData.shadowCamera; const origShadowMap = shadowCam.renderTarget; const tempShadowMap = this.renderer.shadowMapCache.get(device, light); const tempRt = tempShadowMap.renderTargets[0]; const blurMode = light.vsmBlurMode; const filterSize = light._vsmBlurSize; const blurShader = this.getVsmBlurShader(blurMode, filterSize); blurScissorRect.z = light._shadowResolution - 2; blurScissorRect.w = blurScissorRect.z; this.sourceId.setValue(origShadowMap.colorBuffer); pixelOffset[0] = 1 / light._shadowResolution; pixelOffset[1] = 0; this.pixelOffsetId.setValue(pixelOffset); if (blurMode === BLUR_GAUSSIAN) this.weightId.setValue(this.blurVsmWeights[filterSize]); drawQuadWithShader(device, tempRt, blurShader, null, blurScissorRect); this.sourceId.setValue(tempRt.colorBuffer); pixelOffset[1] = pixelOffset[0]; pixelOffset[0] = 0; this.pixelOffsetId.setValue(pixelOffset); drawQuadWithShader(device, origShadowMap, blurShader, null, blurScissorRect); this.renderer.shadowMapCache.add(light, tempShadowMap); } initViewBindGroupFormat() { if (this.device.supportsUniformBuffers && !this.viewUniformFormat) { this.viewUniformFormat = new UniformBufferFormat(this.device, [ new UniformFormat('matrix_viewProjection', UNIFORMTYPE_MAT4) ]); this.viewBindGroupFormat = new BindGroupFormat(this.device, [ new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) ]); } } frameUpdate() { this.initViewBindGroupFormat(); } constructor(renderer, lightTextureAtlas){ this.shadowPassCache = []; this.device = renderer.device; this.renderer = renderer; this.lightTextureAtlas = lightTextureAtlas; const scope = this.device.scope; this.sourceId = scope.resolve('source'); this.pixelOffsetId = scope.resolve('pixelOffset'); this.weightId = scope.resolve('weight[0]'); this.blurVsmShader = [ {}, {} ]; this.blurVsmWeights = {}; this.shadowMapLightRadiusId = scope.resolve('light_radius'); this.viewUniformFormat = null; this.viewBindGroupFormat = null; this.blendStateWrite = new BlendState(); this.blendStateNoWrite = new BlendState(); this.blendStateNoWrite.setColorWrite(false, false, false, false); } } const tempClusterArray = []; class WorldClustersAllocator { destroy() { if (this._empty) { this._empty.destroy(); this._empty = null; } this._allocated.forEach((cluster)=>{ cluster.destroy(); }); this._allocated.length = 0; } get count() { return this._allocated.length; } get empty() { if (!this._empty) { const empty = new WorldClusters(this.device); empty.name = 'ClusterEmpty'; empty.update([]); this._empty = empty; } return this._empty; } assign(renderPasses) { tempClusterArray.push(...this._allocated); this._allocated.length = 0; this._clusters.clear(); const passCount = renderPasses.length; for(let p = 0; p < passCount; p++){ const renderPass = renderPasses[p]; const renderActions = renderPass.renderActions; if (renderActions) { const count = renderActions.length; for(let i = 0; i < count; i++){ const ra = renderActions[i]; ra.lightClusters = null; const layer = ra.layer; if (layer.hasClusteredLights && layer.meshInstances.length) { const hash = layer.getLightIdHash(); const existingRenderAction = this._clusters.get(hash); let clusters = existingRenderAction?.lightClusters; if (!clusters) { clusters = tempClusterArray.pop() ?? new WorldClusters(this.device); this._allocated.push(clusters); this._clusters.set(hash, ra); } ra.lightClusters = clusters; } if (!ra.lightClusters) { ra.lightClusters = this.empty; } } } } tempClusterArray.forEach((item)=>item.destroy()); tempClusterArray.length = 0; } update(renderPasses, lighting) { this.assign(renderPasses); this._clusters.forEach((renderAction)=>{ const layer = renderAction.layer; const cluster = renderAction.lightClusters; cluster.update(layer.clusteredLightsSet, lighting); }); } constructor(graphicsDevice){ this._empty = null; this._allocated = []; this._clusters = new Map(); this.device = graphicsDevice; } } const _viewport = new Vec4(); const _invViewProjMatrices = []; class RenderPassCookieRenderer extends RenderPass { destroy() { this._quadRenderer2D?.destroy(); this._quadRenderer2D = null; this._quadRendererCube?.destroy(); this._quadRendererCube = null; this._evtDeviceRestored?.off(); this._evtDeviceRestored = null; } static create(renderTarget, cubeSlotsOffsets) { const renderPass = new RenderPassCookieRenderer(renderTarget.device, cubeSlotsOffsets); renderPass.init(renderTarget); renderPass.colorOps.clear = false; renderPass.depthStencilOps.clearDepth = false; return renderPass; } onDeviceRestored() { this._forceCopy = true; } update(lights) { const filteredLights = this._filteredLights; this.filter(lights, filteredLights); this.executeEnabled = filteredLights.length > 0; } filter(lights, filteredLights) { for(let i = 0; i < lights.length; i++){ const light = lights[i]; if (light._type === LIGHTTYPE_DIRECTIONAL) { continue; } if (!light.atlasViewportAllocated) { continue; } if (!light.atlasSlotUpdated && !this._forceCopy) { continue; } if (light.enabled && light.cookie && light.visibleThisFrame) { filteredLights.push(light); } } this._forceCopy = false; } initInvViewProjMatrices() { if (!_invViewProjMatrices.length) { for(let face = 0; face < 6; face++){ const camera = LightCamera.create(null, LIGHTTYPE_OMNI, face); const projMat = camera.projectionMatrix; const viewMat = camera.node.getLocalTransform().clone().invert(); _invViewProjMatrices[face] = new Mat4().mul2(projMat, viewMat).invert(); } } } get quadRenderer2D() { if (!this._quadRenderer2D) { const shader = ShaderUtils.createShader(this.device, { uniqueName: 'cookieRenderer2d', attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'cookieBlitVS', fragmentChunk: 'cookieBlit2DPS' }); this._quadRenderer2D = new QuadRender(shader); } return this._quadRenderer2D; } get quadRendererCube() { if (!this._quadRendererCube) { const shader = ShaderUtils.createShader(this.device, { uniqueName: 'cookieRendererCube', attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'cookieBlitVS', fragmentChunk: 'cookieBlitCubePS' }); this._quadRendererCube = new QuadRender(shader); } return this._quadRendererCube; } execute() { const device = this.device; device.setBlendState(BlendState.NOBLEND); device.setCullMode(CULLFACE_NONE); device.setDepthState(DepthState.NODEPTH); device.setStencilState(); const renderTargetWidth = this.renderTarget.colorBuffer.width; const cubeSlotsOffsets = this._cubeSlotsOffsets; const filteredLights = this._filteredLights; for(let i = 0; i < filteredLights.length; i++){ const light = filteredLights[i]; const faceCount = light.numShadowFaces; const quad = faceCount > 1 ? this.quadRendererCube : this.quadRenderer2D; if (faceCount > 1) { this.initInvViewProjMatrices(); } this.blitTextureId.setValue(light.cookie); for(let face = 0; face < faceCount; face++){ _viewport.copy(light.atlasViewport); if (faceCount > 1) { const smallSize = _viewport.z / 3; const offset = cubeSlotsOffsets[face]; _viewport.x += smallSize * offset.x; _viewport.y += smallSize * offset.y; _viewport.z = smallSize; _viewport.w = smallSize; this.invViewProjId.setValue(_invViewProjMatrices[face].data); } _viewport.mulScalar(renderTargetWidth); quad.render(_viewport); } } filteredLights.length = 0; } constructor(device, cubeSlotsOffsets){ super(device), this._quadRenderer2D = null, this._quadRendererCube = null, this._filteredLights = [], this._forceCopy = false, this._evtDeviceRestored = null; this._cubeSlotsOffsets = cubeSlotsOffsets; this.requiresCubemaps = false; this.blitTextureId = device.scope.resolve('blitTexture'); this.invViewProjId = device.scope.resolve('invViewProj'); this._evtDeviceRestored = device.on('devicerestored', this.onDeviceRestored, this); } } class RenderPassShadowLocalClustered extends RenderPass { update(localLights) { const shadowLights = this.shadowRendererLocal.shadowLights; const shadowCamera = this.shadowRendererLocal.prepareLights(shadowLights, localLights); const count = shadowLights.length; this.enabled = count > 0; if (count) { this.shadowRenderer.setupRenderPass(this, shadowCamera, false); } } execute() { const shadowLights = this.shadowRendererLocal.shadowLights; const count = shadowLights.length; for(let i = 0; i < count; i++){ const light = shadowLights[i]; for(let face = 0; face < light.numShadowFaces; face++){ this.shadowRenderer.renderFace(light, null, face, true); } } shadowLights.length = 0; } constructor(device, shadowRenderer, shadowRendererLocal){ super(device); this.requiresCubemaps = false; this.shadowRenderer = shadowRenderer; this.shadowRendererLocal = shadowRendererLocal; } } class RenderPassUpdateClustered extends RenderPass { update(frameGraph, shadowsEnabled, cookiesEnabled, lights, localLights) { this.frameGraph = frameGraph; this.cookiesRenderPass.enabled = cookiesEnabled; if (cookiesEnabled) { this.cookiesRenderPass.update(lights); } this.shadowRenderPass.enabled = shadowsEnabled; if (shadowsEnabled) { this.shadowRenderPass.update(localLights); } } destroy() { this.cookiesRenderPass.destroy(); this.cookiesRenderPass = null; } execute() { const { renderer } = this; const { scene } = renderer; renderer.worldClustersAllocator.update(this.frameGraph.renderPasses, scene.lighting); } constructor(device, renderer, shadowRenderer, shadowRendererLocal, lightTextureAtlas){ super(device); this.renderer = renderer; this.frameGraph = null; this.cookiesRenderPass = RenderPassCookieRenderer.create(lightTextureAtlas.cookieRenderTarget, lightTextureAtlas.cubeSlotsOffsets); this.beforePasses.push(this.cookiesRenderPass); this.shadowRenderPass = new RenderPassShadowLocalClustered(device, shadowRenderer, shadowRendererLocal); this.beforePasses.push(this.shadowRenderPass); } } let _skinUpdateIndex = 0; const viewProjMat = new Mat4(); const viewInvMat = new Mat4(); const viewMat = new Mat4(); const viewMat3 = new Mat3(); const tempSphere$1 = new BoundingSphere(); const _flipYMat = new Mat4().setScale(1, -1, 1); const _tempLightSet = new Set(); const _tempLayerSet = new Set(); const _dynamicBindGroup = new DynamicBindGroup(); const _fixProjRangeMat = new Mat4().set([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1 ]); const _haltonSequence = [ new Vec2(0.5, 0.333333), new Vec2(0.25, 0.666667), new Vec2(0.75, 0.111111), new Vec2(0.125, 0.444444), new Vec2(0.625, 0.777778), new Vec2(0.375, 0.222222), new Vec2(0.875, 0.555556), new Vec2(0.0625, 0.888889), new Vec2(0.5625, 0.037037), new Vec2(0.3125, 0.370370), new Vec2(0.8125, 0.703704), new Vec2(0.1875, 0.148148), new Vec2(0.6875, 0.481481), new Vec2(0.4375, 0.814815), new Vec2(0.9375, 0.259259), new Vec2(0.03125, 0.592593) ]; const _tempProjMat0 = new Mat4(); const _tempProjMat1 = new Mat4(); const _tempProjMat2 = new Mat4(); const _tempProjMat3 = new Mat4(); const _tempProjMat4 = new Mat4(); const _tempProjMat5 = new Mat4(); const _tempSet = new Set(); const _tempMeshInstances = []; const _tempMeshInstancesSkinned = []; class Renderer { destroy() { this.shadowRenderer = null; this._shadowRendererLocal = null; this._shadowRendererDirectional = null; this.shadowMapCache.destroy(); this.shadowMapCache = null; this._renderPassUpdateClustered?.destroy(); this._renderPassUpdateClustered = null; this.lightTextureAtlas.destroy(); this.lightTextureAtlas = null; this.gsplatDirector?.destroy(); this.gsplatDirector = null; } setupViewport(camera, renderTarget) { const device = this.device; const pixelWidth = renderTarget ? renderTarget.width : device.width; const pixelHeight = renderTarget ? renderTarget.height : device.height; const rect = camera.rect; let x = Math.floor(rect.x * pixelWidth); let y = Math.floor(rect.y * pixelHeight); let w = Math.floor(rect.z * pixelWidth); let h = Math.floor(rect.w * pixelHeight); device.setViewport(x, y, w, h); if (camera._scissorRectClear) { const scissorRect = camera.scissorRect; x = Math.floor(scissorRect.x * pixelWidth); y = Math.floor(scissorRect.y * pixelHeight); w = Math.floor(scissorRect.z * pixelWidth); h = Math.floor(scissorRect.w * pixelHeight); } device.setScissor(x, y, w, h); } setCameraUniforms(camera, target) { const flipY = target?.flipY; let viewList = null; if (camera.xr && camera.xr.session) { const transform = camera._node?.parent?.getWorldTransform() || null; const views = camera.xr.views; viewList = views.list; for(let v = 0; v < viewList.length; v++){ const view = viewList[v]; view.updateTransforms(transform); camera.frustum.setFromMat4(view.projViewOffMat); } } else { let projMat = camera.projectionMatrix; if (camera.calculateProjection) { camera.calculateProjection(projMat, VIEW_CENTER); } let projMatSkybox = camera.getProjectionMatrixSkybox(); if (flipY) { projMat = _tempProjMat0.mul2(_flipYMat, projMat); projMatSkybox = _tempProjMat1.mul2(_flipYMat, projMatSkybox); } if (this.device.isWebGPU) { projMat = _tempProjMat2.mul2(_fixProjRangeMat, projMat); projMatSkybox = _tempProjMat3.mul2(_fixProjRangeMat, projMatSkybox); } const { jitter } = camera; let jitterX = 0; let jitterY = 0; if (jitter > 0) { const targetWidth = target ? target.width : this.device.width; const targetHeight = target ? target.height : this.device.height; const offset = _haltonSequence[this.device.renderVersion % _haltonSequence.length]; jitterX = jitter * (offset.x * 2 - 1) / targetWidth; jitterY = jitter * (offset.y * 2 - 1) / targetHeight; projMat = _tempProjMat4.copy(projMat); projMat.data[8] = jitterX; projMat.data[9] = jitterY; projMatSkybox = _tempProjMat5.copy(projMatSkybox); projMatSkybox.data[8] = jitterX; projMatSkybox.data[9] = jitterY; if (this.blueNoiseJitterVersion !== this.device.renderVersion) { this.blueNoiseJitterVersion = this.device.renderVersion; this.blueNoise.vec4(this.blueNoiseJitterVec); } } const jitterVec = jitter > 0 ? this.blueNoiseJitterVec : Vec4.ZERO; this.blueNoiseJitterData[0] = jitterVec.x; this.blueNoiseJitterData[1] = jitterVec.y; this.blueNoiseJitterData[2] = jitterVec.z; this.blueNoiseJitterData[3] = jitterVec.w; this.blueNoiseJitterId.setValue(this.blueNoiseJitterData); this.projId.setValue(projMat.data); this.projSkyboxId.setValue(projMatSkybox.data); if (camera.calculateTransform) { camera.calculateTransform(viewInvMat, VIEW_CENTER); } else { const pos = camera._node.getPosition(); const rot = camera._node.getRotation(); viewInvMat.setTRS(pos, rot, Vec3.ONE); } this.viewInvId.setValue(viewInvMat.data); viewMat.copy(viewInvMat).invert(); this.viewId.setValue(viewMat.data); viewMat3.setFromMat4(viewMat); this.viewId3.setValue(viewMat3.data); viewProjMat.mul2(projMat, viewMat); this.viewProjId.setValue(viewProjMat.data); camera._storeShaderMatrices(viewProjMat, jitterX, jitterY, this.device.renderVersion); this.flipYId.setValue(flipY ? -1 : 1); this.dispatchViewPos(camera._node.getPosition()); camera.frustum.setFromMat4(viewProjMat); } this.tbnBasis.setValue(flipY ? -1 : 1); this.cameraParamsId.setValue(camera.fillShaderParams(this.cameraParams)); let viewportWidth = target ? target.width : this.device.width; let viewportHeight = target ? target.height : this.device.height; viewportWidth *= camera.rect.z; viewportHeight *= camera.rect.w; if (camera.xr?.active && camera.xr.views.list.length === 2) { viewportWidth *= 0.5; } this.viewportSize[0] = viewportWidth; this.viewportSize[1] = viewportHeight; this.viewportSize[2] = 1 / viewportWidth; this.viewportSize[3] = 1 / viewportHeight; this.viewportSizeId.setValue(this.viewportSize); this.exposureId.setValue(this.scene.physicalUnits ? camera.getExposure() : this.scene.exposure); return viewList; } clear(camera, clearColor, clearDepth, clearStencil) { const flags = (clearColor ?? camera._clearColorBuffer ? CLEARFLAG_COLOR : 0) | (clearDepth ?? camera._clearDepthBuffer ? CLEARFLAG_DEPTH : 0) | (clearStencil ?? camera._clearStencilBuffer ? CLEARFLAG_STENCIL : 0); if (flags) { const device = this.device; device.clear({ color: [ camera._clearColor.r, camera._clearColor.g, camera._clearColor.b, camera._clearColor.a ], depth: camera._clearDepth, stencil: camera._clearStencil, flags: flags }); } } setupCullMode(cullFaces, flipFactor, drawCall) { const material = drawCall.material; let mode = CULLFACE_NONE; if (cullFaces) { let flipFaces = 1; if (material.cull === CULLFACE_FRONT || material.cull === CULLFACE_BACK) { flipFaces = flipFactor * drawCall.flipFacesFactor * drawCall.node.worldScaleSign; } if (flipFaces < 0) { mode = material.cull === CULLFACE_FRONT ? CULLFACE_BACK : CULLFACE_FRONT; } else { mode = material.cull; } } this.device.setCullMode(mode); if (mode === CULLFACE_NONE && material.cull === CULLFACE_NONE) { this.twoSidedLightingNegScaleFactorId.setValue(drawCall.node.worldScaleSign); } } updateCameraFrustum(camera) { if (camera.xr && camera.xr.views.list.length) { const view = camera.xr.views.list[0]; viewProjMat.mul2(view.projMat, view.viewOffMat); camera.frustum.setFromMat4(viewProjMat); return; } const projMat = camera.projectionMatrix; if (camera.calculateProjection) { camera.calculateProjection(projMat, VIEW_CENTER); } if (camera.calculateTransform) { camera.calculateTransform(viewInvMat, VIEW_CENTER); } else { const pos = camera._node.getPosition(); const rot = camera._node.getRotation(); viewInvMat.setTRS(pos, rot, Vec3.ONE); this.viewInvId.setValue(viewInvMat.data); } viewMat.copy(viewInvMat).invert(); viewProjMat.mul2(projMat, viewMat); camera.frustum.setFromMat4(viewProjMat); } setBaseConstants(device, material) { device.setCullMode(material.cull); if (material.opacityMap) { this.opacityMapId.setValue(material.opacityMap); } if (material.opacityMap || material.alphaTest > 0) { this.alphaTestId.setValue(material.alphaTest); } } updateCpuSkinMatrices(drawCalls) { _skinUpdateIndex++; const drawCallsCount = drawCalls.length; if (drawCallsCount === 0) return; for(let i = 0; i < drawCallsCount; i++){ const si = drawCalls[i].skinInstance; if (si) { si.updateMatrices(drawCalls[i].node, _skinUpdateIndex); si._dirty = true; } } } updateGpuSkinMatrices(drawCalls) { for (const drawCall of drawCalls){ const skin = drawCall.skinInstance; if (skin && skin._dirty) { skin.updateMatrixPalette(drawCall.node, _skinUpdateIndex); skin._dirty = false; } } } updateMorphing(drawCalls) { for (const drawCall of drawCalls){ const morphInst = drawCall.morphInstance; if (morphInst && morphInst._dirty) { morphInst.update(); } } } updateGSplats(drawCalls) { for (const drawCall of drawCalls){ drawCall.gsplatInstance?.update(); } } gpuUpdate(drawCalls) { this.updateGpuSkinMatrices(drawCalls); this.updateMorphing(drawCalls); this.updateGSplats(drawCalls); } setVertexBuffers(device, mesh) { device.setVertexBuffer(mesh.vertexBuffer); } setMorphing(device, morphInstance) { if (morphInstance) { morphInstance.prepareRendering(device); device.setVertexBuffer(morphInstance.morph.vertexBufferIds); this.morphPositionTex.setValue(morphInstance.texturePositions); this.morphNormalTex.setValue(morphInstance.textureNormals); this.morphTexParams.setValue(morphInstance._textureParams); } } setSkinning(device, meshInstance) { const skinInstance = meshInstance.skinInstance; if (skinInstance) { this._skinDrawCalls++; const boneTexture = skinInstance.boneTexture; this.boneTextureId.setValue(boneTexture); } } dispatchViewPos(position) { const vp = this.viewPos; vp[0] = position.x; vp[1] = position.y; vp[2] = position.z; this.viewPosId.setValue(vp); } initViewBindGroupFormat(isClustered) { if (this.device.supportsUniformBuffers && !this.viewUniformFormat) { const uniforms = [ new UniformFormat('matrix_view', UNIFORMTYPE_MAT4), new UniformFormat('matrix_viewInverse', UNIFORMTYPE_MAT4), new UniformFormat('matrix_projection', UNIFORMTYPE_MAT4), new UniformFormat('matrix_projectionSkybox', UNIFORMTYPE_MAT4), new UniformFormat('matrix_viewProjection', UNIFORMTYPE_MAT4), new UniformFormat('matrix_view3', UNIFORMTYPE_MAT3), new UniformFormat('cubeMapRotationMatrix', UNIFORMTYPE_MAT3), new UniformFormat('view_position', UNIFORMTYPE_VEC3), new UniformFormat('viewport_size', UNIFORMTYPE_VEC4), new UniformFormat('skyboxIntensity', UNIFORMTYPE_FLOAT), new UniformFormat('exposure', UNIFORMTYPE_FLOAT), new UniformFormat('textureBias', UNIFORMTYPE_FLOAT), new UniformFormat('view_index', UNIFORMTYPE_FLOAT) ]; if (isClustered) { uniforms.push(...[ new UniformFormat('clusterCellsCountByBoundsSize', UNIFORMTYPE_VEC3), new UniformFormat('clusterBoundsMin', UNIFORMTYPE_VEC3), new UniformFormat('clusterBoundsDelta', UNIFORMTYPE_VEC3), new UniformFormat('clusterCellsDot', UNIFORMTYPE_IVEC3), new UniformFormat('clusterCellsMax', UNIFORMTYPE_IVEC3), new UniformFormat('shadowAtlasParams', UNIFORMTYPE_VEC2), new UniformFormat('clusterMaxCells', UNIFORMTYPE_INT), new UniformFormat('numClusteredLights', UNIFORMTYPE_INT), new UniformFormat('clusterTextureWidth', UNIFORMTYPE_INT) ]); } this.viewUniformFormat = new UniformBufferFormat(this.device, uniforms); const formats = [ new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT) ]; this.viewBindGroupFormat = new BindGroupFormat(this.device, formats); } } setupViewUniforms(view, index) { this.projId.setValue(view.projMat.data); this.projSkyboxId.setValue(view.projMat.data); this.viewId.setValue(view.viewOffMat.data); this.viewInvId.setValue(view.viewInvOffMat.data); this.viewId3.setValue(view.viewMat3.data); this.viewProjId.setValue(view.projViewOffMat.data); this.viewPosId.setValue(view.positionData); this.viewIndexId.setValue(index); } setupViewUniformBuffers(viewBindGroups, viewUniformFormat, viewBindGroupFormat, viewList) { const { device } = this; const viewCount = viewList?.length ?? 1; while(viewBindGroups.length < viewCount){ const ub = new UniformBuffer(device, viewUniformFormat, false); const bg = new BindGroup(device, viewBindGroupFormat, ub); viewBindGroups.push(bg); } if (viewList) { for(let i = 0; i < viewCount; i++){ const view = viewList[i]; this.setupViewUniforms(view, i); const viewBindGroup = viewBindGroups[i]; viewBindGroup.defaultUniformBuffer.update(); viewBindGroup.update(); } } else { const viewBindGroup = viewBindGroups[0]; viewBindGroup.defaultUniformBuffer.update(); viewBindGroup.update(); } if (!viewList) { device.setBindGroup(BINDGROUP_VIEW, viewBindGroups[0]); } } setupMeshUniformBuffers(shaderInstance) { const device = this.device; if (device.supportsUniformBuffers) { const meshBindGroup = shaderInstance.getBindGroup(device); meshBindGroup.update(); device.setBindGroup(BINDGROUP_MESH, meshBindGroup); const meshUniformBuffer = shaderInstance.getUniformBuffer(device); meshUniformBuffer.update(_dynamicBindGroup); device.setBindGroup(BINDGROUP_MESH_UB, _dynamicBindGroup.bindGroup, _dynamicBindGroup.offsets); } } setMeshInstanceMatrices(meshInstance, setNormalMatrix = false) { const modelMatrix = meshInstance.node.worldTransform; this.modelMatrixId.setValue(modelMatrix.data); if (setNormalMatrix) { this.normalMatrixId.setValue(meshInstance.node.normalMatrix.data); } } cull(camera, drawCalls, culledInstances) { const opaque = culledInstances.opaque; opaque.length = 0; const transparent = culledInstances.transparent; transparent.length = 0; const doCull = camera.frustumCulling; const count = drawCalls.length; for(let i = 0; i < count; i++){ const drawCall = drawCalls[i]; if (drawCall.visible) { const visible = !doCull || !drawCall.cull || drawCall._isVisible(camera); if (visible) { drawCall.visibleThisFrame = true; const bucket = drawCall.transparent ? transparent : opaque; bucket.push(drawCall); if (drawCall.skinInstance || drawCall.morphInstance || drawCall.gsplatInstance) { this.processingMeshInstances.add(drawCall); if (drawCall.gsplatInstance) { drawCall.gsplatInstance.cameras.push(camera); } } } } } } collectLights(comp) { this.lights.length = 0; this.localLights.length = 0; const stats = this.scene._stats; const count = comp.layerList.length; for(let i = 0; i < count; i++){ const layer = comp.layerList[i]; if (!_tempLayerSet.has(layer)) { _tempLayerSet.add(layer); const lights = layer._lights; for(let j = 0; j < lights.length; j++){ const light = lights[j]; if (!_tempLightSet.has(light)) { _tempLightSet.add(light); this.lights.push(light); if (light._type !== LIGHTTYPE_DIRECTIONAL) { this.localLights.push(light); } } } } } stats.lights = this.lights.length; _tempLightSet.clear(); _tempLayerSet.clear(); } cullLights(camera, lights) { const clusteredLightingEnabled = this.scene.clusteredLightingEnabled; const physicalUnits = this.scene.physicalUnits; for(let i = 0; i < lights.length; i++){ const light = lights[i]; if (light.enabled) { if (light._type !== LIGHTTYPE_DIRECTIONAL) { light.getBoundingSphere(tempSphere$1); if (camera.frustum.containsSphere(tempSphere$1)) { light.visibleThisFrame = true; light.usePhysicalUnits = physicalUnits; const screenSize = camera.getScreenSize(tempSphere$1); light.maxScreenSize = Math.max(light.maxScreenSize, screenSize); } else { if (!clusteredLightingEnabled) { if (light.castShadows && !light.shadowMap) { light.visibleThisFrame = true; } } } } else { light.usePhysicalUnits = this.scene.physicalUnits; } } } } cullShadowmaps(comp) { const isClustered = this.scene.clusteredLightingEnabled; for(let i = 0; i < this.localLights.length; i++){ const light = this.localLights[i]; if (light._type !== LIGHTTYPE_DIRECTIONAL) { if (isClustered) { if (light.atlasSlotUpdated && light.shadowUpdateMode === SHADOWUPDATE_NONE) { light.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } else { if (light.shadowUpdateMode === SHADOWUPDATE_NONE && light.castShadows) { if (!light.getRenderData(null, 0).shadowCamera.renderTarget) { light.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } } if (light.visibleThisFrame && light.castShadows && light.shadowUpdateMode !== SHADOWUPDATE_NONE) { this._shadowRendererLocal.cull(light, comp); } } } this.cameraDirShadowLights.clear(); const cameras = comp.cameras; for(let i = 0; i < cameras.length; i++){ const cameraComponent = cameras[i]; if (cameraComponent.enabled) { const camera = cameraComponent.camera; let lightList; const cameraLayers = camera.layers; for(let l = 0; l < cameraLayers.length; l++){ const cameraLayer = comp.getLayerById(cameraLayers[l]); if (cameraLayer) { const layerDirLights = cameraLayer.splitLights[LIGHTTYPE_DIRECTIONAL]; for(let j = 0; j < layerDirLights.length; j++){ const light = layerDirLights[j]; if (light.castShadows && !_tempSet.has(light)) { _tempSet.add(light); lightList = lightList ?? []; lightList.push(light); this._shadowRendererDirectional.cull(light, comp, camera); } } } } if (lightList) { this.cameraDirShadowLights.set(camera, lightList); } _tempSet.clear(); } } } cullComposition(comp) { const { scene } = this; this.processingMeshInstances.clear(); const numCameras = comp.cameras.length; this._camerasRendered += numCameras; for(let i = 0; i < numCameras; i++){ const camera = comp.cameras[i]; scene?.fire(EVENT_PRECULL, camera); const renderTarget = camera.renderTarget; camera.frameUpdate(renderTarget); this.updateCameraFrustum(camera.camera); const layerIds = camera.layers; for(let j = 0; j < layerIds.length; j++){ const layer = comp.getLayerById(layerIds[j]); if (layer && layer.enabled) { this.cullLights(camera.camera, layer._lights); const culledInstances = layer.getCulledInstances(camera.camera); this.cull(camera.camera, layer.meshInstances, culledInstances); } } scene?.fire(EVENT_POSTCULL, camera); } if (scene.clusteredLightingEnabled) { this.updateLightTextureAtlas(); } this.cullShadowmaps(comp); scene?.fire(EVENT_CULL_END); } updateShaders(drawCalls, onlyLitShaders) { const count = drawCalls.length; for(let i = 0; i < count; i++){ const mat = drawCalls[i].material; if (mat) { if (!_tempSet.has(mat)) { _tempSet.add(mat); if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { if (onlyLitShaders) { if (!mat.useLighting || mat.emitter && !mat.emitter.lighting) { continue; } } mat.clearVariants(); } } } } _tempSet.clear(); } updateFrameUniforms() { this.blueNoiseTextureId.setValue(getBlueNoiseTexture(this.device)); } beginFrame(comp) { const scene = this.scene; const updateShaders = scene.updateShaders || this.device._shadersDirty; const layers = comp.layerList; const layerCount = layers.length; for(let i = 0; i < layerCount; i++){ const layer = layers[i]; const meshInstances = layer.meshInstances; const count = meshInstances.length; for(let j = 0; j < count; j++){ const meshInst = meshInstances[j]; meshInst.visibleThisFrame = false; if (updateShaders) { _tempMeshInstances.push(meshInst); } if (meshInst.skinInstance) { _tempMeshInstancesSkinned.push(meshInst); } } } if (updateShaders) { const onlyLitShaders = !scene.updateShaders || !this.device._shadersDirty; this.updateShaders(_tempMeshInstances, onlyLitShaders); scene.updateShaders = false; this.device._shadersDirty = false; scene._shaderVersion++; } this.updateFrameUniforms(); this.updateCpuSkinMatrices(_tempMeshInstancesSkinned); _tempMeshInstances.length = 0; _tempMeshInstancesSkinned.length = 0; const lights = this.lights; const lightCount = lights.length; for(let i = 0; i < lightCount; i++){ lights[i].beginFrame(); } } updateLightTextureAtlas() { this.lightTextureAtlas.update(this.localLights, this.scene.lighting); } updateLayerComposition(comp) { const len = comp.layerList.length; const scene = this.scene; const shaderVersion = scene._shaderVersion; for(let i = 0; i < len; i++){ const layer = comp.layerList[i]; layer._shaderVersion = shaderVersion; } comp._update(); } frameUpdate() { this.clustersDebugRendered = false; this.initViewBindGroupFormat(this.scene.clusteredLightingEnabled); this.dirLightShadows.clear(); } constructor(graphicsDevice, scene){ this.clustersDebugRendered = false; this.processingMeshInstances = new Set(); this.lights = []; this.localLights = []; this.cameraDirShadowLights = new Map(); this.dirLightShadows = new Map(); this.blueNoise = new BlueNoise(123); this.gsplatDirector = null; this.device = graphicsDevice; this.scene = scene; this.worldClustersAllocator = new WorldClustersAllocator(graphicsDevice); this.lightTextureAtlas = new LightTextureAtlas(graphicsDevice); this.shadowMapCache = new ShadowMapCache(); this.shadowRenderer = new ShadowRenderer(this, this.lightTextureAtlas); this._shadowRendererLocal = new ShadowRendererLocal(this, this.shadowRenderer); this._shadowRendererDirectional = new ShadowRendererDirectional(this, this.shadowRenderer); if (this.scene.clusteredLightingEnabled) { this._renderPassUpdateClustered = new RenderPassUpdateClustered(this.device, this, this.shadowRenderer, this._shadowRendererLocal, this.lightTextureAtlas); } this.viewUniformFormat = null; this.viewBindGroupFormat = null; this._skinTime = 0; this._morphTime = 0; this._cullTime = 0; this._shadowMapTime = 0; this._lightClustersTime = 0; this._layerCompositionUpdateTime = 0; this._shadowDrawCalls = 0; this._skinDrawCalls = 0; this._instancedDrawCalls = 0; this._shadowMapUpdates = 0; this._numDrawCallsCulled = 0; this._camerasRendered = 0; this._lightClusters = 0; this._gsplatCount = 0; const scope = graphicsDevice.scope; this.boneTextureId = scope.resolve('texture_poseMap'); this.modelMatrixId = scope.resolve('matrix_model'); this.normalMatrixId = scope.resolve('matrix_normal'); this.viewInvId = scope.resolve('matrix_viewInverse'); this.viewPos = new Float32Array(3); this.viewPosId = scope.resolve('view_position'); this.projId = scope.resolve('matrix_projection'); this.projSkyboxId = scope.resolve('matrix_projectionSkybox'); this.viewId = scope.resolve('matrix_view'); this.viewId3 = scope.resolve('matrix_view3'); this.viewProjId = scope.resolve('matrix_viewProjection'); this.flipYId = scope.resolve('projectionFlipY'); this.tbnBasis = scope.resolve('tbnBasis'); this.cameraParams = new Float32Array(4); this.cameraParamsId = scope.resolve('camera_params'); this.viewportSize = new Float32Array(4); this.viewportSizeId = scope.resolve('viewport_size'); this.viewIndexId = scope.resolve('view_index'); this.viewIndexId.setValue(0); this.blueNoiseJitterVersion = 0; this.blueNoiseJitterVec = new Vec4(); this.blueNoiseJitterData = new Float32Array(4); this.blueNoiseJitterId = scope.resolve('blueNoiseJitter'); this.blueNoiseTextureId = scope.resolve('blueNoiseTex32'); this.alphaTestId = scope.resolve('alpha_ref'); this.opacityMapId = scope.resolve('texture_opacityMap'); this.exposureId = scope.resolve('exposure'); this.twoSidedLightingNegScaleFactorId = scope.resolve('twoSidedLightingNegScaleFactor'); this.twoSidedLightingNegScaleFactorId.setValue(0); this.morphPositionTex = scope.resolve('morphPositionTex'); this.morphNormalTex = scope.resolve('morphNormalTex'); this.morphTexParams = scope.resolve('morph_tex_params'); this.lightCube = new LightCube(); this.constantLightCube = scope.resolve('lightCube[0]'); } } class RenderAction { destroy() { this.viewBindGroups.forEach((bg)=>{ bg.defaultUniformBuffer.destroy(); bg.destroy(); }); this.viewBindGroups.length = 0; } setupClears(camera, layer) { this.clearColor = camera?.clearColorBuffer || layer.clearColorBuffer; this.clearDepth = camera?.clearDepthBuffer || layer.clearDepthBuffer; this.clearStencil = camera?.clearStencilBuffer || layer.clearStencilBuffer; } constructor(){ this.camera = null; this.layer = null; this.transparent = false; this.renderTarget = null; this.lightClusters = null; this.clearColor = false; this.clearDepth = false; this.clearStencil = false; this.triggerPostprocess = false; this.firstCameraUse = false; this.lastCameraUse = false; this.viewBindGroups = []; this.useCameraPasses = false; } } class RenderPassForward extends RenderPass { get rendersAnything() { return this.renderActions.length > 0; } addRenderAction(renderAction) { this.renderActions.push(renderAction); } addLayer(cameraComponent, layer, transparent, autoClears = true) { const ra = new RenderAction(); ra.renderTarget = this.renderTarget; ra.camera = cameraComponent; ra.layer = layer; ra.transparent = transparent; if (autoClears) { const firstRa = this.renderActions.length === 0; ra.setupClears(firstRa ? cameraComponent : undefined, layer); } this.addRenderAction(ra); } addLayers(composition, cameraComponent, startIndex, firstLayerClears, lastLayerId, lastLayerIsTransparent = true) { const { layerList, subLayerList } = composition; let clearRenderTarget = firstLayerClears; let index = startIndex; while(index < layerList.length){ const layer = layerList[index]; const isTransparent = subLayerList[index]; const renderedByCamera = cameraComponent.camera.layersSet.has(layer.id); if (renderedByCamera) { this.addLayer(cameraComponent, layer, isTransparent, clearRenderTarget); clearRenderTarget = false; } index++; if (layer.id === lastLayerId && isTransparent === lastLayerIsTransparent) { break; } } return index; } updateDirectionalShadows() { const { renderer, renderActions } = this; for(let i = 0; i < renderActions.length; i++){ const renderAction = renderActions[i]; const cameraComp = renderAction.camera; const camera = cameraComp.camera; const shadowDirLights = this.renderer.cameraDirShadowLights.get(camera); if (shadowDirLights) { for(let l = 0; l < shadowDirLights.length; l++){ const light = shadowDirLights[l]; if (renderer.dirLightShadows.get(light) !== camera) { renderer.dirLightShadows.set(light, camera); const shadowPass = renderer._shadowRendererDirectional.getLightRenderPass(light, camera); if (shadowPass) { this.beforePasses.push(shadowPass); } } } } } } updateClears() { const renderAction = this.renderActions[0]; if (renderAction) { const cameraComponent = renderAction.camera; const camera = cameraComponent.camera; const fullSizeClearRect = camera.fullSizeClearRect; this.setClearColor(fullSizeClearRect && renderAction.clearColor ? camera.clearColor : undefined); this.setClearDepth(fullSizeClearRect && renderAction.clearDepth && !this.noDepthClear ? camera.clearDepth : undefined); this.setClearStencil(fullSizeClearRect && renderAction.clearStencil ? camera.clearStencil : undefined); } } frameUpdate() { super.frameUpdate(); this.updateDirectionalShadows(); this.updateClears(); } before() { const { renderActions } = this; for(let i = 0; i < renderActions.length; i++){ const ra = renderActions[i]; if (ra.firstCameraUse) { this.scene.fire(EVENT_PRERENDER, ra.camera); } } } execute() { const { layerComposition, renderActions } = this; for(let i = 0; i < renderActions.length; i++){ const ra = renderActions[i]; const layer = ra.layer; if (layerComposition.isEnabled(layer, ra.transparent)) { this.renderRenderAction(ra, i === 0); } } } after() { for(let i = 0; i < this.renderActions.length; i++){ const ra = this.renderActions[i]; if (ra.lastCameraUse) { this.scene.fire(EVENT_POSTRENDER, ra.camera); } } this.beforePasses.length = 0; } renderRenderAction(renderAction, firstRenderAction) { const { renderer, scene } = this; const device = renderer.device; const { layer, transparent, camera } = renderAction; if (camera) { const originalGammaCorrection = camera.gammaCorrection; const originalToneMapping = camera.toneMapping; if (this.gammaCorrection !== undefined) camera.gammaCorrection = this.gammaCorrection; if (this.toneMapping !== undefined) camera.toneMapping = this.toneMapping; scene.fire(EVENT_PRERENDER_LAYER, camera, layer, transparent); const options = { lightClusters: renderAction.lightClusters }; const shaderPass = camera.camera.shaderPassInfo?.index ?? SHADER_FORWARD; if (!firstRenderAction || !camera.camera.fullSizeClearRect) { options.clearColor = renderAction.clearColor; options.clearDepth = renderAction.clearDepth; options.clearStencil = renderAction.clearStencil; } const renderTarget = renderAction.renderTarget ?? device.backBuffer; renderer.renderForwardLayer(camera.camera, renderTarget, layer, transparent, shaderPass, renderAction.viewBindGroups, options); device.setBlendState(BlendState.NOBLEND); device.setStencilState(null, null); device.setAlphaToCoverage(false); scene.fire(EVENT_POSTRENDER_LAYER, camera, layer, transparent); if (this.gammaCorrection !== undefined) camera.gammaCorrection = originalGammaCorrection; if (this.toneMapping !== undefined) camera.toneMapping = originalToneMapping; } } constructor(device, layerComposition, scene, renderer){ super(device), this.renderActions = [], this.noDepthClear = false; this.layerComposition = layerComposition; this.scene = scene; this.renderer = renderer; } } class RenderPassPostprocessing extends RenderPass { execute() { const renderAction = this.renderAction; const camera = renderAction.camera; camera.onPostprocessing(); } constructor(device, renderer, renderAction){ super(device); this.renderer = renderer; this.renderAction = renderAction; this.requiresCubemaps = false; } } const _noLights = [ [], [], [] ]; const tmpColor$1 = new Color(); const _drawCallList = { drawCalls: [], shaderInstances: [], isNewMaterial: [], lightMaskChanged: [], clear: function() { this.drawCalls.length = 0; this.shaderInstances.length = 0; this.isNewMaterial.length = 0; this.lightMaskChanged.length = 0; } }; function vogelDiskPrecalculationSamples(numSamples) { const samples = []; for(let i = 0; i < numSamples; ++i){ const r = Math.sqrt(i + 0.5) / Math.sqrt(numSamples); samples.push(r); } return samples; } function vogelSpherePrecalculationSamples(numSamples) { const samples = []; for(let i = 0; i < numSamples; i++){ const weight = i / numSamples; const radius = Math.sqrt(weight * weight); samples.push(radius); } return samples; } class ForwardRenderer extends Renderer { destroy() { super.destroy(); } dispatchGlobalLights(scene) { const ambientUniform = this.ambientColor; tmpColor$1.linear(scene.ambientLight); ambientUniform[0] = tmpColor$1.r; ambientUniform[1] = tmpColor$1.g; ambientUniform[2] = tmpColor$1.b; if (scene.physicalUnits) { for(let i = 0; i < 3; i++){ ambientUniform[i] *= scene.ambientLuminance; } } this.ambientId.setValue(ambientUniform); this.skyboxIntensityId.setValue(scene.physicalUnits ? scene.skyboxLuminance : scene.skyboxIntensity); this.cubeMapRotationMatrixId.setValue(scene._skyboxRotationMat3.data); } _resolveLight(scope, i) { const light = `light${i}`; this.lightColorId[i] = scope.resolve(`${light}_color`); this.lightDir[i] = new Float32Array(3); this.lightDirId[i] = scope.resolve(`${light}_direction`); this.lightShadowMapId[i] = scope.resolve(`${light}_shadowMap`); this.lightShadowMatrixId[i] = scope.resolve(`${light}_shadowMatrix`); this.lightShadowParamsId[i] = scope.resolve(`${light}_shadowParams`); this.lightShadowIntensity[i] = scope.resolve(`${light}_shadowIntensity`); this.lightShadowSearchAreaId[i] = scope.resolve(`${light}_shadowSearchArea`); this.lightRadiusId[i] = scope.resolve(`${light}_radius`); this.lightPos[i] = new Float32Array(3); this.lightPosId[i] = scope.resolve(`${light}_position`); this.lightWidth[i] = new Float32Array(3); this.lightWidthId[i] = scope.resolve(`${light}_halfWidth`); this.lightHeight[i] = new Float32Array(3); this.lightHeightId[i] = scope.resolve(`${light}_halfHeight`); this.lightInAngleId[i] = scope.resolve(`${light}_innerConeAngle`); this.lightOutAngleId[i] = scope.resolve(`${light}_outerConeAngle`); this.lightCookieId[i] = scope.resolve(`${light}_cookie`); this.lightCookieIntId[i] = scope.resolve(`${light}_cookieIntensity`); this.lightCookieMatrixId[i] = scope.resolve(`${light}_cookieMatrix`); this.lightCookieOffsetId[i] = scope.resolve(`${light}_cookieOffset`); this.lightCameraParamsId[i] = scope.resolve(`${light}_cameraParams`); this.lightSoftShadowParamsId[i] = scope.resolve(`${light}_softShadowParams`); this.shadowMatrixPaletteId[i] = scope.resolve(`${light}_shadowMatrixPalette[0]`); this.shadowCascadeDistancesId[i] = scope.resolve(`${light}_shadowCascadeDistances`); this.shadowCascadeCountId[i] = scope.resolve(`${light}_shadowCascadeCount`); this.shadowCascadeBlendId[i] = scope.resolve(`${light}_shadowCascadeBlend`); } setLTCDirectionalLight(wtm, cnt, dir, campos, far) { this.lightPos[cnt][0] = campos.x - dir.x * far; this.lightPos[cnt][1] = campos.y - dir.y * far; this.lightPos[cnt][2] = campos.z - dir.z * far; this.lightPosId[cnt].setValue(this.lightPos[cnt]); const hWidth = wtm.transformVector(new Vec3(-0.5, 0, 0)); this.lightWidth[cnt][0] = hWidth.x * far; this.lightWidth[cnt][1] = hWidth.y * far; this.lightWidth[cnt][2] = hWidth.z * far; this.lightWidthId[cnt].setValue(this.lightWidth[cnt]); const hHeight = wtm.transformVector(new Vec3(0, 0, 0.5)); this.lightHeight[cnt][0] = hHeight.x * far; this.lightHeight[cnt][1] = hHeight.y * far; this.lightHeight[cnt][2] = hHeight.z * far; this.lightHeightId[cnt].setValue(this.lightHeight[cnt]); } dispatchDirectLights(dirs, mask, camera) { let cnt = 0; const scope = this.device.scope; for(let i = 0; i < dirs.length; i++){ if (!(dirs[i].mask & mask)) continue; const directional = dirs[i]; const wtm = directional._node.getWorldTransform(); if (!this.lightColorId[cnt]) { this._resolveLight(scope, cnt); } this.lightColorId[cnt].setValue(directional._colorLinear); wtm.getY(directional._direction).mulScalar(-1); directional._direction.normalize(); this.lightDir[cnt][0] = directional._direction.x; this.lightDir[cnt][1] = directional._direction.y; this.lightDir[cnt][2] = directional._direction.z; this.lightDirId[cnt].setValue(this.lightDir[cnt]); if (directional.shape !== LIGHTSHAPE_PUNCTUAL) { this.setLTCDirectionalLight(wtm, cnt, directional._direction, camera._node.getPosition(), camera.farClip); } if (directional.castShadows) { const lightRenderData = directional.getRenderData(camera, 0); const biases = directional._getUniformBiasValues(lightRenderData); this.lightShadowMapId[cnt].setValue(lightRenderData.shadowBuffer); this.lightShadowMatrixId[cnt].setValue(lightRenderData.shadowMatrix.data); this.shadowMatrixPaletteId[cnt].setValue(directional._shadowMatrixPalette); this.shadowCascadeDistancesId[cnt].setValue(directional._shadowCascadeDistances); this.shadowCascadeCountId[cnt].setValue(directional.numCascades); this.shadowCascadeBlendId[cnt].setValue(1 - directional.cascadeBlend); this.lightShadowIntensity[cnt].setValue(directional.shadowIntensity); this.lightSoftShadowParamsId[cnt].setValue(directional._softShadowParams); const shadowRT = lightRenderData.shadowCamera.renderTarget; if (shadowRT) { this.lightShadowSearchAreaId[cnt].setValue(directional.penumbraSize / lightRenderData.shadowCamera.renderTarget.width * lightRenderData.projectionCompensation); } const cameraParams = directional._shadowCameraParams; cameraParams.length = 4; cameraParams[0] = 0; cameraParams[1] = lightRenderData.shadowCamera._farClip; cameraParams[2] = lightRenderData.shadowCamera._nearClip; cameraParams[3] = 1; this.lightCameraParamsId[cnt].setValue(cameraParams); const params = directional._shadowRenderParams; params.length = 4; params[0] = directional._shadowResolution; params[1] = biases.normalBias; params[2] = biases.bias; params[3] = 0; this.lightShadowParamsId[cnt].setValue(params); } cnt++; } return cnt; } setLTCPositionalLight(wtm, cnt) { const hWidth = wtm.transformVector(new Vec3(-0.5, 0, 0)); this.lightWidth[cnt][0] = hWidth.x; this.lightWidth[cnt][1] = hWidth.y; this.lightWidth[cnt][2] = hWidth.z; this.lightWidthId[cnt].setValue(this.lightWidth[cnt]); const hHeight = wtm.transformVector(new Vec3(0, 0, 0.5)); this.lightHeight[cnt][0] = hHeight.x; this.lightHeight[cnt][1] = hHeight.y; this.lightHeight[cnt][2] = hHeight.z; this.lightHeightId[cnt].setValue(this.lightHeight[cnt]); } dispatchOmniLight(scope, omni, cnt) { const wtm = omni._node.getWorldTransform(); if (!this.lightColorId[cnt]) { this._resolveLight(scope, cnt); } this.lightRadiusId[cnt].setValue(omni.attenuationEnd); this.lightColorId[cnt].setValue(omni._colorLinear); wtm.getTranslation(omni._position); this.lightPos[cnt][0] = omni._position.x; this.lightPos[cnt][1] = omni._position.y; this.lightPos[cnt][2] = omni._position.z; this.lightPosId[cnt].setValue(this.lightPos[cnt]); if (omni.shape !== LIGHTSHAPE_PUNCTUAL) { this.setLTCPositionalLight(wtm, cnt); } if (omni.castShadows) { const lightRenderData = omni.getRenderData(null, 0); this.lightShadowMapId[cnt].setValue(lightRenderData.shadowBuffer); const biases = omni._getUniformBiasValues(lightRenderData); const params = omni._shadowRenderParams; params.length = 4; params[0] = omni._shadowResolution; params[1] = biases.normalBias; params[2] = biases.bias; params[3] = 1.0 / omni.attenuationEnd; this.lightShadowParamsId[cnt].setValue(params); this.lightShadowIntensity[cnt].setValue(omni.shadowIntensity); const pixelsPerMeter = omni.penumbraSize / lightRenderData.shadowCamera.renderTarget.width; this.lightShadowSearchAreaId[cnt].setValue(pixelsPerMeter); const cameraParams = omni._shadowCameraParams; cameraParams.length = 4; cameraParams[0] = 0; cameraParams[1] = lightRenderData.shadowCamera._farClip; cameraParams[2] = lightRenderData.shadowCamera._nearClip; cameraParams[3] = 0; this.lightCameraParamsId[cnt].setValue(cameraParams); } if (omni._cookie) { this.lightCookieId[cnt].setValue(omni._cookie); this.lightShadowMatrixId[cnt].setValue(wtm.data); this.lightCookieIntId[cnt].setValue(omni.cookieIntensity); } } dispatchSpotLight(scope, spot, cnt) { const wtm = spot._node.getWorldTransform(); if (!this.lightColorId[cnt]) { this._resolveLight(scope, cnt); } this.lightInAngleId[cnt].setValue(spot._innerConeAngleCos); this.lightOutAngleId[cnt].setValue(spot._outerConeAngleCos); this.lightRadiusId[cnt].setValue(spot.attenuationEnd); this.lightColorId[cnt].setValue(spot._colorLinear); wtm.getTranslation(spot._position); this.lightPos[cnt][0] = spot._position.x; this.lightPos[cnt][1] = spot._position.y; this.lightPos[cnt][2] = spot._position.z; this.lightPosId[cnt].setValue(this.lightPos[cnt]); if (spot.shape !== LIGHTSHAPE_PUNCTUAL) { this.setLTCPositionalLight(wtm, cnt); } wtm.getY(spot._direction).mulScalar(-1); spot._direction.normalize(); this.lightDir[cnt][0] = spot._direction.x; this.lightDir[cnt][1] = spot._direction.y; this.lightDir[cnt][2] = spot._direction.z; this.lightDirId[cnt].setValue(this.lightDir[cnt]); if (spot.castShadows) { const lightRenderData = spot.getRenderData(null, 0); this.lightShadowMapId[cnt].setValue(lightRenderData.shadowBuffer); this.lightShadowMatrixId[cnt].setValue(lightRenderData.shadowMatrix.data); const biases = spot._getUniformBiasValues(lightRenderData); const params = spot._shadowRenderParams; params.length = 4; params[0] = spot._shadowResolution; params[1] = biases.normalBias; params[2] = biases.bias; params[3] = 1.0 / spot.attenuationEnd; this.lightShadowParamsId[cnt].setValue(params); this.lightShadowIntensity[cnt].setValue(spot.shadowIntensity); const pixelsPerMeter = spot.penumbraSize / lightRenderData.shadowCamera.renderTarget.width; const fov = lightRenderData.shadowCamera._fov * math.DEG_TO_RAD; const fovRatio = 1.0 / Math.tan(fov / 2.0); this.lightShadowSearchAreaId[cnt].setValue(pixelsPerMeter * fovRatio); const cameraParams = spot._shadowCameraParams; cameraParams.length = 4; cameraParams[0] = 0; cameraParams[1] = lightRenderData.shadowCamera._farClip; cameraParams[2] = lightRenderData.shadowCamera._nearClip; cameraParams[3] = 0; this.lightCameraParamsId[cnt].setValue(cameraParams); } if (spot._cookie) { if (!spot.castShadows) { const cookieMatrix = LightCamera.evalSpotCookieMatrix(spot); this.lightShadowMatrixId[cnt].setValue(cookieMatrix.data); } this.lightCookieId[cnt].setValue(spot._cookie); this.lightCookieIntId[cnt].setValue(spot.cookieIntensity); if (spot._cookieTransform) { spot._cookieTransformUniform[0] = spot._cookieTransform.x; spot._cookieTransformUniform[1] = spot._cookieTransform.y; spot._cookieTransformUniform[2] = spot._cookieTransform.z; spot._cookieTransformUniform[3] = spot._cookieTransform.w; this.lightCookieMatrixId[cnt].setValue(spot._cookieTransformUniform); spot._cookieOffsetUniform[0] = spot._cookieOffset.x; spot._cookieOffsetUniform[1] = spot._cookieOffset.y; this.lightCookieOffsetId[cnt].setValue(spot._cookieOffsetUniform); } } } dispatchLocalLights(sortedLights, mask, usedDirLights) { let cnt = usedDirLights; const scope = this.device.scope; const omnis = sortedLights[LIGHTTYPE_OMNI]; const numOmnis = omnis.length; for(let i = 0; i < numOmnis; i++){ const omni = omnis[i]; if (!(omni.mask & mask)) continue; this.dispatchOmniLight(scope, omni, cnt); cnt++; } const spts = sortedLights[LIGHTTYPE_SPOT]; const numSpts = spts.length; for(let i = 0; i < numSpts; i++){ const spot = spts[i]; if (!(spot.mask & mask)) continue; this.dispatchSpotLight(scope, spot, cnt); cnt++; } } renderForwardPrepareMaterials(camera, renderTarget, drawCalls, sortedLights, layer, pass) { const fogParams = camera.fogParams ?? this.scene.fog; const shaderParams = camera.shaderParams; shaderParams.fog = fogParams.type; shaderParams.srgbRenderTarget = renderTarget?.isColorBufferSrgb(0) ?? false; const addCall = (drawCall, shaderInstance, isNewMaterial, lightMaskChanged)=>{ _drawCallList.drawCalls.push(drawCall); _drawCallList.shaderInstances.push(shaderInstance); _drawCallList.isNewMaterial.push(isNewMaterial); _drawCallList.lightMaskChanged.push(lightMaskChanged); }; _drawCallList.clear(); const device = this.device; const scene = this.scene; const clusteredLightingEnabled = scene.clusteredLightingEnabled; const lightHash = layer?.getLightHash(clusteredLightingEnabled) ?? 0; let prevMaterial = null, prevObjDefs, prevLightMask; const drawCallsCount = drawCalls.length; for(let i = 0; i < drawCallsCount; i++){ const drawCall = drawCalls[i]; const instancingData = drawCall.instancingData; if (instancingData && instancingData.count <= 0) { continue; } drawCall.ensureMaterial(device); const material = drawCall.material; const objDefs = drawCall._shaderDefs; const lightMask = drawCall.mask; if (material && material === prevMaterial && objDefs !== prevObjDefs) { prevMaterial = null; } if (material !== prevMaterial) { this._materialSwitches++; material._scene = scene; if (material.dirty) { material.updateUniforms(device, scene); material.dirty = false; } } const shaderInstance = drawCall.getShaderInstance(pass, lightHash, scene, shaderParams, this.viewUniformFormat, this.viewBindGroupFormat, sortedLights); addCall(drawCall, shaderInstance, material !== prevMaterial, !prevMaterial || lightMask !== prevLightMask); prevMaterial = material; prevObjDefs = objDefs; prevLightMask = lightMask; } return _drawCallList; } renderForwardInternal(camera, preparedCalls, sortedLights, pass, drawCallback, flipFaces, viewBindGroups) { const device = this.device; const scene = this.scene; const passFlag = 1 << pass; const flipFactor = flipFaces ? -1 : 1; const clusteredLightingEnabled = scene.clusteredLightingEnabled; const viewList = camera.xr?.session && camera.xr.views.list.length ? camera.xr.views.list : null; const preparedCallsCount = preparedCalls.drawCalls.length; for(let i = 0; i < preparedCallsCount; i++){ const drawCall = preparedCalls.drawCalls[i]; const newMaterial = preparedCalls.isNewMaterial[i]; const lightMaskChanged = preparedCalls.lightMaskChanged[i]; const shaderInstance = preparedCalls.shaderInstances[i]; const material = drawCall.material; const lightMask = drawCall.mask; if (shaderInstance.shader.failed) continue; if (newMaterial) { const asyncCompile = false; device.setShader(shaderInstance.shader, asyncCompile); material.setParameters(device); if (lightMaskChanged) { const usedDirLights = this.dispatchDirectLights(sortedLights[LIGHTTYPE_DIRECTIONAL], lightMask, camera); if (!clusteredLightingEnabled) { this.dispatchLocalLights(sortedLights, lightMask, usedDirLights); } } this.alphaTestId.setValue(material.alphaTest); device.setBlendState(material.blendState); device.setDepthState(material.depthState); device.setAlphaToCoverage(material.alphaToCoverage); } this.setupCullMode(camera._cullFaces, flipFactor, drawCall); const stencilFront = drawCall.stencilFront ?? material.stencilFront; const stencilBack = drawCall.stencilBack ?? material.stencilBack; device.setStencilState(stencilFront, stencilBack); drawCall.setParameters(device, passFlag); device.scope.resolve('meshInstanceId').setValue(drawCall.id); const mesh = drawCall.mesh; this.setVertexBuffers(device, mesh); this.setMorphing(device, drawCall.morphInstance); this.setSkinning(device, drawCall); const instancingData = drawCall.instancingData; if (instancingData) { device.setVertexBuffer(instancingData.vertexBuffer); } this.setMeshInstanceMatrices(drawCall, true); this.setupMeshUniformBuffers(shaderInstance); const style = drawCall.renderStyle; const indexBuffer = mesh.indexBuffer[style]; drawCallback?.(drawCall, i); const indirectData = drawCall.getDrawCommands(camera); if (viewList) { for(let v = 0; v < viewList.length; v++){ const view = viewList[v]; device.setViewport(view.viewport.x, view.viewport.y, view.viewport.z, view.viewport.w); if (device.supportsUniformBuffers) { const viewBindGroup = viewBindGroups[v]; device.setBindGroup(BINDGROUP_VIEW, viewBindGroup); } else { this.setupViewUniforms(view, v); } const first = v === 0; const last = v === viewList.length - 1; device.draw(mesh.primitive[style], indexBuffer, instancingData?.count, indirectData, first, last); this._forwardDrawCalls++; if (drawCall.instancingData) { this._instancedDrawCalls++; } } } else { device.draw(mesh.primitive[style], indexBuffer, instancingData?.count, indirectData); this._forwardDrawCalls++; if (drawCall.instancingData) { this._instancedDrawCalls++; } } if (i < preparedCallsCount - 1 && !preparedCalls.isNewMaterial[i + 1]) { material.setParameters(device, drawCall.parameters); } } } renderForward(camera, renderTarget, allDrawCalls, sortedLights, pass, drawCallback, layer, flipFaces, viewBindGroups) { const preparedCalls = this.renderForwardPrepareMaterials(camera, renderTarget, allDrawCalls, sortedLights, layer, pass); this.renderForwardInternal(camera, preparedCalls, sortedLights, pass, drawCallback, flipFaces, viewBindGroups); _drawCallList.clear(); } renderForwardLayer(camera, renderTarget, layer, transparent, shaderPass, viewBindGroups, options = {}) { const { scene, device } = this; const clusteredLightingEnabled = scene.clusteredLightingEnabled; this.setupViewport(camera, renderTarget); let visible, splitLights; if (layer) { layer.sortVisible(camera, transparent); const culledInstances = layer.getCulledInstances(camera); visible = transparent ? culledInstances.transparent : culledInstances.opaque; scene.immediate.onPreRenderLayer(layer, visible, transparent); if (layer.requiresLightCube) { this.lightCube.update(scene.ambientLight, layer._lights); this.constantLightCube.setValue(this.lightCube.colors); } splitLights = layer.splitLights; } else { visible = options.meshInstances; splitLights = options.splitLights ?? _noLights; } if (clusteredLightingEnabled) { const lightClusters = options.lightClusters ?? this.worldClustersAllocator.empty; lightClusters.activate(); if (layer) { if (!this.clustersDebugRendered && scene.lighting.debugLayer === layer.id) { this.clustersDebugRendered = true; } } } scene._activeCamera = camera; const fogParams = camera.fogParams ?? this.scene.fog; this.setFogConstants(fogParams); const viewList = this.setCameraUniforms(camera, renderTarget); if (device.supportsUniformBuffers) { this.setupViewUniformBuffers(viewBindGroups, this.viewUniformFormat, this.viewBindGroupFormat, viewList); } const clearColor = options.clearColor ?? false; const clearDepth = options.clearDepth ?? false; const clearStencil = options.clearStencil ?? false; if (clearColor || clearDepth || clearStencil) { this.clear(camera, clearColor, clearDepth, clearStencil); } const flipFaces = !!(camera._flipFaces ^ renderTarget?.flipY); const forwardDrawCalls = this._forwardDrawCalls; this.renderForward(camera, renderTarget, visible, splitLights, shaderPass, null, layer, flipFaces, viewBindGroups); if (layer) { layer._forwardDrawCalls += this._forwardDrawCalls - forwardDrawCalls; } } setFogConstants(fogParams) { if (fogParams.type !== FOG_NONE) { tmpColor$1.linear(fogParams.color); const fogUniform = this.fogColor; fogUniform[0] = tmpColor$1.r; fogUniform[1] = tmpColor$1.g; fogUniform[2] = tmpColor$1.b; this.fogColorId.setValue(fogUniform); if (fogParams.type === FOG_LINEAR) { this.fogStartId.setValue(fogParams.start); this.fogEndId.setValue(fogParams.end); } else { this.fogDensityId.setValue(fogParams.density); } } } setSceneConstants() { const scene = this.scene; this.dispatchGlobalLights(scene); const device = this.device; this._screenSize[0] = device.width; this._screenSize[1] = device.height; this._screenSize[2] = 1 / device.width; this._screenSize[3] = 1 / device.height; this.screenSizeId.setValue(this._screenSize); this.pcssDiskSamplesId.setValue(this.pcssDiskSamples); this.pcssSphereSamplesId.setValue(this.pcssSphereSamples); } buildFrameGraph(frameGraph, layerComposition) { const scene = this.scene; frameGraph.reset(); if (scene.clusteredLightingEnabled) { const { shadowsEnabled, cookiesEnabled } = scene.lighting; this._renderPassUpdateClustered.update(frameGraph, shadowsEnabled, cookiesEnabled, this.lights, this.localLights); frameGraph.addRenderPass(this._renderPassUpdateClustered); } else { this._shadowRendererLocal.buildNonClusteredRenderPasses(frameGraph, this.localLights); } let startIndex = 0; let newStart = true; let renderTarget = null; const renderActions = layerComposition._renderActions; for(let i = startIndex; i < renderActions.length; i++){ const renderAction = renderActions[i]; const { layer, camera } = renderAction; if (renderAction.useCameraPasses) { camera.camera.renderPasses.forEach((renderPass)=>{ frameGraph.addRenderPass(renderPass); }); } else { const isDepthLayer = layer.id === LAYERID_DEPTH; const isGrabPass = isDepthLayer && (camera.renderSceneColorMap || camera.renderSceneDepthMap); if (newStart) { newStart = false; startIndex = i; renderTarget = renderAction.renderTarget; } const nextRenderAction = renderActions[i + 1]; const isNextLayerDepth = nextRenderAction ? !nextRenderAction.useCameraPasses && nextRenderAction.layer.id === LAYERID_DEPTH : false; const isNextLayerGrabPass = isNextLayerDepth && (camera.renderSceneColorMap || camera.renderSceneDepthMap); const nextNeedDirShadows = nextRenderAction ? nextRenderAction.firstCameraUse && this.cameraDirShadowLights.has(nextRenderAction.camera.camera) : false; if (!nextRenderAction || nextRenderAction.renderTarget !== renderTarget || nextNeedDirShadows || isNextLayerGrabPass || isGrabPass) { const isDepthOnly = isDepthLayer && startIndex === i; if (!isDepthOnly) { this.addMainRenderPass(frameGraph, layerComposition, renderTarget, startIndex, i); } if (isDepthLayer) { if (camera.renderSceneColorMap) { const colorGrabPass = camera.camera.renderPassColorGrab; colorGrabPass.source = camera.renderTarget; frameGraph.addRenderPass(colorGrabPass); } if (camera.renderSceneDepthMap) { frameGraph.addRenderPass(camera.camera.renderPassDepthGrab); } } if (renderAction.triggerPostprocess && camera?.onPostprocessing) { const renderPass = new RenderPassPostprocessing(this.device, this, renderAction); frameGraph.addRenderPass(renderPass); } newStart = true; } } } } addMainRenderPass(frameGraph, layerComposition, renderTarget, startIndex, endIndex) { const renderPass = new RenderPassForward(this.device, layerComposition, this.scene, this); renderPass.init(renderTarget); const renderActions = layerComposition._renderActions; for(let i = startIndex; i <= endIndex; i++){ renderPass.addRenderAction(renderActions[i]); } frameGraph.addRenderPass(renderPass); } update(comp) { this.frameUpdate(); this.shadowRenderer.frameUpdate(); this.scene._updateSkyMesh(); this.updateLayerComposition(comp); this.collectLights(comp); this.beginFrame(comp); this.setSceneConstants(); this.gsplatDirector?.update(comp); this.cullComposition(comp); this.gpuUpdate(this.processingMeshInstances); } constructor(graphicsDevice, scene){ super(graphicsDevice, scene); const device = this.device; this._forwardDrawCalls = 0; this._materialSwitches = 0; this._depthMapTime = 0; this._forwardTime = 0; this._sortTime = 0; const scope = device.scope; this.fogColorId = scope.resolve('fog_color'); this.fogStartId = scope.resolve('fog_start'); this.fogEndId = scope.resolve('fog_end'); this.fogDensityId = scope.resolve('fog_density'); this.ambientId = scope.resolve('light_globalAmbient'); this.skyboxIntensityId = scope.resolve('skyboxIntensity'); this.cubeMapRotationMatrixId = scope.resolve('cubeMapRotationMatrix'); this.pcssDiskSamplesId = scope.resolve('pcssDiskSamples[0]'); this.pcssSphereSamplesId = scope.resolve('pcssSphereSamples[0]'); this.lightColorId = []; this.lightDir = []; this.lightDirId = []; this.lightShadowMapId = []; this.lightShadowMatrixId = []; this.lightShadowParamsId = []; this.lightShadowIntensity = []; this.lightRadiusId = []; this.lightPos = []; this.lightPosId = []; this.lightWidth = []; this.lightWidthId = []; this.lightHeight = []; this.lightHeightId = []; this.lightInAngleId = []; this.lightOutAngleId = []; this.lightCookieId = []; this.lightCookieIntId = []; this.lightCookieMatrixId = []; this.lightCookieOffsetId = []; this.lightShadowSearchAreaId = []; this.lightCameraParamsId = []; this.lightSoftShadowParamsId = []; this.shadowMatrixPaletteId = []; this.shadowCascadeDistancesId = []; this.shadowCascadeCountId = []; this.shadowCascadeBlendId = []; this.screenSizeId = scope.resolve('uScreenSize'); this._screenSize = new Float32Array(4); this.fogColor = new Float32Array(3); this.ambientColor = new Float32Array(3); this.pcssDiskSamples = vogelDiskPrecalculationSamples(16); this.pcssSphereSamples = vogelSpherePrecalculationSamples(16); } } let layerCounter = 0; const lightKeys = []; const _tempMaterials = new Set(); function sortManual(drawCallA, drawCallB) { return drawCallA.drawOrder - drawCallB.drawOrder; } function sortMaterialMesh(drawCallA, drawCallB) { const keyA = drawCallA._sortKeyForward; const keyB = drawCallB._sortKeyForward; if (keyA === keyB) { return drawCallB.mesh.id - drawCallA.mesh.id; } return keyB - keyA; } function sortBackToFront(drawCallA, drawCallB) { return drawCallB._sortKeyDynamic - drawCallA._sortKeyDynamic; } function sortFrontToBack(drawCallA, drawCallB) { return drawCallA._sortKeyDynamic - drawCallB._sortKeyDynamic; } const sortCallbacks = [ null, sortManual, sortMaterialMesh, sortBackToFront, sortFrontToBack ]; class CulledInstances { constructor(){ this.opaque = []; this.transparent = []; } } class Layer { set enabled(val) { if (val !== this._enabled) { this._dirtyComposition = true; this.gsplatPlacementsDirty = true; this._enabled = val; if (val) { this.incrementCounter(); if (this.onEnable) this.onEnable(); } else { this.decrementCounter(); if (this.onDisable) this.onDisable(); } } } get enabled() { return this._enabled; } set clearColorBuffer(val) { this._clearColorBuffer = val; this._dirtyComposition = true; } get clearColorBuffer() { return this._clearColorBuffer; } set clearDepthBuffer(val) { this._clearDepthBuffer = val; this._dirtyComposition = true; } get clearDepthBuffer() { return this._clearDepthBuffer; } set clearStencilBuffer(val) { this._clearStencilBuffer = val; this._dirtyComposition = true; } get clearStencilBuffer() { return this._clearStencilBuffer; } get hasClusteredLights() { return this._clusteredLightsSet.size > 0; } get clusteredLightsSet() { return this._clusteredLightsSet; } incrementCounter() { if (this._refCounter === 0) { this._enabled = true; if (this.onEnable) this.onEnable(); } this._refCounter++; } decrementCounter() { if (this._refCounter === 1) { this._enabled = false; if (this.onDisable) this.onDisable(); } else if (this._refCounter === 0) { return; } this._refCounter--; } addGSplatPlacement(placement) { if (!this.gsplatPlacementsSet.has(placement)) { this.gsplatPlacements.push(placement); this.gsplatPlacementsSet.add(placement); this.gsplatPlacementsDirty = true; } } removeGSplatPlacement(placement) { const index = this.gsplatPlacements.indexOf(placement); if (index >= 0) { this.gsplatPlacements.splice(index, 1); this.gsplatPlacementsSet.delete(placement); this.gsplatPlacementsDirty = true; } } addGSplatShadowCaster(placement) { if (!this.gsplatShadowCastersSet.has(placement)) { this.gsplatShadowCasters.push(placement); this.gsplatShadowCastersSet.add(placement); this.gsplatPlacementsDirty = true; } } removeGSplatShadowCaster(placement) { const index = this.gsplatShadowCasters.indexOf(placement); if (index >= 0) { this.gsplatShadowCasters.splice(index, 1); this.gsplatShadowCastersSet.delete(placement); this.gsplatPlacementsDirty = true; } } addMeshInstances(meshInstances, skipShadowCasters) { const destMeshInstances = this.meshInstances; const destMeshInstancesSet = this.meshInstancesSet; for(let i = 0; i < meshInstances.length; i++){ const mi = meshInstances[i]; if (!destMeshInstancesSet.has(mi)) { destMeshInstances.push(mi); destMeshInstancesSet.add(mi); _tempMaterials.add(mi.material); } } if (!skipShadowCasters) { this.addShadowCasters(meshInstances); } if (_tempMaterials.size > 0) { const sceneShaderVer = this._shaderVersion; _tempMaterials.forEach((mat)=>{ if (sceneShaderVer >= 0 && mat._shaderVersion !== sceneShaderVer) { if (mat.getShaderVariant !== Material.prototype.getShaderVariant) { mat.clearVariants(); } mat._shaderVersion = sceneShaderVer; } }); _tempMaterials.clear(); } } removeMeshInstances(meshInstances, skipShadowCasters) { const destMeshInstances = this.meshInstances; const destMeshInstancesSet = this.meshInstancesSet; for(let i = 0; i < meshInstances.length; i++){ const mi = meshInstances[i]; if (destMeshInstancesSet.has(mi)) { destMeshInstancesSet.delete(mi); const j = destMeshInstances.indexOf(mi); if (j >= 0) { destMeshInstances.splice(j, 1); } } } if (!skipShadowCasters) { this.removeShadowCasters(meshInstances); } } addShadowCasters(meshInstances) { const shadowCasters = this.shadowCasters; const shadowCastersSet = this.shadowCastersSet; for(let i = 0; i < meshInstances.length; i++){ const mi = meshInstances[i]; if (mi.castShadow && !shadowCastersSet.has(mi)) { shadowCastersSet.add(mi); shadowCasters.push(mi); } } } removeShadowCasters(meshInstances) { const shadowCasters = this.shadowCasters; const shadowCastersSet = this.shadowCastersSet; for(let i = 0; i < meshInstances.length; i++){ const mi = meshInstances[i]; if (shadowCastersSet.has(mi)) { shadowCastersSet.delete(mi); const j = shadowCasters.indexOf(mi); if (j >= 0) { shadowCasters.splice(j, 1); } } } } clearMeshInstances(skipShadowCasters = false) { this.meshInstances.length = 0; this.meshInstancesSet.clear(); if (!skipShadowCasters) { this.shadowCasters.length = 0; this.shadowCastersSet.clear(); } } markLightsDirty() { this._lightHashDirty = true; this._lightIdHashDirty = true; this._splitLightsDirty = true; } hasLight(light) { return this._lightsSet.has(light); } addLight(light) { const l = light.light; if (!this._lightsSet.has(l)) { this._lightsSet.add(l); this._lights.push(l); this.markLightsDirty(); } if (l.type !== LIGHTTYPE_DIRECTIONAL) { this._clusteredLightsSet.add(l); } } removeLight(light) { const l = light.light; if (this._lightsSet.has(l)) { this._lightsSet.delete(l); this._lights.splice(this._lights.indexOf(l), 1); this.markLightsDirty(); } if (l.type !== LIGHTTYPE_DIRECTIONAL) { this._clusteredLightsSet.delete(l); } } clearLights() { this._lightsSet.forEach((light)=>light.removeLayer(this)); this._lightsSet.clear(); this._clusteredLightsSet.clear(); this._lights.length = 0; this.markLightsDirty(); } get splitLights() { if (this._splitLightsDirty) { this._splitLightsDirty = false; const splitLights = this._splitLights; for(let i = 0; i < splitLights.length; i++){ splitLights[i].length = 0; } const lights = this._lights; for(let i = 0; i < lights.length; i++){ const light = lights[i]; if (light.enabled) { splitLights[light._type].push(light); } } for(let i = 0; i < splitLights.length; i++){ splitLights[i].sort((a, b)=>a.key - b.key); } } return this._splitLights; } evaluateLightHash(localLights, directionalLights, useIds) { let hash = 0; const lights = this._lights; for(let i = 0; i < lights.length; i++){ const isLocalLight = lights[i].type !== LIGHTTYPE_DIRECTIONAL; if (localLights && isLocalLight || directionalLights && !isLocalLight) { lightKeys.push(useIds ? lights[i].id : lights[i].key); } } if (lightKeys.length > 0) { lightKeys.sort(); hash = hash32Fnv1a(lightKeys); lightKeys.length = 0; } return hash; } getLightHash(isClustered) { if (this._lightHashDirty) { this._lightHashDirty = false; this._lightHash = this.evaluateLightHash(!isClustered, true, false); } return this._lightHash; } getLightIdHash() { if (this._lightIdHashDirty) { this._lightIdHashDirty = false; this._lightIdHash = this.evaluateLightHash(true, false, true); } return this._lightIdHash; } addCamera(camera) { if (!this.camerasSet.has(camera.camera)) { this.camerasSet.add(camera.camera); this.cameras.push(camera); this._dirtyComposition = true; } } removeCamera(camera) { if (this.camerasSet.has(camera.camera)) { this.camerasSet.delete(camera.camera); const index = this.cameras.indexOf(camera); this.cameras.splice(index, 1); this._dirtyComposition = true; } } clearCameras() { this.cameras.length = 0; this.camerasSet.clear(); this._dirtyComposition = true; } _calculateSortDistances(drawCalls, camPos, camFwd) { const count = drawCalls.length; const { x: px, y: py, z: pz } = camPos; const { x: fx, y: fy, z: fz } = camFwd; for(let i = 0; i < count; i++){ const drawCall = drawCalls[i]; let zDist; if (drawCall.calculateSortDistance) { zDist = drawCall.calculateSortDistance(drawCall, camPos, camFwd); } else { const meshPos = drawCall.aabb.center; zDist = (meshPos.x - px) * fx + (meshPos.y - py) * fy + (meshPos.z - pz) * fz; } const bucket = drawCall._drawBucket * 1e9; drawCall._sortKeyDynamic = bucket + zDist; } } getCulledInstances(camera) { let instances = this._visibleInstances.get(camera); if (!instances) { instances = new CulledInstances(); this._visibleInstances.set(camera, instances); } return instances; } sortVisible(camera, transparent) { const sortMode = transparent ? this.transparentSortMode : this.opaqueSortMode; if (sortMode === SORTMODE_NONE) { return; } const culledInstances = this.getCulledInstances(camera); const instances = transparent ? culledInstances.transparent : culledInstances.opaque; const cameraNode = camera.node; if (sortMode === SORTMODE_CUSTOM) { const sortPos = cameraNode.getPosition(); const sortDir = cameraNode.forward; if (this.customCalculateSortValues) { this.customCalculateSortValues(instances, instances.length, sortPos, sortDir); } if (this.customSortCallback) { instances.sort(this.customSortCallback); } } else { if (sortMode === SORTMODE_BACK2FRONT || sortMode === SORTMODE_FRONT2BACK) { const sortPos = cameraNode.getPosition(); const sortDir = cameraNode.forward; this._calculateSortDistances(instances, sortPos, sortDir); } instances.sort(sortCallbacks[sortMode]); } } constructor(options = {}){ this.meshInstances = []; this.meshInstancesSet = new Set(); this.shadowCasters = []; this.shadowCastersSet = new Set(); this._visibleInstances = new WeakMap(); this._lights = []; this._lightsSet = new Set(); this._clusteredLightsSet = new Set(); this._splitLights = [ [], [], [] ]; this._splitLightsDirty = true; this.requiresLightCube = false; this.cameras = []; this.camerasSet = new Set(); this.gsplatPlacements = []; this.gsplatPlacementsSet = new Set(); this.gsplatShadowCasters = []; this.gsplatShadowCastersSet = new Set(); this.gsplatPlacementsDirty = true; this._dirtyComposition = false; if (options.id !== undefined) { this.id = options.id; layerCounter = Math.max(this.id + 1, layerCounter); } else { this.id = layerCounter++; } this.name = options.name; this._enabled = options.enabled ?? true; this._refCounter = this._enabled ? 1 : 0; this.opaqueSortMode = options.opaqueSortMode ?? SORTMODE_MATERIALMESH; this.transparentSortMode = options.transparentSortMode ?? SORTMODE_BACK2FRONT; if (options.renderTarget) { this.renderTarget = options.renderTarget; } this._clearColorBuffer = !!options.clearColorBuffer; this._clearDepthBuffer = !!options.clearDepthBuffer; this._clearStencilBuffer = !!options.clearStencilBuffer; this.onEnable = options.onEnable; this.onDisable = options.onDisable; if (this._enabled && this.onEnable) { this.onEnable(); } this.customSortCallback = null; this.customCalculateSortValues = null; this._lightHash = 0; this._lightHashDirty = false; this._lightIdHash = 0; this._lightIdHashDirty = false; this._shaderVersion = -1; } } const cmpPriority = (a, b)=>a.priority - b.priority; const sortPriority = (arr)=>arr.sort(cmpPriority); class LayerComposition extends EventHandler { destroy() { this.destroyRenderActions(); } destroyRenderActions() { this._renderActions.forEach((ra)=>ra.destroy()); this._renderActions.length = 0; } markDirty() { this._dirty = true; } _update() { const len = this.layerList.length; if (!this._dirty) { for(let i = 0; i < len; i++){ if (this.layerList[i]._dirtyComposition) { this._dirty = true; break; } } } if (this._dirty) { this._dirty = false; this.cameras.length = 0; this.camerasSet.clear(); for(let i = 0; i < len; i++){ const layer = this.layerList[i]; layer._dirtyComposition = false; for(let j = 0; j < layer.cameras.length; j++){ const cameraComponent = layer.cameras[j]; if (!this.camerasSet.has(cameraComponent.camera)) { this.camerasSet.add(cameraComponent.camera); this.cameras.push(cameraComponent); } } } if (this.cameras.length > 1) { sortPriority(this.cameras); } let renderActionCount = 0; this.destroyRenderActions(); for(let i = 0; i < this.cameras.length; i++){ const camera = this.cameras[i]; if (camera.camera.renderPasses.length > 0) { this.addDummyRenderAction(renderActionCount, camera); renderActionCount++; continue; } let cameraFirstRenderAction = true; const cameraFirstRenderActionIndex = renderActionCount; let lastRenderAction = null; let postProcessMarked = false; for(let j = 0; j < len; j++){ const layer = this.layerList[j]; const isLayerEnabled = layer.enabled && this.subLayerEnabled[j]; if (isLayerEnabled) { if (layer.cameras.length > 0) { if (camera.layers.indexOf(layer.id) >= 0) { if (!postProcessMarked && layer.id === camera.disablePostEffectsLayer) { postProcessMarked = true; if (lastRenderAction) { lastRenderAction.triggerPostprocess = true; } } const isTransparent = this.subLayerList[j]; lastRenderAction = this.addRenderAction(renderActionCount, layer, isTransparent, camera, cameraFirstRenderAction, postProcessMarked); renderActionCount++; cameraFirstRenderAction = false; } } } } if (cameraFirstRenderActionIndex < renderActionCount) { lastRenderAction.lastCameraUse = true; } if (!postProcessMarked && lastRenderAction) { lastRenderAction.triggerPostprocess = true; } if (camera.renderTarget && camera.postEffectsEnabled) { this.propagateRenderTarget(cameraFirstRenderActionIndex - 1, camera); } } this._logRenderActions(); } } getNextRenderAction(renderActionIndex) { const renderAction = new RenderAction(); this._renderActions.push(renderAction); return renderAction; } addDummyRenderAction(renderActionIndex, camera) { const renderAction = this.getNextRenderAction(renderActionIndex); renderAction.camera = camera; renderAction.useCameraPasses = true; } addRenderAction(renderActionIndex, layer, isTransparent, camera, cameraFirstRenderAction, postProcessMarked) { let rt = layer.id !== LAYERID_DEPTH ? camera.renderTarget : null; let used = false; const renderActions = this._renderActions; for(let i = renderActionIndex - 1; i >= 0; i--){ if (renderActions[i].camera === camera && renderActions[i].renderTarget === rt) { used = true; break; } } if (postProcessMarked && camera.postEffectsEnabled) { rt = null; } const renderAction = this.getNextRenderAction(renderActionIndex); renderAction.triggerPostprocess = false; renderAction.layer = layer; renderAction.transparent = isTransparent; renderAction.camera = camera; renderAction.renderTarget = rt; renderAction.firstCameraUse = cameraFirstRenderAction; renderAction.lastCameraUse = false; const needsCameraClear = cameraFirstRenderAction || !used; const needsLayerClear = layer.clearColorBuffer || layer.clearDepthBuffer || layer.clearStencilBuffer; if (needsCameraClear || needsLayerClear) { renderAction.setupClears(needsCameraClear ? camera : undefined, layer); } return renderAction; } propagateRenderTarget(startIndex, fromCamera) { for(let a = startIndex; a >= 0; a--){ const ra = this._renderActions[a]; const layer = ra.layer; if (ra.renderTarget && layer.id !== LAYERID_DEPTH) { break; } if (layer.id === LAYERID_DEPTH) { continue; } if (ra.useCameraPasses) { break; } const thisCamera = ra?.camera.camera; if (thisCamera) { if (!fromCamera.camera.rect.equals(thisCamera.rect) || !fromCamera.camera.scissorRect.equals(thisCamera.scissorRect)) { break; } } ra.renderTarget = fromCamera.renderTarget; } } _logRenderActions() {} _isLayerAdded(layer) { const found = this.layerIdMap.get(layer.id) === layer; return found; } _isSublayerAdded(layer, transparent) { const map = transparent ? this.layerTransparentIndexMap : this.layerOpaqueIndexMap; if (map.get(layer) !== undefined) { return true; } return false; } push(layer) { if (this._isLayerAdded(layer)) return; this.layerList.push(layer); this.layerList.push(layer); this._opaqueOrder[layer.id] = this.subLayerList.push(false) - 1; this._transparentOrder[layer.id] = this.subLayerList.push(true) - 1; this.subLayerEnabled.push(true); this.subLayerEnabled.push(true); this._updateLayerMaps(); this._dirty = true; this.fire('add', layer); } insert(layer, index) { if (this._isLayerAdded(layer)) return; this.layerList.splice(index, 0, layer, layer); this.subLayerList.splice(index, 0, false, true); const count = this.layerList.length; this._updateOpaqueOrder(index, count - 1); this._updateTransparentOrder(index, count - 1); this.subLayerEnabled.splice(index, 0, true, true); this._updateLayerMaps(); this._dirty = true; this.fire('add', layer); } remove(layer) { let id = this.layerList.indexOf(layer); delete this._opaqueOrder[id]; delete this._transparentOrder[id]; while(id >= 0){ this.layerList.splice(id, 1); this.subLayerList.splice(id, 1); this.subLayerEnabled.splice(id, 1); id = this.layerList.indexOf(layer); this._dirty = true; this.fire('remove', layer); } const count = this.layerList.length; this._updateOpaqueOrder(0, count - 1); this._updateTransparentOrder(0, count - 1); this._updateLayerMaps(); } pushOpaque(layer) { if (this._isSublayerAdded(layer, false)) return; this.layerList.push(layer); this._opaqueOrder[layer.id] = this.subLayerList.push(false) - 1; this.subLayerEnabled.push(true); this._updateLayerMaps(); this._dirty = true; this.fire('add', layer); } insertOpaque(layer, index) { if (this._isSublayerAdded(layer, false)) return; this.layerList.splice(index, 0, layer); this.subLayerList.splice(index, 0, false); const count = this.subLayerList.length; this._updateOpaqueOrder(index, count - 1); this.subLayerEnabled.splice(index, 0, true); this._updateLayerMaps(); this._dirty = true; this.fire('add', layer); } removeOpaque(layer) { for(let i = 0, len = this.layerList.length; i < len; i++){ if (this.layerList[i] === layer && !this.subLayerList[i]) { this.layerList.splice(i, 1); this.subLayerList.splice(i, 1); len--; this._updateOpaqueOrder(i, len - 1); this.subLayerEnabled.splice(i, 1); this._dirty = true; if (this.layerList.indexOf(layer) < 0) { this.fire('remove', layer); } break; } } this._updateLayerMaps(); } pushTransparent(layer) { if (this._isSublayerAdded(layer, true)) return; this.layerList.push(layer); this._transparentOrder[layer.id] = this.subLayerList.push(true) - 1; this.subLayerEnabled.push(true); this._updateLayerMaps(); this._dirty = true; this.fire('add', layer); } insertTransparent(layer, index) { if (this._isSublayerAdded(layer, true)) return; this.layerList.splice(index, 0, layer); this.subLayerList.splice(index, 0, true); const count = this.subLayerList.length; this._updateTransparentOrder(index, count - 1); this.subLayerEnabled.splice(index, 0, true); this._updateLayerMaps(); this._dirty = true; this.fire('add', layer); } removeTransparent(layer) { for(let i = 0, len = this.layerList.length; i < len; i++){ if (this.layerList[i] === layer && this.subLayerList[i]) { this.layerList.splice(i, 1); this.subLayerList.splice(i, 1); len--; this._updateTransparentOrder(i, len - 1); this.subLayerEnabled.splice(i, 1); this._dirty = true; if (this.layerList.indexOf(layer) < 0) { this.fire('remove', layer); } break; } } this._updateLayerMaps(); } getOpaqueIndex(layer) { return this.layerOpaqueIndexMap.get(layer) ?? -1; } getTransparentIndex(layer) { return this.layerTransparentIndexMap.get(layer) ?? -1; } isEnabled(layer, transparent) { if (layer.enabled) { const index = transparent ? this.getTransparentIndex(layer) : this.getOpaqueIndex(layer); if (index >= 0) { return this.subLayerEnabled[index]; } } return false; } _updateLayerMaps() { this.layerIdMap.clear(); this.layerNameMap.clear(); this.layerOpaqueIndexMap.clear(); this.layerTransparentIndexMap.clear(); for(let i = 0; i < this.layerList.length; i++){ const layer = this.layerList[i]; this.layerIdMap.set(layer.id, layer); this.layerNameMap.set(layer.name, layer); const subLayerIndexMap = this.subLayerList[i] ? this.layerTransparentIndexMap : this.layerOpaqueIndexMap; subLayerIndexMap.set(layer, i); } } getLayerById(id) { return this.layerIdMap.get(id) ?? null; } getLayerByName(name) { return this.layerNameMap.get(name) ?? null; } _updateOpaqueOrder(startIndex, endIndex) { for(let i = startIndex; i <= endIndex; i++){ if (this.subLayerList[i] === false) { this._opaqueOrder[this.layerList[i].id] = i; } } } _updateTransparentOrder(startIndex, endIndex) { for(let i = startIndex; i <= endIndex; i++){ if (this.subLayerList[i] === true) { this._transparentOrder[this.layerList[i].id] = i; } } } _sortLayersDescending(layersA, layersB, order) { let topLayerA = -1; let topLayerB = -1; for(let i = 0, len = layersA.length; i < len; i++){ const id = layersA[i]; if (order.hasOwnProperty(id)) { topLayerA = Math.max(topLayerA, order[id]); } } for(let i = 0, len = layersB.length; i < len; i++){ const id = layersB[i]; if (order.hasOwnProperty(id)) { topLayerB = Math.max(topLayerB, order[id]); } } if (topLayerA === -1 && topLayerB !== -1) { return 1; } else if (topLayerB === -1 && topLayerA !== -1) { return -1; } return topLayerB - topLayerA; } sortTransparentLayers(layersA, layersB) { return this._sortLayersDescending(layersA, layersB, this._transparentOrder); } sortOpaqueLayers(layersA, layersB) { return this._sortLayersDescending(layersA, layersB, this._opaqueOrder); } constructor(name = 'Untitled'){ super(), this.layerList = [], this.layerIdMap = new Map(), this.layerNameMap = new Map(), this.layerOpaqueIndexMap = new Map(), this.layerTransparentIndexMap = new Map(), this.subLayerList = [], this.subLayerEnabled = [], this.cameras = [], this.camerasSet = new Set(), this._renderActions = [], this._dirty = false; this.name = name; this._opaqueOrder = {}; this._transparentOrder = {}; } } const tmpVec = new Vec3(); const tmpBiases = { bias: 0, normalBias: 0 }; const tmpColor = new Color(); const chanId = { r: 0, g: 1, b: 2, a: 3 }; const lightTypes = { 'directional': LIGHTTYPE_DIRECTIONAL, 'omni': LIGHTTYPE_OMNI, 'point': LIGHTTYPE_OMNI, 'spot': LIGHTTYPE_SPOT }; const directionalCascades = [ [ new Vec4(0, 0, 1, 1) ], [ new Vec4(0, 0, 0.5, 0.5), new Vec4(0, 0.5, 0.5, 0.5) ], [ new Vec4(0, 0, 0.5, 0.5), new Vec4(0, 0.5, 0.5, 0.5), new Vec4(0.5, 0, 0.5, 0.5) ], [ new Vec4(0, 0, 0.5, 0.5), new Vec4(0, 0.5, 0.5, 0.5), new Vec4(0.5, 0, 0.5, 0.5), new Vec4(0.5, 0.5, 0.5, 0.5) ] ]; const channelMap = { 'rrr': 0b0001, 'ggg': 0b0010, 'bbb': 0b0100, 'aaa': 0b1000, 'rgb': 0b0111 }; let id$2 = 0; class LightRenderData { destroy() { this.viewBindGroups.forEach((bg)=>{ bg.defaultUniformBuffer.destroy(); bg.destroy(); }); this.viewBindGroups.length = 0; } get shadowBuffer() { const rt = this.shadowCamera.renderTarget; if (rt) { return this.light._isPcf ? rt.depthBuffer : rt.colorBuffer; } return null; } constructor(camera, face, light){ this.light = light; this.camera = camera; this.shadowCamera = ShadowRenderer.createShadowCamera(light._shadowType, light._type, face); this.shadowMatrix = new Mat4(); this.shadowViewport = new Vec4(0, 0, 1, 1); this.shadowScissor = new Vec4(0, 0, 1, 1); this.projectionCompensation = 0; this.face = face; this.visibleCasters = []; this.viewBindGroups = []; } } class Light { destroy() { this._evtDeviceRestored?.off(); this._evtDeviceRestored = null; this._destroyShadowMap(); this.releaseRenderData(); this._renderData = null; } onDeviceRestored() { if (this.shadowUpdateMode === SHADOWUPDATE_NONE) { this.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } } releaseRenderData() { if (this._renderData) { for(let i = 0; i < this._renderData.length; i++){ this._renderData[i].destroy(); } this._renderData.length = 0; } } addLayer(layer) { this.layers.add(layer); } removeLayer(layer) { this.layers.delete(layer); } set shadowSamples(value) { this._softShadowParams[0] = value; } get shadowSamples() { return this._softShadowParams[0]; } set shadowBlockerSamples(value) { this._softShadowParams[1] = value; } get shadowBlockerSamples() { return this._softShadowParams[1]; } set shadowBias(value) { if (this._shadowBias !== value) { this._shadowBias = value; this._updateShadowBias(); } } get shadowBias() { return this._shadowBias; } set numCascades(value) { if (!this.cascades || this.numCascades !== value) { this.cascades = directionalCascades[value - 1]; this._shadowMatrixPalette = new Float32Array(4 * 16); this._shadowCascadeDistances = new Float32Array(4); this._destroyShadowMap(); this.updateKey(); } } get numCascades() { return this.cascades.length; } set cascadeBlend(value) { if (this._cascadeBlend !== value) { this._cascadeBlend = value; this.updateKey(); } } get cascadeBlend() { return this._cascadeBlend; } set shadowMap(shadowMap) { if (this._shadowMap !== shadowMap) { this._destroyShadowMap(); this._shadowMap = shadowMap; } } get shadowMap() { return this._shadowMap; } set mask(value) { if (this._mask !== value) { this._mask = value; this.updateKey(); this.updateClusteredFlags(); } } get mask() { return this._mask; } get numShadowFaces() { const type = this._type; if (type === LIGHTTYPE_DIRECTIONAL) { return this.numCascades; } else if (type === LIGHTTYPE_OMNI) { return 6; } return 1; } set type(value) { if (this._type === value) { return; } this._type = value; this._destroyShadowMap(); this._updateShadowBias(); this.updateKey(); this.updateClusteredFlags(); const stype = this._shadowType; this._shadowType = null; this.shadowUpdateOverrides = null; this.shadowType = stype; } get type() { return this._type; } set shape(value) { if (this._shape === value) { return; } this._shape = value; this._destroyShadowMap(); this.updateKey(); this.updateClusteredFlags(); const stype = this._shadowType; this._shadowType = null; this.shadowType = stype; } get shape() { return this._shape; } set usePhysicalUnits(value) { if (this._usePhysicalUnits !== value) { this._usePhysicalUnits = value; this._updateLinearColor(); } } get usePhysicalUnits() { return this._usePhysicalUnits; } set shadowType(value) { if (this._shadowType === value) { return; } let shadowInfo = shadowTypeInfo.get(value); if (!shadowInfo) { value = SHADOW_PCF3_32F; } const device = this.device; if (value === SHADOW_PCSS_32F && (!device.textureFloatRenderable || !device.textureFloatFilterable)) { value = SHADOW_PCF3_32F; } if (this._type === LIGHTTYPE_OMNI && value !== SHADOW_PCF1_32F && value !== SHADOW_PCF3_32F && value !== SHADOW_PCF1_16F && value !== SHADOW_PCF3_16F && value !== SHADOW_PCSS_32F) { value = SHADOW_PCF3_32F; } if (value === SHADOW_VSM_32F && (!device.textureFloatRenderable || !device.textureFloatFilterable)) { value = SHADOW_VSM_16F; } if (value === SHADOW_VSM_16F && !device.textureHalfFloatRenderable) { value = SHADOW_PCF3_32F; } shadowInfo = shadowTypeInfo.get(value); this._isVsm = shadowInfo?.vsm ?? false; this._isPcf = shadowInfo?.pcf ?? false; this._shadowType = value; this._destroyShadowMap(); this.updateKey(); } get shadowType() { return this._shadowType; } set enabled(value) { if (this._enabled !== value) { this._enabled = value; this.layersDirty(); } } get enabled() { return this._enabled; } set castShadows(value) { if (this._castShadows !== value) { this._castShadows = value; this._destroyShadowMap(); this.layersDirty(); this.updateKey(); } } get castShadows() { return this._castShadows && this._mask !== MASK_BAKE && this._mask !== 0; } set shadowIntensity(value) { if (this._shadowIntensity !== value) { this._shadowIntensity = value; this.updateKey(); } } get shadowIntensity() { return this._shadowIntensity; } get bakeShadows() { return this._castShadows && this._mask === MASK_BAKE; } set shadowResolution(value) { if (this._shadowResolution !== value) { if (this._type === LIGHTTYPE_OMNI) { value = Math.min(value, this.device.maxCubeMapSize); } else { value = Math.min(value, this.device.maxTextureSize); } this._shadowResolution = value; this._destroyShadowMap(); } } get shadowResolution() { return this._shadowResolution; } set vsmBlurSize(value) { if (this._vsmBlurSize === value) { return; } if (value % 2 === 0) value++; this._vsmBlurSize = value; } get vsmBlurSize() { return this._vsmBlurSize; } set normalOffsetBias(value) { if (this._normalOffsetBias !== value) { const dirty = !this._normalOffsetBias && value || this._normalOffsetBias && !value; this._normalOffsetBias = value; if (dirty) { this.updateKey(); } } } get normalOffsetBias() { return this._normalOffsetBias; } set falloffMode(value) { if (this._falloffMode === value) { return; } this._falloffMode = value; this.updateKey(); this.updateClusteredFlags(); } get falloffMode() { return this._falloffMode; } set innerConeAngle(value) { if (this._innerConeAngle === value) { return; } this._innerConeAngle = value; this._innerConeAngleCos = Math.cos(value * math.DEG_TO_RAD); this.updateClusterData(false, true); if (this._usePhysicalUnits) { this._updateLinearColor(); } } get innerConeAngle() { return this._innerConeAngle; } set outerConeAngle(value) { if (this._outerConeAngle === value) { return; } this._outerConeAngle = value; this._updateOuterAngle(value); if (this._usePhysicalUnits) { this._updateLinearColor(); } } get outerConeAngle() { return this._outerConeAngle; } set penumbraSize(value) { this._penumbraSize = value; this._softShadowParams[2] = value; } get penumbraSize() { return this._penumbraSize; } set penumbraFalloff(value) { this._softShadowParams[3] = value; } get penumbraFalloff() { return this._softShadowParams[3]; } _updateOuterAngle(angle) { const radAngle = angle * math.DEG_TO_RAD; this._outerConeAngleCos = Math.cos(radAngle); this._outerConeAngleSin = Math.sin(radAngle); this.updateClusterData(false, true); } set intensity(value) { if (this._intensity !== value) { this._intensity = value; this._updateLinearColor(); } } get intensity() { return this._intensity; } set affectSpecularity(value) { if (this._type === LIGHTTYPE_DIRECTIONAL) { this._affectSpecularity = value; this.updateKey(); } } get affectSpecularity() { return this._affectSpecularity; } set luminance(value) { if (this._luminance !== value) { this._luminance = value; this._updateLinearColor(); } } get luminance() { return this._luminance; } get cookieMatrix() { if (!this._cookieMatrix) { this._cookieMatrix = new Mat4(); } return this._cookieMatrix; } get atlasViewport() { if (!this._atlasViewport) { this._atlasViewport = new Vec4(0, 0, 1, 1); } return this._atlasViewport; } set cookie(value) { if (this._cookie === value) { return; } this._cookie = value; this.updateKey(); } get cookie() { return this._cookie; } set cookieFalloff(value) { if (this._cookieFalloff === value) { return; } this._cookieFalloff = value; this.updateKey(); } get cookieFalloff() { return this._cookieFalloff; } set cookieChannel(value) { if (this._cookieChannel === value) { return; } if (value.length < 3) { const chr = value.charAt(value.length - 1); const addLen = 3 - value.length; for(let i = 0; i < addLen; i++){ value += chr; } } this._cookieChannel = value; this.updateKey(); this.updateClusteredFlags(); } get cookieChannel() { return this._cookieChannel; } set cookieTransform(value) { if (this._cookieTransform === value) { return; } this._cookieTransform = value; this._cookieTransformSet = !!value; if (value && !this._cookieOffset) { this.cookieOffset = new Vec2(); this._cookieOffsetSet = false; } this.updateKey(); } get cookieTransform() { return this._cookieTransform; } set cookieOffset(value) { if (this._cookieOffset === value) { return; } const xformNew = !!(this._cookieTransformSet || value); if (xformNew && !value && this._cookieOffset) { this._cookieOffset.set(0, 0); } else { this._cookieOffset = value; } this._cookieOffsetSet = !!value; if (value && !this._cookieTransform) { this.cookieTransform = new Vec4(1, 1, 0, 0); this._cookieTransformSet = false; } this.updateKey(); } get cookieOffset() { return this._cookieOffset; } beginFrame() { this.visibleThisFrame = this._type === LIGHTTYPE_DIRECTIONAL && this._enabled; this.maxScreenSize = 0; this.atlasViewportAllocated = false; this.atlasSlotUpdated = false; } _destroyShadowMap() { this.releaseRenderData(); if (this._shadowMap) { if (!this._shadowMap.cached) { this._shadowMap.destroy(); } this._shadowMap = null; } if (this.shadowUpdateMode === SHADOWUPDATE_NONE) { this.shadowUpdateMode = SHADOWUPDATE_THISFRAME; } if (this.shadowUpdateOverrides) { for(let i = 0; i < this.shadowUpdateOverrides.length; i++){ if (this.shadowUpdateOverrides[i] === SHADOWUPDATE_NONE) { this.shadowUpdateOverrides[i] = SHADOWUPDATE_THISFRAME; } } } } getRenderData(camera, face) { for(let i = 0; i < this._renderData.length; i++){ const current = this._renderData[i]; if (current.camera === camera && current.face === face) { return current; } } const rd = new LightRenderData(camera, face, this); this._renderData.push(rd); return rd; } clone() { const clone = new Light(this.device, this.clusteredLighting); clone.type = this._type; clone.setColor(this._color); clone.intensity = this._intensity; clone.affectSpecularity = this._affectSpecularity; clone.luminance = this._luminance; clone.castShadows = this.castShadows; clone._enabled = this._enabled; clone.attenuationStart = this.attenuationStart; clone.attenuationEnd = this.attenuationEnd; clone.falloffMode = this._falloffMode; clone.shadowType = this._shadowType; clone.vsmBlurSize = this._vsmBlurSize; clone.vsmBlurMode = this.vsmBlurMode; clone.vsmBias = this.vsmBias; clone.shadowUpdateMode = this.shadowUpdateMode; clone.mask = this.mask; if (this.shadowUpdateOverrides) { clone.shadowUpdateOverrides = this.shadowUpdateOverrides.slice(); } clone.innerConeAngle = this._innerConeAngle; clone.outerConeAngle = this._outerConeAngle; clone.numCascades = this.numCascades; clone.cascadeDistribution = this.cascadeDistribution; clone.cascadeBlend = this._cascadeBlend; clone.shape = this._shape; clone.shadowDepthState.copy(this.shadowDepthState); clone.shadowBias = this.shadowBias; clone.normalOffsetBias = this._normalOffsetBias; clone.shadowResolution = this._shadowResolution; clone.shadowDistance = this.shadowDistance; clone.shadowIntensity = this.shadowIntensity; clone.shadowSamples = this.shadowSamples; clone.shadowBlockerSamples = this.shadowBlockerSamples; clone.penumbraSize = this.penumbraSize; clone.penumbraFalloff = this.penumbraFalloff; return clone; } static getLightUnitConversion(type, outerAngle = Math.PI / 4, innerAngle = 0) { switch(type){ case LIGHTTYPE_SPOT: { const falloffEnd = Math.cos(outerAngle); const falloffStart = Math.cos(innerAngle); return 2 * Math.PI * (1 - falloffStart + (falloffStart - falloffEnd) / 2.0); } case LIGHTTYPE_OMNI: return 4 * Math.PI; case LIGHTTYPE_DIRECTIONAL: return 1; } } _getUniformBiasValues(lightRenderData) { const farClip = lightRenderData.shadowCamera._farClip; switch(this._type){ case LIGHTTYPE_OMNI: tmpBiases.bias = this.shadowBias; tmpBiases.normalBias = this._normalOffsetBias; break; case LIGHTTYPE_SPOT: if (this._isVsm) { tmpBiases.bias = -1e-5 * 20; } else { tmpBiases.bias = this.shadowBias * 20; } tmpBiases.normalBias = this._isVsm ? this.vsmBias / (this.attenuationEnd / 7.0) : this._normalOffsetBias; break; case LIGHTTYPE_DIRECTIONAL: if (this._isVsm) { tmpBiases.bias = -1e-5 * 20; } else { tmpBiases.bias = this.shadowBias / farClip * 100; } tmpBiases.normalBias = this._isVsm ? this.vsmBias / (farClip / 7.0) : this._normalOffsetBias; break; } return tmpBiases; } getColor() { return this._color; } getBoundingSphere(sphere) { if (this._type === LIGHTTYPE_SPOT) { const size = this.attenuationEnd; const angle = this._outerConeAngle; const cosAngle = this._outerConeAngleCos; const node = this._node; tmpVec.copy(node.up); if (angle > 45) { sphere.radius = size * this._outerConeAngleSin; tmpVec.mulScalar(-size * cosAngle); } else { sphere.radius = size / (2 * cosAngle); tmpVec.mulScalar(-sphere.radius); } sphere.center.add2(node.getPosition(), tmpVec); } else if (this._type === LIGHTTYPE_OMNI) { sphere.center = this._node.getPosition(); sphere.radius = this.attenuationEnd; } } getBoundingBox(box) { if (this._type === LIGHTTYPE_SPOT) { const range = this.attenuationEnd; const angle = this._outerConeAngle; const node = this._node; const scl = Math.abs(Math.sin(angle * math.DEG_TO_RAD) * range); box.center.set(0, -range * 0.5, 0); box.halfExtents.set(scl, range * 0.5, scl); box.setFromTransformedAabb(box, node.getWorldTransform(), true); } else if (this._type === LIGHTTYPE_OMNI) { box.center.copy(this._node.getPosition()); box.halfExtents.set(this.attenuationEnd, this.attenuationEnd, this.attenuationEnd); } } _updateShadowBias() { if (this._type === LIGHTTYPE_OMNI && !this.clusteredLighting) { this.shadowDepthState.depthBias = 0; this.shadowDepthState.depthBiasSlope = 0; } else { const bias = this.shadowBias * -1e3; this.shadowDepthState.depthBias = bias; this.shadowDepthState.depthBiasSlope = bias; } } _updateLinearColor() { let intensity = this._intensity; if (this._usePhysicalUnits) { intensity = this._luminance / Light.getLightUnitConversion(this._type, this._outerConeAngle * math.DEG_TO_RAD, this._innerConeAngle * math.DEG_TO_RAD); } const color = this._color; const colorLinear = this._colorLinear; if (intensity >= 1) { tmpColor.linear(color).mulScalar(intensity); } else { tmpColor.copy(color).mulScalar(intensity).linear(); } colorLinear[0] = tmpColor.r; colorLinear[1] = tmpColor.g; colorLinear[2] = tmpColor.b; this.updateClusterData(true); } setColor() { if (arguments.length === 1) { this._color.set(arguments[0].r, arguments[0].g, arguments[0].b); } else if (arguments.length === 3) { this._color.set(arguments[0], arguments[1], arguments[2]); } this._updateLinearColor(); } layersDirty() { this.layers.forEach((layer)=>{ if (layer.hasLight(this)) { layer.markLightsDirty(); } }); } updateKey() { let key = this._type << 29 | this._shadowType << 25 | this._falloffMode << 23 | (this._normalOffsetBias !== 0.0 ? 1 : 0) << 22 | (this._cookie ? 1 : 0) << 21 | (this._cookieFalloff ? 1 : 0) << 20 | chanId[this._cookieChannel.charAt(0)] << 18 | (this._cookieTransform ? 1 : 0) << 12 | this._shape << 10 | (this.numCascades > 0 ? 1 : 0) << 9 | (this._cascadeBlend > 0 ? 1 : 0) << 8 | (this.affectSpecularity ? 1 : 0) << 7 | this.mask << 6 | (this._castShadows ? 1 : 0) << 3; if (this._cookieChannel.length === 3) { key |= chanId[this._cookieChannel.charAt(1)] << 16; key |= chanId[this._cookieChannel.charAt(2)] << 14; } if (key !== this.key) { this.layersDirty(); } this.key = key; } updateClusteredFlags() { const isDynamic = !!(this.mask & MASK_AFFECT_DYNAMIC); const isLightmapped = !!(this.mask & MASK_AFFECT_LIGHTMAPPED); this.clusteredFlags = (this.type === LIGHTTYPE_SPOT ? 1 : 0) << 30 | (this._shape & 0x3) << 28 | (this._falloffMode & 0x1) << 27 | (channelMap[this._cookieChannel] ?? 0) << 23 | (isDynamic ? 1 : 0) << 22 | (isLightmapped ? 1 : 0) << 21; } getClusteredFlags(castShadows, useCookie) { return this.clusteredFlags | ((castShadows ? Math.floor(this.shadowIntensity * 255) : 0) & 0xFF) << 0 | ((useCookie ? Math.floor(this.cookieIntensity * 255) : 0) & 0xFF) << 8; } updateClusterData(updateColor, updateAngles) { const { clusteredData16 } = this; const float2Half = FloatPacking.float2Half; if (updateColor) { clusteredData16[0] = float2Half(math.clamp(this._colorLinear[0] / LIGHT_COLOR_DIVIDER, 0, 65504)); clusteredData16[1] = float2Half(math.clamp(this._colorLinear[1] / LIGHT_COLOR_DIVIDER, 0, 65504)); clusteredData16[2] = float2Half(math.clamp(this._colorLinear[2] / LIGHT_COLOR_DIVIDER, 0, 65504)); } if (updateAngles) { const cosThreshold = 0.5; let flags = 0; const angleShrinkFactor = 0.99; let innerCos = Math.cos(this._innerConeAngle * angleShrinkFactor * math.DEG_TO_RAD); if (innerCos > cosThreshold) { innerCos = 1.0 - innerCos; flags |= 1; } let outerCos = Math.cos(this._outerConeAngle * angleShrinkFactor * math.DEG_TO_RAD); if (outerCos > cosThreshold) { outerCos = 1.0 - outerCos; flags |= 2; } clusteredData16[3] = flags; clusteredData16[4] = float2Half(innerCos); clusteredData16[5] = float2Half(outerCos); } } constructor(graphicsDevice, clusteredLighting){ this.layers = new Set(); this.shadowDepthState = DepthState.DEFAULT.clone(); this.clusteredFlags = 0; this.clusteredData = new Uint32Array(3); this.clusteredData16 = new Uint16Array(this.clusteredData.buffer); this._evtDeviceRestored = null; this.device = graphicsDevice; this.clusteredLighting = clusteredLighting; this.id = id$2++; this._evtDeviceRestored = graphicsDevice.on('devicerestored', this.onDeviceRestored, this); this._type = LIGHTTYPE_DIRECTIONAL; this._color = new Color(0.8, 0.8, 0.8); this._intensity = 1; this._affectSpecularity = true; this._luminance = 0; this._castShadows = false; this._enabled = false; this._mask = MASK_AFFECT_DYNAMIC; this.isStatic = false; this.key = 0; this.bakeDir = true; this.bakeNumSamples = 1; this.bakeArea = 0; this.attenuationStart = 10; this.attenuationEnd = 10; this._falloffMode = LIGHTFALLOFF_LINEAR; this._shadowType = SHADOW_PCF3_32F; this._vsmBlurSize = 11; this.vsmBlurMode = BLUR_GAUSSIAN; this.vsmBias = 0.01 * 0.25; this._cookie = null; this.cookieIntensity = 1; this._cookieFalloff = true; this._cookieChannel = 'rgb'; this._cookieTransform = null; this._cookieTransformUniform = new Float32Array(4); this._cookieOffset = null; this._cookieOffsetUniform = new Float32Array(2); this._cookieTransformSet = false; this._cookieOffsetSet = false; this._innerConeAngle = 40; this._outerConeAngle = 45; this.cascades = null; this._shadowMatrixPalette = null; this._shadowCascadeDistances = null; this.numCascades = 1; this._cascadeBlend = 0; this.cascadeDistribution = 0.5; this._shape = LIGHTSHAPE_PUNCTUAL; this._colorLinear = new Float32Array(3); this._updateLinearColor(); this._position = new Vec3(0, 0, 0); this._direction = new Vec3(0, 0, 0); this._innerConeAngleCos = Math.cos(this._innerConeAngle * math.DEG_TO_RAD); this._updateOuterAngle(this._outerConeAngle); this._usePhysicalUnits = undefined; this._shadowMap = null; this._shadowRenderParams = []; this._shadowCameraParams = []; this.shadowDistance = 40; this._shadowResolution = 1024; this._shadowBias = -5e-4; this._shadowIntensity = 1.0; this._normalOffsetBias = 0.0; this.shadowUpdateMode = SHADOWUPDATE_REALTIME; this.shadowUpdateOverrides = null; this._isVsm = false; this._isPcf = true; this._softShadowParams = new Float32Array(4); this.shadowSamples = 16; this.shadowBlockerSamples = 16; this.penumbraSize = 1.0; this.penumbraFalloff = 1.0; this._cookieMatrix = null; this._atlasViewport = null; this.atlasViewportAllocated = false; this.atlasVersion = 0; this.atlasSlotIndex = 0; this.atlasSlotUpdated = false; this._node = null; this._renderData = []; this.visibleThisFrame = false; this.maxScreenSize = 0; this._updateShadowBias(); } } class LightingParams { applySettings(render) { this.shadowsEnabled = render.lightingShadowsEnabled ?? this.shadowsEnabled; this.cookiesEnabled = render.lightingCookiesEnabled ?? this.cookiesEnabled; this.areaLightsEnabled = render.lightingAreaLightsEnabled ?? this.areaLightsEnabled; this.shadowAtlasResolution = render.lightingShadowAtlasResolution ?? this.shadowAtlasResolution; this.cookieAtlasResolution = render.lightingCookieAtlasResolution ?? this.cookieAtlasResolution; this.maxLightsPerCell = render.lightingMaxLightsPerCell ?? this.maxLightsPerCell; this.shadowType = render.lightingShadowType ?? this.shadowType; if (render.lightingCells) { this.cells = new Vec3(render.lightingCells); } } set cells(value) { this._cells.copy(value); } get cells() { return this._cells; } set maxLightsPerCell(value) { this._maxLightsPerCell = math.clamp(value, 1, 255); } get maxLightsPerCell() { return this._maxLightsPerCell; } set cookieAtlasResolution(value) { this._cookieAtlasResolution = math.clamp(value, 32, this._maxTextureSize); } get cookieAtlasResolution() { return this._cookieAtlasResolution; } set shadowAtlasResolution(value) { this._shadowAtlasResolution = math.clamp(value, 32, this._maxTextureSize); } get shadowAtlasResolution() { return this._shadowAtlasResolution; } set shadowType(value) { if (this._shadowType !== value) { this._shadowType = value; this._dirtyLightsFnc(); } } get shadowType() { return this._shadowType; } set cookiesEnabled(value) { if (this._cookiesEnabled !== value) { this._cookiesEnabled = value; this._dirtyLightsFnc(); } } get cookiesEnabled() { return this._cookiesEnabled; } set areaLightsEnabled(value) { if (this._supportsAreaLights) { if (this._areaLightsEnabled !== value) { this._areaLightsEnabled = value; this._dirtyLightsFnc(); } } } get areaLightsEnabled() { return this._areaLightsEnabled; } set shadowsEnabled(value) { if (this._shadowsEnabled !== value) { this._shadowsEnabled = value; this._dirtyLightsFnc(); } } get shadowsEnabled() { return this._shadowsEnabled; } constructor(supportsAreaLights, maxTextureSize, dirtyLightsFnc){ this._areaLightsEnabled = false; this._cells = new Vec3(10, 3, 10); this._maxLightsPerCell = 255; this._shadowsEnabled = true; this._shadowType = SHADOW_PCF3_32F; this._shadowAtlasResolution = 2048; this._cookiesEnabled = false; this._cookieAtlasResolution = 2048; this.atlasSplit = null; this._supportsAreaLights = supportsAreaLights; this._maxTextureSize = maxTextureSize; this._dirtyLightsFnc = dirtyLightsFnc; } } class MorphInstance { destroy() { this.shader = null; const morph = this.morph; if (morph) { this.morph = null; morph.decRefCount(); if (morph.refCount < 1) { morph.destroy(); } } this.rtPositions?.destroy(); this.rtPositions = null; this.texturePositions?.destroy(); this.texturePositions = null; this.rtNormals?.destroy(); this.rtNormals = null; this.textureNormals?.destroy(); this.textureNormals = null; } clone() { return new MorphInstance(this.morph); } _getWeightIndex(key) { if (typeof key === 'string') { const index = this._weightMap.get(key); return index; } return key; } getWeight(key) { const index = this._getWeightIndex(key); return this._weights[index]; } setWeight(key, weight) { const index = this._getWeightIndex(key); this._weights[index] = weight; this._dirty = true; } _createShader(maxCount) { const defines = new Map(); defines.set('{MORPH_TEXTURE_MAX_COUNT}', maxCount); if (this.morph.intRenderFormat) defines.set('MORPH_INT', ''); const outputType = this.morph.intRenderFormat ? 'uvec4' : 'vec4'; return ShaderUtils.createShader(this.device, { uniqueName: `TextureMorphShader_${maxCount}-${this.morph.intRenderFormat ? 'int' : 'float'}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'morphVS', fragmentChunk: 'morphPS', fragmentDefines: defines, fragmentOutputTypes: [ outputType ] }); } _updateTextureRenderTarget(renderTarget, activeCount, isPos) { const { morph, device } = this; this.setAabbUniforms(isPos); this.morphTextureId.setValue(isPos ? morph.targetsTexturePositions : morph.targetsTextureNormals); device.setBlendState(BlendState.NOBLEND); this.countId.setValue(activeCount); this.morphFactor.setValue(this._shaderMorphWeights); this.morphIndex.setValue(this._shaderMorphIndex); drawQuadWithShader(device, renderTarget, this.shader); } _updateTextureMorph(activeCount) { this.device; if (activeCount > 0 || !this.zeroTextures) { if (this.rtPositions) { this._updateTextureRenderTarget(this.rtPositions, activeCount, true); } if (this.rtNormals) { this._updateTextureRenderTarget(this.rtNormals, activeCount, false); } this.zeroTextures = activeCount === 0; } } setAabbUniforms(isPos = true) { this.aabbSizeId.setValue(isPos ? this._aabbSize : this._aabbNrmSize); this.aabbMinId.setValue(isPos ? this._aabbMin : this._aabbNrmMin); } prepareRendering(device) { this.setAabbUniforms(); } update() { this._dirty = false; const targets = this.morph._targets; const epsilon = 0.00001; const weights = this._shaderMorphWeights; const indices = this._shaderMorphIndex; let activeCount = 0; for(let i = 0; i < targets.length; i++){ if (Math.abs(this.getWeight(i)) > epsilon) { weights[activeCount] = this.getWeight(i); indices[activeCount] = i; activeCount++; } } this._updateTextureMorph(activeCount); } constructor(morph){ this.morph = morph; morph.incRefCount(); this.device = morph.device; const maxNumTargets = morph._targets.length; this.shader = this._createShader(maxNumTargets); this._weights = []; this._weightMap = new Map(); for(let v = 0; v < morph._targets.length; v++){ const target = morph._targets[v]; if (target.name) { this._weightMap.set(target.name, v); } this.setWeight(v, target.defaultWeight); } this._shaderMorphWeights = new Float32Array(maxNumTargets); this._shaderMorphIndex = new Uint32Array(maxNumTargets); const createRT = (name, textureVar)=>{ this[textureVar] = morph._createTexture(name, morph._renderTextureFormat); return new RenderTarget({ colorBuffer: this[textureVar], depth: false }); }; if (morph.morphPositions) { this.rtPositions = createRT('MorphRTPos', 'texturePositions'); } if (morph.morphNormals) { this.rtNormals = createRT('MorphRTNrm', 'textureNormals'); } this._textureParams = new Float32Array([ morph.morphTextureWidth, morph.morphTextureHeight ]); const halfSize = morph.aabb.halfExtents; this._aabbSize = new Float32Array([ halfSize.x * 4, halfSize.y * 4, halfSize.z * 4 ]); const min = morph.aabb.getMin(); this._aabbMin = new Float32Array([ min.x * 2, min.y * 2, min.z * 2 ]); this._aabbNrmSize = new Float32Array([ 2, 2, 2 ]); this._aabbNrmMin = new Float32Array([ -1, -1, -1 ]); this.aabbSizeId = this.device.scope.resolve('aabbSize'); this.aabbMinId = this.device.scope.resolve('aabbMin'); this.morphTextureId = this.device.scope.resolve('morphTexture'); this.morphFactor = this.device.scope.resolve('morphFactor[0]'); this.morphIndex = this.device.scope.resolve('morphIndex[0]'); this.countId = this.device.scope.resolve('count'); this.zeroTextures = false; } } class Model { getGraph() { return this.graph; } setGraph(graph) { this.graph = graph; } getCameras() { return this.cameras; } setCameras(cameras) { this.cameras = cameras; } getLights() { return this.lights; } setLights(lights) { this.lights = lights; } getMaterials() { const materials = []; for(let i = 0; i < this.meshInstances.length; i++){ const meshInstance = this.meshInstances[i]; if (materials.indexOf(meshInstance.material) === -1) { materials.push(meshInstance.material); } } return materials; } clone() { const srcNodes = []; const cloneNodes = []; const _duplicate = function(node) { const newNode = node.clone(); srcNodes.push(node); cloneNodes.push(newNode); for(let idx = 0; idx < node._children.length; idx++){ newNode.addChild(_duplicate(node._children[idx])); } return newNode; }; const cloneGraph = _duplicate(this.graph); const cloneMeshInstances = []; const cloneSkinInstances = []; const cloneMorphInstances = []; for(let i = 0; i < this.skinInstances.length; i++){ const skin = this.skinInstances[i].skin; const cloneSkinInstance = new SkinInstance(skin); const bones = []; for(let j = 0; j < skin.boneNames.length; j++){ const boneName = skin.boneNames[j]; const bone = cloneGraph.findByName(boneName); bones.push(bone); } cloneSkinInstance.bones = bones; cloneSkinInstances.push(cloneSkinInstance); } for(let i = 0; i < this.morphInstances.length; i++){ const morph = this.morphInstances[i].morph; const cloneMorphInstance = new MorphInstance(morph); cloneMorphInstances.push(cloneMorphInstance); } for(let i = 0; i < this.meshInstances.length; i++){ const meshInstance = this.meshInstances[i]; const nodeIndex = srcNodes.indexOf(meshInstance.node); const cloneMeshInstance = new MeshInstance(meshInstance.mesh, meshInstance.material, cloneNodes[nodeIndex]); if (meshInstance.skinInstance) { const skinInstanceIndex = this.skinInstances.indexOf(meshInstance.skinInstance); cloneMeshInstance.skinInstance = cloneSkinInstances[skinInstanceIndex]; } if (meshInstance.morphInstance) { const morphInstanceIndex = this.morphInstances.indexOf(meshInstance.morphInstance); cloneMeshInstance.morphInstance = cloneMorphInstances[morphInstanceIndex]; } cloneMeshInstances.push(cloneMeshInstance); } const clone = new Model(); clone.graph = cloneGraph; clone.meshInstances = cloneMeshInstances; clone.skinInstances = cloneSkinInstances; clone.morphInstances = cloneMorphInstances; clone.getGraph().syncHierarchy(); return clone; } destroy() { const meshInstances = this.meshInstances; for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].destroy(); } this.meshInstances.length = 0; } generateWireframe() { MeshInstance._prepareRenderStyleForArray(this.meshInstances, RENDERSTYLE_WIREFRAME); } constructor(){ this.graph = null; this.meshInstances = []; this.skinInstances = []; this.morphInstances = []; this.cameras = []; this.lights = []; this._shadersVersion = 0; this._immutable = false; } } class Morph extends RefCountedObject { destroy() { this.vertexBufferIds?.destroy(); this.vertexBufferIds = null; this.targetsTexturePositions?.destroy(); this.targetsTexturePositions = null; this.targetsTextureNormals?.destroy(); this.targetsTextureNormals = null; } get aabb() { if (!this._aabb) { const min = new Vec3(); const max = new Vec3(); for(let i = 0; i < this._targets.length; i++){ const targetAabb = this._targets[i].aabb; min.min(targetAabb.getMin()); max.max(targetAabb.getMax()); } this._aabb = new BoundingBox(); this._aabb.setMinMax(min, max); } return this._aabb; } get morphPositions() { return this._morphPositions; } get morphNormals() { return this._morphNormals; } _init() { this._initTextureBased(); for(let i = 0; i < this._targets.length; i++){ this._targets[i]._postInit(); } } _findSparseSet(deltaArrays, ids, usedDataIndices) { let freeIndex = 1; const dataCount = deltaArrays[0].length; for(let v = 0; v < dataCount; v += 3){ let vertexUsed = false; for(let i = 0; i < deltaArrays.length; i++){ const data = deltaArrays[i]; if (data[v] !== 0 || data[v + 1] !== 0 || data[v + 2] !== 0) { vertexUsed = true; break; } } if (vertexUsed) { ids.push(freeIndex); usedDataIndices.push(v / 3); freeIndex++; } else { ids.push(0); } } return freeIndex; } _initTextureBased() { const deltaArrays = [], deltaInfos = []; const targets = this._targets; for(let i = 0; i < targets.length; i++){ const target = targets[i]; if (target.options.deltaPositions) { deltaArrays.push(target.options.deltaPositions); deltaInfos.push(true); } if (target.options.deltaNormals) { deltaArrays.push(target.options.deltaNormals); deltaInfos.push(false); } } const ids = [], usedDataIndices = []; const freeIndex = this._findSparseSet(deltaArrays, ids, usedDataIndices); const maxTextureSize = this.device.maxTextureSize; let morphTextureWidth = Math.ceil(Math.sqrt(freeIndex)); morphTextureWidth = Math.min(morphTextureWidth, maxTextureSize); const morphTextureHeight = Math.ceil(freeIndex / morphTextureWidth); if (morphTextureHeight > maxTextureSize) { return; } this.morphTextureWidth = morphTextureWidth; this.morphTextureHeight = morphTextureHeight; let halfFloat = false; const float2Half = FloatPacking.float2Half; if (this._textureFormat === PIXELFORMAT_RGBA16F) { halfFloat = true; } const texturesDataPositions = []; const texturesDataNormals = []; const textureDataSize = morphTextureWidth * morphTextureHeight * 4; for(let i = 0; i < deltaArrays.length; i++){ const data = deltaArrays[i]; const textureData = this._textureFormat === PIXELFORMAT_RGBA16F ? new Uint16Array(textureDataSize) : new Float32Array(textureDataSize); (deltaInfos[i] ? texturesDataPositions : texturesDataNormals).push(textureData); if (halfFloat) { for(let v = 0; v < usedDataIndices.length; v++){ const index = usedDataIndices[v] * 3; const dstIndex = v * 4 + 4; textureData[dstIndex] = float2Half(data[index]); textureData[dstIndex + 1] = float2Half(data[index + 1]); textureData[dstIndex + 2] = float2Half(data[index + 2]); } } else { for(let v = 0; v < usedDataIndices.length; v++){ const index = usedDataIndices[v] * 3; const dstIndex = v * 4 + 4; textureData[dstIndex] = data[index]; textureData[dstIndex + 1] = data[index + 1]; textureData[dstIndex + 2] = data[index + 2]; } } } if (texturesDataPositions.length > 0) { this.targetsTexturePositions = this._createTexture('MorphPositionsTexture', this._textureFormat, targets.length, [ texturesDataPositions ]); } if (texturesDataNormals.length > 0) { this.targetsTextureNormals = this._createTexture('MorphNormalsTexture', this._textureFormat, targets.length, [ texturesDataNormals ]); } const formatDesc = [ { semantic: SEMANTIC_ATTR15, components: 1, type: TYPE_UINT32, asInt: true } ]; this.vertexBufferIds = new VertexBuffer(this.device, new VertexFormat(this.device, formatDesc, ids.length), ids.length, { data: new Uint32Array(ids) }); return true; } get targets() { return this._targets; } _updateMorphFlags() { this._morphPositions = false; this._morphNormals = false; for(let i = 0; i < this._targets.length; i++){ const target = this._targets[i]; if (target.morphPositions) { this._morphPositions = true; } if (target.morphNormals) { this._morphNormals = true; } } } _createTexture(name, format, arrayLength, levels) { return new Texture(this.device, { levels: levels, arrayLength: arrayLength, width: this.morphTextureWidth, height: this.morphTextureHeight, format: format, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: name }); } constructor(targets, graphicsDevice, { preferHighPrecision = false } = {}){ super(); this.device = graphicsDevice; const device = graphicsDevice; this.preferHighPrecision = preferHighPrecision; this._targets = targets.slice(); const renderableHalf = device.textureHalfFloatRenderable ? PIXELFORMAT_RGBA16F : undefined; const renderableFloat = device.textureFloatRenderable ? PIXELFORMAT_RGBA32F : undefined; this._renderTextureFormat = this.preferHighPrecision ? renderableFloat ?? renderableHalf : renderableHalf ?? renderableFloat; this._renderTextureFormat = this._renderTextureFormat ?? PIXELFORMAT_RGBA16U; this.intRenderFormat = isIntegerPixelFormat(this._renderTextureFormat); this._textureFormat = this.preferHighPrecision ? PIXELFORMAT_RGBA32F : PIXELFORMAT_RGBA16F; this._init(); this._updateMorphFlags(); } } class MorphTarget { get name() { return this._name; } get defaultWeight() { return this._defaultWeight; } get aabb() { if (!this._aabb) { this._aabb = new BoundingBox(); if (this.deltaPositions) { this._aabb.compute(this.deltaPositions); } } return this._aabb; } clone() { return new MorphTarget(this.options); } _postInit() { if (!this.options.preserveData) { this.options = null; } this.used = true; } constructor(options){ this.used = false; this.options = options; this._name = options.name; this._defaultWeight = options.defaultWeight || 0; this._aabb = options.aabb; this.deltaPositions = options.deltaPositions; this.morphPositions = !!options.deltaPositions; this.morphNormals = !!options.deltaNormals; } } let nonUniformScale; let uniformScale = 1; const particleTexChannels$1 = 4; const rotMat = new Mat4(); const rotMatInv = new Mat4(); const randomPosTformed = new Vec3(); const randomPos = new Vec3(); const rndFactor3Vec = new Vec3(); const particlePosPrev = new Vec3(); const velocityVec = new Vec3(); const localVelocityVec = new Vec3(); const velocityVec2 = new Vec3(); const localVelocityVec2 = new Vec3(); const radialVelocityVec = new Vec3(); const particlePos = new Vec3(); const particleFinalPos = new Vec3(); const moveDirVec = new Vec3(); const tmpVec3$1 = new Vec3(); function frac(f) { return f - Math.floor(f); } function saturate$1(x) { return Math.max(Math.min(x, 1), 0); } function glMod(x, y) { return x - y * Math.floor(x / y); } function encodeFloatRGBA(v) { let encX = frac(v); let encY = frac(255.0 * v); let encZ = frac(65025.0 * v); let encW = frac(160581375.0 * v); encX -= encY / 255.0; encY -= encZ / 255.0; encZ -= encW / 255.0; encW -= encW / 255.0; return [ encX, encY, encZ, encW ]; } function encodeFloatRG(v) { let encX = frac(v); let encY = frac(255.0 * v); encX -= encY / 255.0; encY -= encY / 255.0; return [ encX, encY ]; } class ParticleCPUUpdater { calcSpawnPosition(particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, i) { const emitter = this._emitter; const rX = Math.random(); const rY = Math.random(); const rZ = Math.random(); const rW = Math.random(); if (emitter.useCpu) { particleTex[i * particleTexChannels$1 + 0 + emitter.numParticlesPot * 2 * particleTexChannels$1] = rX; particleTex[i * particleTexChannels$1 + 1 + emitter.numParticlesPot * 2 * particleTexChannels$1] = rY; particleTex[i * particleTexChannels$1 + 2 + emitter.numParticlesPot * 2 * particleTexChannels$1] = rZ; } randomPos.x = rX - 0.5; randomPos.y = rY - 0.5; randomPos.z = rZ - 0.5; if (emitter.emitterShape === EMITTERSHAPE_BOX) { const max = Math.max(Math.abs(randomPos.x), Math.max(Math.abs(randomPos.y), Math.abs(randomPos.z))); const edgeX = max + (0.5 - max) * extentsInnerRatioUniform[0]; const edgeY = max + (0.5 - max) * extentsInnerRatioUniform[1]; const edgeZ = max + (0.5 - max) * extentsInnerRatioUniform[2]; randomPos.x = edgeX * (max === Math.abs(randomPos.x) ? Math.sign(randomPos.x) : 2 * randomPos.x); randomPos.y = edgeY * (max === Math.abs(randomPos.y) ? Math.sign(randomPos.y) : 2 * randomPos.y); randomPos.z = edgeZ * (max === Math.abs(randomPos.z) ? Math.sign(randomPos.z) : 2 * randomPos.z); if (!emitter.localSpace) { randomPosTformed.copy(emitterPos).add(spawnMatrix.transformPoint(randomPos)); } else { randomPosTformed.copy(spawnMatrix.transformPoint(randomPos)); } } else { randomPos.normalize(); const spawnBoundsSphereInnerRatio = emitter.emitterRadius === 0 ? 0 : emitter.emitterRadiusInner / emitter.emitterRadius; const r = rW * (1.0 - spawnBoundsSphereInnerRatio) + spawnBoundsSphereInnerRatio; if (!emitter.localSpace) { randomPosTformed.copy(emitterPos).add(randomPos.mulScalar(r * emitter.emitterRadius)); } else { randomPosTformed.copy(randomPos.mulScalar(r * emitter.emitterRadius)); } } const particleRate = math.lerp(emitter.rate, emitter.rate2, rX); let startSpawnTime = -particleRate * i; if (emitter.pack8) { const packX = (randomPosTformed.x - emitter.worldBounds.center.x) / emitter.worldBoundsSize.x + 0.5; const packY = (randomPosTformed.y - emitter.worldBounds.center.y) / emitter.worldBoundsSize.y + 0.5; const packZ = (randomPosTformed.z - emitter.worldBounds.center.z) / emitter.worldBoundsSize.z + 0.5; let packA = math.lerp(emitter.startAngle * math.DEG_TO_RAD, emitter.startAngle2 * math.DEG_TO_RAD, rX); packA = packA % (Math.PI * 2) / (Math.PI * 2); const rg0 = encodeFloatRG(packX); particleTex[i * particleTexChannels$1] = rg0[0]; particleTex[i * particleTexChannels$1 + 1] = rg0[1]; const ba0 = encodeFloatRG(packY); particleTex[i * particleTexChannels$1 + 2] = ba0[0]; particleTex[i * particleTexChannels$1 + 3] = ba0[1]; const rg1 = encodeFloatRG(packZ); particleTex[i * particleTexChannels$1 + 0 + emitter.numParticlesPot * particleTexChannels$1] = rg1[0]; particleTex[i * particleTexChannels$1 + 1 + emitter.numParticlesPot * particleTexChannels$1] = rg1[1]; const ba1 = encodeFloatRG(packA); particleTex[i * particleTexChannels$1 + 2 + emitter.numParticlesPot * particleTexChannels$1] = ba1[0]; particleTex[i * particleTexChannels$1 + 3 + emitter.numParticlesPot * particleTexChannels$1] = ba1[1]; const a2 = 1.0; particleTex[i * particleTexChannels$1 + 3 + emitter.numParticlesPot * particleTexChannels$1 * 2] = a2; const maxNegLife = Math.max(emitter.lifetime, emitter.numParticles * Math.max(emitter.rate, emitter.rate2)); const maxPosLife = emitter.lifetime + 1.0; startSpawnTime = (startSpawnTime + maxNegLife) / (maxNegLife + maxPosLife); const rgba3 = encodeFloatRGBA(startSpawnTime); particleTex[i * particleTexChannels$1 + 0 + emitter.numParticlesPot * particleTexChannels$1 * 3] = rgba3[0]; particleTex[i * particleTexChannels$1 + 1 + emitter.numParticlesPot * particleTexChannels$1 * 3] = rgba3[1]; particleTex[i * particleTexChannels$1 + 2 + emitter.numParticlesPot * particleTexChannels$1 * 3] = rgba3[2]; particleTex[i * particleTexChannels$1 + 3 + emitter.numParticlesPot * particleTexChannels$1 * 3] = rgba3[3]; } else { particleTex[i * particleTexChannels$1] = randomPosTformed.x; particleTex[i * particleTexChannels$1 + 1] = randomPosTformed.y; particleTex[i * particleTexChannels$1 + 2] = randomPosTformed.z; particleTex[i * particleTexChannels$1 + 3] = math.lerp(emitter.startAngle * math.DEG_TO_RAD, emitter.startAngle2 * math.DEG_TO_RAD, rX); particleTex[i * particleTexChannels$1 + 3 + emitter.numParticlesPot * particleTexChannels$1] = startSpawnTime; } } update(data, vbToSort, particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, delta, isOnStop) { let a, b, c; const emitter = this._emitter; if (emitter.meshInstance.node) { const fullMat = emitter.meshInstance.node.worldTransform; for(let j = 0; j < 12; j++){ rotMat.data[j] = fullMat.data[j]; } rotMatInv.copy(rotMat); rotMatInv.invert(); nonUniformScale = emitter.meshInstance.node.localScale; uniformScale = Math.max(Math.max(nonUniformScale.x, nonUniformScale.y), nonUniformScale.z); } emitterPos = emitter.meshInstance.node === null || emitter.localSpace ? Vec3.ZERO : emitter.meshInstance.node.getPosition(); const posCam = emitter.camera ? emitter.camera._node.getPosition() : Vec3.ZERO; const vertSize = !emitter.useMesh ? 15 : 17; let cf, cc; let rotSpeed, rotSpeed2, scale2, alpha, alpha2, radialSpeed, radialSpeed2; const precision1 = emitter.precision - 1; for(let i = 0; i < emitter.numParticles; i++){ const id = Math.floor(emitter.vbCPU[i * emitter.numParticleVerts * (emitter.useMesh ? 6 : 4) + 3]); const rndFactor = particleTex[id * particleTexChannels$1 + 0 + emitter.numParticlesPot * 2 * particleTexChannels$1]; rndFactor3Vec.x = rndFactor; rndFactor3Vec.y = particleTex[id * particleTexChannels$1 + 1 + emitter.numParticlesPot * 2 * particleTexChannels$1]; rndFactor3Vec.z = particleTex[id * particleTexChannels$1 + 2 + emitter.numParticlesPot * 2 * particleTexChannels$1]; const particleRate = emitter.rate + (emitter.rate2 - emitter.rate) * rndFactor; const particleLifetime = emitter.lifetime; let life = particleTex[id * particleTexChannels$1 + 3 + emitter.numParticlesPot * particleTexChannels$1] + delta; const nlife = saturate$1(life / particleLifetime); let scale = 0; let alphaDiv = 0; const angle = 0; const respawn = life - delta <= 0.0 || life >= particleLifetime; if (respawn) { this.calcSpawnPosition(particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, id); } let particleEnabled = life > 0.0 && life < particleLifetime; if (particleEnabled) { c = nlife * precision1; cf = Math.floor(c); cc = Math.ceil(c); c %= 1; a = emitter.qRotSpeed[cf]; b = emitter.qRotSpeed[cc]; rotSpeed = a + (b - a) * c; a = emitter.qRotSpeed2[cf]; b = emitter.qRotSpeed2[cc]; rotSpeed2 = a + (b - a) * c; a = emitter.qScale[cf]; b = emitter.qScale[cc]; scale = a + (b - a) * c; a = emitter.qScale2[cf]; b = emitter.qScale2[cc]; scale2 = a + (b - a) * c; a = emitter.qAlpha[cf]; b = emitter.qAlpha[cc]; alpha = a + (b - a) * c; a = emitter.qAlpha2[cf]; b = emitter.qAlpha2[cc]; alpha2 = a + (b - a) * c; a = emitter.qRadialSpeed[cf]; b = emitter.qRadialSpeed[cc]; radialSpeed = a + (b - a) * c; a = emitter.qRadialSpeed2[cf]; b = emitter.qRadialSpeed2[cc]; radialSpeed2 = a + (b - a) * c; radialSpeed += (radialSpeed2 - radialSpeed) * (rndFactor * 100.0 % 1.0); particlePosPrev.x = particleTex[id * particleTexChannels$1]; particlePosPrev.y = particleTex[id * particleTexChannels$1 + 1]; particlePosPrev.z = particleTex[id * particleTexChannels$1 + 2]; if (!emitter.localSpace) { radialVelocityVec.copy(particlePosPrev).sub(emitterPos); } else { radialVelocityVec.copy(particlePosPrev); } radialVelocityVec.normalize().mulScalar(radialSpeed); cf *= 3; cc *= 3; a = emitter.qLocalVelocity[cf]; b = emitter.qLocalVelocity[cc]; localVelocityVec.x = a + (b - a) * c; a = emitter.qLocalVelocity[cf + 1]; b = emitter.qLocalVelocity[cc + 1]; localVelocityVec.y = a + (b - a) * c; a = emitter.qLocalVelocity[cf + 2]; b = emitter.qLocalVelocity[cc + 2]; localVelocityVec.z = a + (b - a) * c; a = emitter.qLocalVelocity2[cf]; b = emitter.qLocalVelocity2[cc]; localVelocityVec2.x = a + (b - a) * c; a = emitter.qLocalVelocity2[cf + 1]; b = emitter.qLocalVelocity2[cc + 1]; localVelocityVec2.y = a + (b - a) * c; a = emitter.qLocalVelocity2[cf + 2]; b = emitter.qLocalVelocity2[cc + 2]; localVelocityVec2.z = a + (b - a) * c; a = emitter.qVelocity[cf]; b = emitter.qVelocity[cc]; velocityVec.x = a + (b - a) * c; a = emitter.qVelocity[cf + 1]; b = emitter.qVelocity[cc + 1]; velocityVec.y = a + (b - a) * c; a = emitter.qVelocity[cf + 2]; b = emitter.qVelocity[cc + 2]; velocityVec.z = a + (b - a) * c; a = emitter.qVelocity2[cf]; b = emitter.qVelocity2[cc]; velocityVec2.x = a + (b - a) * c; a = emitter.qVelocity2[cf + 1]; b = emitter.qVelocity2[cc + 1]; velocityVec2.y = a + (b - a) * c; a = emitter.qVelocity2[cf + 2]; b = emitter.qVelocity2[cc + 2]; velocityVec2.z = a + (b - a) * c; localVelocityVec.x += (localVelocityVec2.x - localVelocityVec.x) * rndFactor3Vec.x; localVelocityVec.y += (localVelocityVec2.y - localVelocityVec.y) * rndFactor3Vec.y; localVelocityVec.z += (localVelocityVec2.z - localVelocityVec.z) * rndFactor3Vec.z; if (emitter.initialVelocity > 0) { if (emitter.emitterShape === EMITTERSHAPE_SPHERE) { randomPos.copy(rndFactor3Vec).mulScalar(2).sub(Vec3.ONE).normalize(); localVelocityVec.add(randomPos.mulScalar(emitter.initialVelocity)); } else { localVelocityVec.add(Vec3.FORWARD.mulScalar(emitter.initialVelocity)); } } velocityVec.x += (velocityVec2.x - velocityVec.x) * rndFactor3Vec.x; velocityVec.y += (velocityVec2.y - velocityVec.y) * rndFactor3Vec.y; velocityVec.z += (velocityVec2.z - velocityVec.z) * rndFactor3Vec.z; rotSpeed += (rotSpeed2 - rotSpeed) * rndFactor3Vec.y; scale = (scale + (scale2 - scale) * (rndFactor * 10000.0 % 1.0)) * uniformScale; alphaDiv = (alpha2 - alpha) * (rndFactor * 1000.0 % 1.0); if (emitter.meshInstance.node) { if (!emitter.localSpace) { rotMat.transformPoint(localVelocityVec, localVelocityVec); } else { localVelocityVec.x /= nonUniformScale.x; localVelocityVec.y /= nonUniformScale.y; localVelocityVec.z /= nonUniformScale.z; } } if (!emitter.localSpace) { localVelocityVec.add(velocityVec.mul(nonUniformScale)); localVelocityVec.add(radialVelocityVec.mul(nonUniformScale)); } else { rotMatInv.transformPoint(velocityVec, velocityVec); localVelocityVec.add(velocityVec).add(radialVelocityVec); } moveDirVec.copy(localVelocityVec); particlePos.copy(particlePosPrev).add(localVelocityVec.mulScalar(delta)); particleFinalPos.copy(particlePos); particleTex[id * particleTexChannels$1] = particleFinalPos.x; particleTex[id * particleTexChannels$1 + 1] = particleFinalPos.y; particleTex[id * particleTexChannels$1 + 2] = particleFinalPos.z; particleTex[id * particleTexChannels$1 + 3] += rotSpeed * delta; if (emitter.wrap && emitter.wrapBounds) { if (!emitter.localSpace) { particleFinalPos.sub(emitterPos); } particleFinalPos.x = glMod(particleFinalPos.x, emitter.wrapBounds.x) - emitter.wrapBounds.x * 0.5; particleFinalPos.y = glMod(particleFinalPos.y, emitter.wrapBounds.y) - emitter.wrapBounds.y * 0.5; particleFinalPos.z = glMod(particleFinalPos.z, emitter.wrapBounds.z) - emitter.wrapBounds.z * 0.5; if (!emitter.localSpace) { particleFinalPos.add(emitterPos); } } if (emitter.sort > 0) { if (emitter.sort === 1) { tmpVec3$1.copy(particleFinalPos).sub(posCam); emitter.particleDistance[id] = -(tmpVec3$1.x * tmpVec3$1.x + tmpVec3$1.y * tmpVec3$1.y + tmpVec3$1.z * tmpVec3$1.z); } else if (emitter.sort === 2) { emitter.particleDistance[id] = life; } else if (emitter.sort === 3) { emitter.particleDistance[id] = -life; } } } if (isOnStop) { if (life < 0) { particleTex[id * particleTexChannels$1 + 3 + emitter.numParticlesPot * 2 * particleTexChannels$1] = -1; } } else { if (life >= particleLifetime) { life -= Math.max(particleLifetime, emitter.numParticles * particleRate); particleTex[id * particleTexChannels$1 + 3 + emitter.numParticlesPot * 2 * particleTexChannels$1] = emitter.loop ? 1 : -1; } if (life < 0 && emitter.loop) { particleTex[id * particleTexChannels$1 + 3 + emitter.numParticlesPot * 2 * particleTexChannels$1] = 1; } } if (particleTex[id * particleTexChannels$1 + 3 + emitter.numParticlesPot * 2 * particleTexChannels$1] < 0) { particleEnabled = false; } particleTex[id * particleTexChannels$1 + 3 + emitter.numParticlesPot * particleTexChannels$1] = life; for(let v = 0; v < emitter.numParticleVerts; v++){ const vbOffset = (i * emitter.numParticleVerts + v) * (emitter.useMesh ? 6 : 4); let quadX = emitter.vbCPU[vbOffset]; let quadY = emitter.vbCPU[vbOffset + 1]; let quadZ = emitter.vbCPU[vbOffset + 2]; if (!particleEnabled) { quadX = quadY = quadZ = 0; } const w = i * emitter.numParticleVerts * vertSize + v * vertSize; data[w] = particleFinalPos.x; data[w + 1] = particleFinalPos.y; data[w + 2] = particleFinalPos.z; data[w + 3] = nlife; data[w + 4] = emitter.alignToMotion ? angle : particleTex[id * particleTexChannels$1 + 3]; data[w + 5] = scale; data[w + 6] = alphaDiv; data[w + 7] = moveDirVec.x; data[w + 8] = quadX; data[w + 9] = quadY; data[w + 10] = quadZ; data[w + 11] = moveDirVec.y; data[w + 12] = id; data[w + 13] = moveDirVec.z; data[w + 14] = emitter.vbCPU[vbOffset + 3]; if (emitter.useMesh) { data[w + 15] = emitter.vbCPU[vbOffset + 4]; data[w + 16] = emitter.vbCPU[vbOffset + 5]; } } } if (emitter.sort > PARTICLESORT_NONE && emitter.camera) { const vbStride = emitter.useMesh ? 6 : 4; const particleDistance = emitter.particleDistance; for(let i = 0; i < emitter.numParticles; i++){ vbToSort[i][0] = i; vbToSort[i][1] = particleDistance[Math.floor(emitter.vbCPU[i * emitter.numParticleVerts * vbStride + 3])]; } emitter.vbOld.set(emitter.vbCPU); vbToSort.sort((p1, p2)=>{ return p1[1] - p2[1]; }); for(let i = 0; i < emitter.numParticles; i++){ const src = vbToSort[i][0] * emitter.numParticleVerts * vbStride; const dest = i * emitter.numParticleVerts * vbStride; for(let j = 0; j < emitter.numParticleVerts * vbStride; j++){ emitter.vbCPU[dest + j] = emitter.vbOld[src + j]; } } } } constructor(emitter){ this._emitter = emitter; } } const spawnMatrix3 = new Mat3(); const emitterMatrix3 = new Mat3(); const emitterMatrix3Inv = new Mat3(); class ParticleGPUUpdater { _setInputBounds() { this.inBoundsSizeUniform[0] = this._emitter.prevWorldBoundsSize.x; this.inBoundsSizeUniform[1] = this._emitter.prevWorldBoundsSize.y; this.inBoundsSizeUniform[2] = this._emitter.prevWorldBoundsSize.z; this.constantInBoundsSize.setValue(this.inBoundsSizeUniform); this.inBoundsCenterUniform[0] = this._emitter.prevWorldBoundsCenter.x; this.inBoundsCenterUniform[1] = this._emitter.prevWorldBoundsCenter.y; this.inBoundsCenterUniform[2] = this._emitter.prevWorldBoundsCenter.z; this.constantInBoundsCenter.setValue(this.inBoundsCenterUniform); } randomize() { this.frameRandomUniform[0] = Math.random(); this.frameRandomUniform[1] = Math.random(); this.frameRandomUniform[2] = Math.random(); } update(device, spawnMatrix, extentsInnerRatioUniform, delta, isOnStop) { const emitter = this._emitter; device.setBlendState(BlendState.NOBLEND); device.setDepthState(DepthState.NODEPTH); device.setCullMode(CULLFACE_NONE); this.randomize(); this.constantRadialSpeedDivMult.setValue(emitter.material.getParameter('radialSpeedDivMult').data); this.constantGraphSampleSize.setValue(1.0 / emitter.precision); this.constantGraphNumSamples.setValue(emitter.precision); this.constantNumParticles.setValue(emitter.numParticles); this.constantNumParticlesPot.setValue(emitter.numParticlesPot); this.constantInternalTex0.setValue(emitter.internalTex0); this.constantInternalTex1.setValue(emitter.internalTex1); this.constantInternalTex2.setValue(emitter.internalTex2); this.constantInternalTex3.setValue(emitter.internalTex3); const node = emitter.meshInstance.node; const emitterScale = node === null ? Vec3.ONE : node.localScale; if (emitter.pack8) { this.worldBoundsMulUniform[0] = emitter.worldBoundsMul.x; this.worldBoundsMulUniform[1] = emitter.worldBoundsMul.y; this.worldBoundsMulUniform[2] = emitter.worldBoundsMul.z; this.constantOutBoundsMul.setValue(this.worldBoundsMulUniform); this.worldBoundsAddUniform[0] = emitter.worldBoundsAdd.x; this.worldBoundsAddUniform[1] = emitter.worldBoundsAdd.y; this.worldBoundsAddUniform[2] = emitter.worldBoundsAdd.z; this.constantOutBoundsAdd.setValue(this.worldBoundsAddUniform); this._setInputBounds(); let maxVel = emitter.maxVel * Math.max(Math.max(emitterScale.x, emitterScale.y), emitterScale.z); maxVel = Math.max(maxVel, 1); this.constantMaxVel.setValue(maxVel); } const emitterPos = node === null || emitter.localSpace ? Vec3.ZERO : node.getPosition(); const emitterMatrix = node === null ? Mat4.IDENTITY : node.getWorldTransform(); if (emitter.emitterShape === EMITTERSHAPE_BOX) { spawnMatrix3.setFromMat4(spawnMatrix); this.constantSpawnBounds.setValue(spawnMatrix3.data); this.constantSpawnPosInnerRatio.setValue(extentsInnerRatioUniform); } else { this.constantSpawnBoundsSphere.setValue(emitter.emitterRadius); this.constantSpawnBoundsSphereInnerRatio.setValue(emitter.emitterRadius === 0 ? 0 : emitter.emitterRadiusInner / emitter.emitterRadius); } this.constantInitialVelocity.setValue(emitter.initialVelocity); emitterMatrix3.setFromMat4(emitterMatrix); emitterMatrix3Inv.invertMat4(emitterMatrix); this.emitterPosUniform[0] = emitterPos.x; this.emitterPosUniform[1] = emitterPos.y; this.emitterPosUniform[2] = emitterPos.z; this.constantEmitterPos.setValue(this.emitterPosUniform); this.constantFrameRandom.setValue(this.frameRandomUniform); this.constantDelta.setValue(delta); this.constantRate.setValue(emitter.rate); this.constantRateDiv.setValue(emitter.rate2 - emitter.rate); this.constantStartAngle.setValue(emitter.startAngle * math.DEG_TO_RAD); this.constantStartAngle2.setValue(emitter.startAngle2 * math.DEG_TO_RAD); this.constantSeed.setValue(emitter.seed); this.constantLifetime.setValue(emitter.lifetime); this.emitterScaleUniform[0] = emitterScale.x; this.emitterScaleUniform[1] = emitterScale.y; this.emitterScaleUniform[2] = emitterScale.z; this.constantEmitterScale.setValue(this.emitterScaleUniform); this.constantEmitterMatrix.setValue(emitterMatrix3.data); this.constantEmitterMatrixInv.setValue(emitterMatrix3Inv.data); this.constantLocalVelocityDivMult.setValue(emitter.localVelocityUMax); this.constantVelocityDivMult.setValue(emitter.velocityUMax); this.constantRotSpeedDivMult.setValue(emitter.rotSpeedUMax[0]); let texIN = emitter.swapTex ? emitter.particleTexOUT : emitter.particleTexIN; texIN = emitter.beenReset ? emitter.particleTexStart : texIN; const texOUT = emitter.swapTex ? emitter.particleTexIN : emitter.particleTexOUT; this.constantParticleTexIN.setValue(texIN); drawQuadWithShader(device, emitter.swapTex ? emitter.rtParticleTexIN : emitter.rtParticleTexOUT, !isOnStop ? emitter.loop ? emitter.shaderParticleUpdateRespawn : emitter.shaderParticleUpdateNoRespawn : emitter.shaderParticleUpdateOnStop); emitter.material.setParameter('particleTexOUT', texIN); emitter.material.setParameter('particleTexIN', texOUT); emitter.beenReset = false; emitter.swapTex = !emitter.swapTex; emitter.prevWorldBoundsSize.copy(emitter.worldBoundsSize); emitter.prevWorldBoundsCenter.copy(emitter.worldBounds.center); if (emitter.pack8) { this._setInputBounds(); } } constructor(emitter, gd){ this._emitter = emitter; this.frameRandomUniform = new Float32Array(3); this.emitterPosUniform = new Float32Array(3); this.emitterScaleUniform = new Float32Array([ 1, 1, 1 ]); this.worldBoundsMulUniform = new Float32Array(3); this.worldBoundsAddUniform = new Float32Array(3); this.inBoundsSizeUniform = new Float32Array(3); this.inBoundsCenterUniform = new Float32Array(3); this.constantParticleTexIN = gd.scope.resolve('particleTexIN'); this.constantParticleTexOUT = gd.scope.resolve('particleTexOUT'); this.constantEmitterPos = gd.scope.resolve('emitterPos'); this.constantEmitterScale = gd.scope.resolve('emitterScale'); this.constantSpawnBounds = gd.scope.resolve('spawnBounds'); this.constantSpawnPosInnerRatio = gd.scope.resolve('spawnPosInnerRatio'); this.constantSpawnBoundsSphere = gd.scope.resolve('spawnBoundsSphere'); this.constantSpawnBoundsSphereInnerRatio = gd.scope.resolve('spawnBoundsSphereInnerRatio'); this.constantInitialVelocity = gd.scope.resolve('initialVelocity'); this.constantFrameRandom = gd.scope.resolve('frameRandom'); this.constantDelta = gd.scope.resolve('delta'); this.constantRate = gd.scope.resolve('rate'); this.constantRateDiv = gd.scope.resolve('rateDiv'); this.constantLifetime = gd.scope.resolve('lifetime'); this.constantGraphSampleSize = gd.scope.resolve('graphSampleSize'); this.constantGraphNumSamples = gd.scope.resolve('graphNumSamples'); this.constantInternalTex0 = gd.scope.resolve('internalTex0'); this.constantInternalTex1 = gd.scope.resolve('internalTex1'); this.constantInternalTex2 = gd.scope.resolve('internalTex2'); this.constantInternalTex3 = gd.scope.resolve('internalTex3'); this.constantEmitterMatrix = gd.scope.resolve('emitterMatrix'); this.constantEmitterMatrixInv = gd.scope.resolve('emitterMatrixInv'); this.constantNumParticles = gd.scope.resolve('numParticles'); this.constantNumParticlesPot = gd.scope.resolve('numParticlesPot'); this.constantLocalVelocityDivMult = gd.scope.resolve('localVelocityDivMult'); this.constantVelocityDivMult = gd.scope.resolve('velocityDivMult'); this.constantRotSpeedDivMult = gd.scope.resolve('rotSpeedDivMult'); this.constantSeed = gd.scope.resolve('seed'); this.constantStartAngle = gd.scope.resolve('startAngle'); this.constantStartAngle2 = gd.scope.resolve('startAngle2'); this.constantOutBoundsMul = gd.scope.resolve('outBoundsMul'); this.constantOutBoundsAdd = gd.scope.resolve('outBoundsAdd'); this.constantInBoundsSize = gd.scope.resolve('inBoundsSize'); this.constantInBoundsCenter = gd.scope.resolve('inBoundsCenter'); this.constantMaxVel = gd.scope.resolve('maxVel'); this.constantFaceTangent = gd.scope.resolve('faceTangent'); this.constantFaceBinorm = gd.scope.resolve('faceBinorm'); this.constantRadialSpeedDivMult = gd.scope.resolve('radialSpeedDivMult'); } } const normalTypeNames = [ 'NONE', 'VERTEX', 'MAP' ]; class ShaderGeneratorParticle extends ShaderGenerator { generateKey(options) { const definesHash = ShaderGenerator.definesHash(options.defines); let key = `particle_${definesHash}_`; for(const prop in options){ if (options.hasOwnProperty(prop)) { key += options[prop]; } } return key; } createVertexDefines(options, attributes) { const vDefines = new Map(options.defines); if (options.mesh) vDefines.set('USE_MESH', ''); if (options.meshUv) vDefines.set('USE_MESH_UV', ''); if (options.localSpace) vDefines.set('LOCAL_SPACE', ''); if (options.screenSpace) vDefines.set('SCREEN_SPACE', ''); if (options.animTex) vDefines.set('ANIMTEX', ''); if (options.soft > 0) vDefines.set('SOFT', ''); if (options.stretch > 0.0) vDefines.set('STRETCH', ''); if (options.customFace) vDefines.set('CUSTOM_FACE', ''); if (options.pack8) vDefines.set('PACK8', ''); if (options.localSpace) vDefines.set('LOCAL_SPACE', ''); if (options.animTexLoop) vDefines.set('ANIMTEX_LOOP', ''); if (options.wrap) vDefines.set('WRAP', ''); if (options.alignToMotion) vDefines.set('ALIGN_TO_MOTION', ''); vDefines.set('NORMAL', normalTypeNames[options.normal]); attributes.particle_vertexData = SEMANTIC_POSITION; if (options.mesh && options.meshUv) { attributes.particle_uv = SEMANTIC_TEXCOORD0; } if (options.useCpu) { attributes.particle_vertexData2 = SEMANTIC_ATTR1; attributes.particle_vertexData3 = SEMANTIC_ATTR2; attributes.particle_vertexData4 = SEMANTIC_ATTR3; attributes.particle_vertexData5 = SEMANTIC_ATTR4; } return vDefines; } createFragmentDefines(options) { const fDefines = new Map(options.defines); if (options.soft > 0) fDefines.set('SOFT', ''); if (options.halflambert) fDefines.set('HALF_LAMBERT', ''); fDefines.set('NORMAL', normalTypeNames[options.normal]); fDefines.set('BLEND', blendNames[options.blend]); return fDefines; } createShaderDefinition(device, options) { const shaderLanguage = device.isWebGPU ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL; const engineChunks = ShaderChunks.get(device, shaderLanguage); const attributes = {}; const vDefines = this.createVertexDefines(options, attributes); const fDefines = this.createFragmentDefines(options); const executionDefine = `PARTICLE_${options.useCpu ? 'CPU' : 'GPU'}\n`; vDefines.set(executionDefine, ''); fDefines.set(executionDefine, ''); const includes = new Map(engineChunks); return ShaderDefinitionUtils.createDefinition(device, { name: 'ParticleShader', shaderLanguage: shaderLanguage, attributes: attributes, vertexCode: engineChunks.get('particle_shaderVS'), fragmentCode: engineChunks.get('particle_shaderPS'), fragmentDefines: fDefines, fragmentIncludes: includes, vertexIncludes: includes, vertexDefines: vDefines }); } } const particle = new ShaderGeneratorParticle(); class ParticleMaterial extends Material { getShaderVariant(params) { const { device, scene, cameraShaderParams, objDefs } = params; const { emitter } = this; const options = { defines: ShaderUtils.getCoreDefines(this, params), pass: SHADER_FORWARD, useCpu: this.emitter.useCpu, normal: emitter.lighting ? emitter.normalMap !== null ? 2 : 1 : 0, halflambert: this.emitter.halfLambert, stretch: this.emitter.stretch, alignToMotion: this.emitter.alignToMotion, soft: this.emitter.depthSoftening, mesh: this.emitter.useMesh, meshUv: objDefs & SHADERDEF_UV0, gamma: cameraShaderParams?.shaderOutputGamma ?? GAMMA_NONE, toneMap: cameraShaderParams?.toneMapping ?? TONEMAP_LINEAR, fog: scene && !this.emitter.noFog ? scene.fog.type : 'none', wrap: this.emitter.wrap && this.emitter.wrapBounds, localSpace: this.emitter.localSpace, screenSpace: emitter.inTools ? false : this.emitter.screenSpace, blend: this.emitter.blendType, animTex: this.emitter._isAnimated(), animTexLoop: this.emitter.animLoop, pack8: this.emitter.pack8, customFace: this.emitter.orientation !== PARTICLEORIENTATION_SCREEN }; const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); const library = getProgramLibrary(device); library.register('particle', particle); return library.getProgram('particle', options, processingOptions, this.userId); } constructor(emitter){ super(), this.emitter = null; this.emitter = emitter; } } const particleVerts = [ [ -1, -1 ], [ 1, -1 ], [ 1, 1 ], [ -1, 1 ] ]; function _createTexture(device, width, height, pixelData, format = PIXELFORMAT_RGBA32F, mult8Bit, filter) { let mipFilter = FILTER_NEAREST; if (filter && (format === PIXELFORMAT_RGBA8 || format === PIXELFORMAT_SRGBA8)) { mipFilter = FILTER_LINEAR; } const texture = new Texture(device, { width: width, height: height, format: format, cubemap: false, mipmaps: false, minFilter: mipFilter, magFilter: mipFilter, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: 'ParticleSystemTexture' }); const pixels = texture.lock(); if (format === PIXELFORMAT_RGBA8 || format === PIXELFORMAT_SRGBA8) { const temp = new Uint8Array(pixelData.length); for(let i = 0; i < pixelData.length; i++){ temp[i] = pixelData[i] * mult8Bit * 255; } pixelData = temp; } pixels.set(pixelData); texture.unlock(); return texture; } function saturate(x) { return Math.max(Math.min(x, 1), 0); } const default0Curve = new Curve([ 0, 0, 1, 0 ]); const default1Curve = new Curve([ 0, 1, 1, 1 ]); const default0Curve3 = new CurveSet([ 0, 0, 1, 0 ], [ 0, 0, 1, 0 ], [ 0, 0, 1, 0 ]); const default1Curve3 = new CurveSet([ 0, 1, 1, 1 ], [ 0, 1, 1, 1 ], [ 0, 1, 1, 1 ]); let particleTexHeight = 2; const particleTexChannels = 4; const extentsInnerRatioUniform = new Float32Array(3); const spawnMatrix = new Mat4(); const tmpVec3 = new Vec3(); const bMin = new Vec3(); const bMax = new Vec3(); let setPropertyTarget; let setPropertyOptions; function setProperty(pName, defaultVal) { if (setPropertyOptions[pName] !== undefined && setPropertyOptions[pName] !== null) { setPropertyTarget[pName] = setPropertyOptions[pName]; } else { setPropertyTarget[pName] = defaultVal; } } function pack3NFloats(a, b, c) { const packed = a * 255 << 16 | b * 255 << 8 | c * 255; return packed / (1 << 24); } function packTextureXYZ_NXYZ(qXYZ, qXYZ2) { const num = qXYZ.length / 3; const colors = new Array(num * 4); for(let i = 0; i < num; i++){ colors[i * 4] = qXYZ[i * 3]; colors[i * 4 + 1] = qXYZ[i * 3 + 1]; colors[i * 4 + 2] = qXYZ[i * 3 + 2]; colors[i * 4 + 3] = pack3NFloats(qXYZ2[i * 3], qXYZ2[i * 3 + 1], qXYZ2[i * 3 + 2]); } return colors; } function packTextureRGBA(qRGB, qA) { const colors = new Array(qA.length * 4); for(let i = 0; i < qA.length; i++){ colors[i * 4] = qRGB[i * 3]; colors[i * 4 + 1] = qRGB[i * 3 + 1]; colors[i * 4 + 2] = qRGB[i * 3 + 2]; colors[i * 4 + 3] = qA[i]; } return colors; } function packTexture5Floats(qA, qB, qC, qD, qE) { const colors = new Array(qA.length * 4); for(let i = 0; i < qA.length; i++){ colors[i * 4] = qA[i]; colors[i * 4 + 1] = qB[i]; colors[i * 4 + 2] = 0; colors[i * 4 + 3] = pack3NFloats(qC[i], qD[i], qE[i]); } return colors; } function packTexture2Floats(qA, qB) { const colors = new Array(qA.length * 4); for(let i = 0; i < qA.length; i++){ colors[i * 4] = qA[i]; colors[i * 4 + 1] = qB[i]; colors[i * 4 + 2] = 0; colors[i * 4 + 3] = 0; } return colors; } function calcEndTime(emitter) { const interval = Math.max(emitter.rate, emitter.rate2) * emitter.numParticles + emitter.lifetime; return Date.now() + interval * 1000; } function subGraph(A, B) { const r = new Float32Array(A.length); for(let i = 0; i < A.length; i++){ r[i] = A[i] - B[i]; } return r; } function maxUnsignedGraphValue(A, outUMax) { const chans = outUMax.length; const values = A.length / chans; for(let i = 0; i < values; i++){ for(let j = 0; j < chans; j++){ const a = Math.abs(A[i * chans + j]); outUMax[j] = Math.max(outUMax[j], a); } } } function normalizeGraph(A, uMax) { const chans = uMax.length; const values = A.length / chans; for(let i = 0; i < values; i++){ for(let j = 0; j < chans; j++){ A[i * chans + j] /= uMax[j] === 0 ? 1 : uMax[j]; A[i * chans + j] *= 0.5; A[i * chans + j] += 0.5; } } } function divGraphFrom2Curves(curve1, curve2, outUMax) { const sub = subGraph(curve2, curve1); maxUnsignedGraphValue(sub, outUMax); normalizeGraph(sub, outUMax); return sub; } const particleEmitterDeviceCache = new DeviceCache(); class ParticleEmitter { get defaultParamTexture() { return particleEmitterDeviceCache.get(this.graphicsDevice, ()=>{ const resolution = 16; const centerPoint = resolution * 0.5 + 0.5; const dtex = new Float32Array(resolution * resolution * 4); for(let y = 0; y < resolution; y++){ for(let x = 0; x < resolution; x++){ const xgrad = x + 1 - centerPoint; const ygrad = y + 1 - centerPoint; const c = saturate(1 - saturate(Math.sqrt(xgrad * xgrad + ygrad * ygrad) / resolution) - 0.5); const p = y * resolution + x; dtex[p * 4] = 1; dtex[p * 4 + 1] = 1; dtex[p * 4 + 2] = 1; dtex[p * 4 + 3] = c; } } const texture = _createTexture(this.graphicsDevice, resolution, resolution, dtex, PIXELFORMAT_SRGBA8, 1.0, true); texture.minFilter = FILTER_LINEAR; texture.magFilter = FILTER_LINEAR; return texture; }); } onChangeCamera() { this.resetMaterial(); } calculateBoundsMad() { this.worldBoundsMul.x = 1.0 / this.worldBoundsSize.x; this.worldBoundsMul.y = 1.0 / this.worldBoundsSize.y; this.worldBoundsMul.z = 1.0 / this.worldBoundsSize.z; this.worldBoundsAdd.copy(this.worldBounds.center).mul(this.worldBoundsMul).mulScalar(-1); this.worldBoundsAdd.x += 0.5; this.worldBoundsAdd.y += 0.5; this.worldBoundsAdd.z += 0.5; } calculateWorldBounds() { if (!this.node) return; this.prevWorldBoundsSize.copy(this.worldBoundsSize); this.prevWorldBoundsCenter.copy(this.worldBounds.center); if (!this.useCpu) { let recalculateLocalBounds = false; if (this.emitterShape === EMITTERSHAPE_BOX) { recalculateLocalBounds = !this.emitterExtents.equals(this.prevEmitterExtents); } else { recalculateLocalBounds = !(this.emitterRadius === this.prevEmitterRadius); } if (recalculateLocalBounds) { this.calculateLocalBounds(); } } const nodeWT = this.node.getWorldTransform(); if (this.localSpace) { this.worldBoundsNoTrail.copy(this.localBounds); } else { this.worldBoundsNoTrail.setFromTransformedAabb(this.localBounds, nodeWT); } this.worldBoundsTrail[0].add(this.worldBoundsNoTrail); this.worldBoundsTrail[1].add(this.worldBoundsNoTrail); const now = this.simTimeTotal; if (now >= this.timeToSwitchBounds) { this.worldBoundsTrail[0].copy(this.worldBoundsTrail[1]); this.worldBoundsTrail[1].copy(this.worldBoundsNoTrail); this.timeToSwitchBounds = now + this.lifetime; } this.worldBounds.copy(this.worldBoundsTrail[0]); this.worldBoundsSize.copy(this.worldBounds.halfExtents).mulScalar(2); if (this.localSpace) { this.meshInstance.aabb.setFromTransformedAabb(this.worldBounds, nodeWT); this.meshInstance.mesh.aabb.setFromTransformedAabb(this.worldBounds, nodeWT); } else { this.meshInstance.aabb.copy(this.worldBounds); this.meshInstance.mesh.aabb.copy(this.worldBounds); } this.meshInstance._aabbVer = 1 - this.meshInstance._aabbVer; if (this.pack8) this.calculateBoundsMad(); } resetWorldBounds() { if (!this.node) return; this.worldBoundsNoTrail.setFromTransformedAabb(this.localBounds, this.localSpace ? Mat4.IDENTITY : this.node.getWorldTransform()); this.worldBoundsTrail[0].copy(this.worldBoundsNoTrail); this.worldBoundsTrail[1].copy(this.worldBoundsNoTrail); this.worldBounds.copy(this.worldBoundsTrail[0]); this.worldBoundsSize.copy(this.worldBounds.halfExtents).mulScalar(2); this.prevWorldBoundsSize.copy(this.worldBoundsSize); this.prevWorldBoundsCenter.copy(this.worldBounds.center); this.simTimeTotal = 0; this.timeToSwitchBounds = 0; } calculateLocalBounds() { let minx = Number.MAX_VALUE; let miny = Number.MAX_VALUE; let minz = Number.MAX_VALUE; let maxx = -Number.MAX_VALUE; let maxy = -Number.MAX_VALUE; let maxz = -Number.MAX_VALUE; let maxR = 0; let maxScale = 0; const stepWeight = this.lifetime / this.precision; const wVels = [ this.qVelocity, this.qVelocity2 ]; const lVels = [ this.qLocalVelocity, this.qLocalVelocity2 ]; const accumX = [ 0, 0 ]; const accumY = [ 0, 0 ]; const accumZ = [ 0, 0 ]; const accumR = [ 0, 0 ]; const accumW = [ 0, 0 ]; let x, y, z; for(let i = 0; i < this.precision + 1; i++){ const index = Math.min(i, this.precision - 1); for(let j = 0; j < 2; j++){ x = lVels[j][index * 3 + 0] * stepWeight + accumX[j]; y = lVels[j][index * 3 + 1] * stepWeight + accumY[j]; z = lVels[j][index * 3 + 2] * stepWeight + accumZ[j]; minx = Math.min(x, minx); miny = Math.min(y, miny); minz = Math.min(z, minz); maxx = Math.max(x, maxx); maxy = Math.max(y, maxy); maxz = Math.max(z, maxz); accumX[j] = x; accumY[j] = y; accumZ[j] = z; } for(let j = 0; j < 2; j++){ accumW[j] += stepWeight * Math.sqrt(wVels[j][index * 3 + 0] * wVels[j][index * 3 + 0] + wVels[j][index * 3 + 1] * wVels[j][index * 3 + 1] + wVels[j][index * 3 + 2] * wVels[j][index * 3 + 2]); } accumR[0] += this.qRadialSpeed[index] * stepWeight; accumR[1] += this.qRadialSpeed2[index] * stepWeight; maxR = Math.max(maxR, Math.max(Math.abs(accumR[0]), Math.abs(accumR[1]))); maxScale = Math.max(maxScale, this.qScale[index]); } if (this.emitterShape === EMITTERSHAPE_BOX) { x = this.emitterExtents.x * 0.5; y = this.emitterExtents.y * 0.5; z = this.emitterExtents.z * 0.5; } else { x = this.emitterRadius; y = this.emitterRadius; z = this.emitterRadius; } const w = Math.max(accumW[0], accumW[1]); bMin.x = minx - maxScale - x - maxR - w; bMin.y = miny - maxScale - y - maxR - w; bMin.z = minz - maxScale - z - maxR - w; bMax.x = maxx + maxScale + x + maxR + w; bMax.y = maxy + maxScale + y + maxR + w; bMax.z = maxz + maxScale + z + maxR + w; this.localBounds.setMinMax(bMin, bMax); } rebuild() { const gd = this.graphicsDevice; if (this.colorMap === null) this.colorMap = this.defaultParamTexture; this.spawnBounds = this.emitterShape === EMITTERSHAPE_BOX ? this.emitterExtents : this.emitterRadius; this.useCpu = this.useCpu || this.sort > PARTICLESORT_NONE || gd.maxVertexTextures <= 1 || gd.fragmentUniformsCount < 64 || gd.forceCpuParticles; const wasVisible = this._destroyResources(); this.pack8 = (this.pack8 || !gd.textureFloatRenderable) && !this.useCpu; particleTexHeight = this.useCpu || this.pack8 ? 4 : 2; this.useMesh = !!this.mesh; this.numParticlesPot = math.nextPowerOfTwo(this.numParticles); this.rebuildGraphs(); this.calculateLocalBounds(); this.resetWorldBounds(); if (this.node) { this.worldBounds.setFromTransformedAabb(this.localBounds, this.localSpace ? Mat4.IDENTITY : this.node.getWorldTransform()); this.worldBoundsTrail[0].copy(this.worldBounds); this.worldBoundsTrail[1].copy(this.worldBounds); this.worldBoundsSize.copy(this.worldBounds.halfExtents).mulScalar(2); this.prevWorldBoundsSize.copy(this.worldBoundsSize); this.prevWorldBoundsCenter.copy(this.worldBounds.center); if (this.pack8) this.calculateBoundsMad(); } this.vbToSort = new Array(this.numParticles); for(let iSort = 0; iSort < this.numParticles; iSort++)this.vbToSort[iSort] = [ 0, 0 ]; this.particleDistance = new Float32Array(this.numParticles); this._gpuUpdater.randomize(); this.particleTex = new Float32Array(this.numParticlesPot * particleTexHeight * particleTexChannels); const emitterPos = this.node === null || this.localSpace ? Vec3.ZERO : this.node.getPosition(); if (this.emitterShape === EMITTERSHAPE_BOX) { if (this.node === null || this.localSpace) { spawnMatrix.setTRS(Vec3.ZERO, Quat.IDENTITY, this.spawnBounds); } else { spawnMatrix.setTRS(Vec3.ZERO, this.node.getRotation(), tmpVec3.copy(this.spawnBounds).mul(this.node.localScale)); } extentsInnerRatioUniform[0] = this.emitterExtents.x !== 0 ? this.emitterExtentsInner.x / this.emitterExtents.x : 0; extentsInnerRatioUniform[1] = this.emitterExtents.y !== 0 ? this.emitterExtentsInner.y / this.emitterExtents.y : 0; extentsInnerRatioUniform[2] = this.emitterExtents.z !== 0 ? this.emitterExtentsInner.z / this.emitterExtents.z : 0; } for(let i = 0; i < this.numParticles; i++){ this._cpuUpdater.calcSpawnPosition(this.particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, i); if (this.useCpu) this.particleTex[i * particleTexChannels + 3 + this.numParticlesPot * 2 * particleTexChannels] = 1; } this.particleTexStart = new Float32Array(this.numParticlesPot * particleTexHeight * particleTexChannels); for(let i = 0; i < this.particleTexStart.length; i++){ this.particleTexStart[i] = this.particleTex[i]; } if (!this.useCpu) { if (this.pack8) { this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, PIXELFORMAT_RGBA8, 1, false); this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, PIXELFORMAT_RGBA8, 1, false); this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart, PIXELFORMAT_RGBA8, 1, false); } else { this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex); this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex); this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart); } this.rtParticleTexIN = new RenderTarget({ colorBuffer: this.particleTexIN, depth: false }); this.rtParticleTexOUT = new RenderTarget({ colorBuffer: this.particleTexOUT, depth: false }); this.swapTex = false; } const defines = new Map(); if (this.localSpace) defines.set('LOCAL_SPACE', ''); if (this.pack8) defines.set('PACK8', ''); if (this.emitterShape === EMITTERSHAPE_BOX) defines.set('EMITTERSHAPE_BOX', ''); const shaderUniqueId = `Shape:${this.emitterShape}-Pack:${this.pack8}-Local:${this.localSpace}`; const engineChunks = ShaderChunks.get(gd, gd.isWebGPU ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL); const includes = new Map(engineChunks); const shaderOptions = { attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'fullscreenQuadVS', fragmentChunk: 'particle_simulationPS', fragmentDefines: defines, fragmentIncludes: includes }; shaderOptions.uniqueName = `ParticleUpdateRespawn-${shaderUniqueId}`; defines.set('RESPAWN', ''); this.shaderParticleUpdateRespawn = ShaderUtils.createShader(gd, shaderOptions); defines.delete('RESPAWN'); shaderOptions.uniqueName = `ParticleUpdateNoRespawn-${shaderUniqueId}`; defines.set('NO_RESPAWN', ''); this.shaderParticleUpdateNoRespawn = ShaderUtils.createShader(gd, shaderOptions); defines.delete('NO_RESPAWN'); shaderOptions.uniqueName = `ParticleUpdateStop-${shaderUniqueId}`; defines.set('ON_STOP', ''); this.shaderParticleUpdateOnStop = ShaderUtils.createShader(gd, shaderOptions); this.numParticleVerts = this.useMesh ? this.mesh.vertexBuffer.numVertices : 4; this.numParticleIndices = this.useMesh ? this.mesh.indexBuffer[0].numIndices : 6; this._allocate(this.numParticles); const mesh = new Mesh(gd); mesh.vertexBuffer = this.vertexBuffer; mesh.indexBuffer[0] = this.indexBuffer; mesh.primitive[0].type = PRIMITIVE_TRIANGLES; mesh.primitive[0].base = 0; mesh.primitive[0].count = this.numParticles * this.numParticleIndices; mesh.primitive[0].indexed = true; this.material = this._createMaterial(); this.resetMaterial(); this.meshInstance = new MeshInstance(mesh, this.material, this.node); this.meshInstance.pick = false; this.meshInstance.updateKey(); this.meshInstance.cull = true; if (this.localSpace) { this.meshInstance.aabb.setFromTransformedAabb(this.worldBounds, this.node.getWorldTransform()); } else { this.meshInstance.aabb.copy(this.worldBounds); } this.meshInstance._updateAabb = false; this.meshInstance.visible = wasVisible; this._setMaterialTextures(); this.resetTime(); this.addTime(0, false); if (this.preWarm) this.prewarm(this.lifetime); } _isAnimated() { return this.animNumFrames >= 1 && (this.animTilesX > 1 || this.animTilesY > 1) && (this.colorMap && this.colorMap !== this.defaultParamTexture || this.normalMap); } rebuildGraphs() { const precision = this.precision; const gd = this.graphicsDevice; this.qLocalVelocity = this.localVelocityGraph.quantize(precision); this.qVelocity = this.velocityGraph.quantize(precision); this.qColor = this.colorGraph.quantizeClamped(precision, 0, 1); this.qRotSpeed = this.rotationSpeedGraph.quantize(precision); this.qScale = this.scaleGraph.quantize(precision); this.qAlpha = this.alphaGraph.quantize(precision); this.qRadialSpeed = this.radialSpeedGraph.quantize(precision); this.qLocalVelocity2 = this.localVelocityGraph2.quantize(precision); this.qVelocity2 = this.velocityGraph2.quantize(precision); this.qColor2 = this.colorGraph2.quantizeClamped(precision, 0, 1); this.qRotSpeed2 = this.rotationSpeedGraph2.quantize(precision); this.qScale2 = this.scaleGraph2.quantize(precision); this.qAlpha2 = this.alphaGraph2.quantize(precision); this.qRadialSpeed2 = this.radialSpeedGraph2.quantize(precision); for(let i = 0; i < precision; i++){ this.qRotSpeed[i] *= math.DEG_TO_RAD; this.qRotSpeed2[i] *= math.DEG_TO_RAD; } this.localVelocityUMax = new Float32Array(3); this.velocityUMax = new Float32Array(3); this.colorUMax = new Float32Array(3); this.rotSpeedUMax = [ 0 ]; this.scaleUMax = [ 0 ]; this.alphaUMax = [ 0 ]; this.radialSpeedUMax = [ 0 ]; this.qLocalVelocityDiv = divGraphFrom2Curves(this.qLocalVelocity, this.qLocalVelocity2, this.localVelocityUMax); this.qVelocityDiv = divGraphFrom2Curves(this.qVelocity, this.qVelocity2, this.velocityUMax); this.qColorDiv = divGraphFrom2Curves(this.qColor, this.qColor2, this.colorUMax); this.qRotSpeedDiv = divGraphFrom2Curves(this.qRotSpeed, this.qRotSpeed2, this.rotSpeedUMax); this.qScaleDiv = divGraphFrom2Curves(this.qScale, this.qScale2, this.scaleUMax); this.qAlphaDiv = divGraphFrom2Curves(this.qAlpha, this.qAlpha2, this.alphaUMax); this.qRadialSpeedDiv = divGraphFrom2Curves(this.qRadialSpeed, this.qRadialSpeed2, this.radialSpeedUMax); if (this.pack8) { const umax = [ 0, 0, 0 ]; maxUnsignedGraphValue(this.qVelocity, umax); const umax2 = [ 0, 0, 0 ]; maxUnsignedGraphValue(this.qVelocity2, umax2); const lumax = [ 0, 0, 0 ]; maxUnsignedGraphValue(this.qLocalVelocity, lumax); const lumax2 = [ 0, 0, 0 ]; maxUnsignedGraphValue(this.qLocalVelocity2, lumax2); const rumax = [ 0 ]; maxUnsignedGraphValue(this.qRadialSpeed, rumax); const rumax2 = [ 0 ]; maxUnsignedGraphValue(this.qRadialSpeed2, rumax2); let maxVel = Math.max(umax[0], umax2[0]); maxVel = Math.max(maxVel, umax[1]); maxVel = Math.max(maxVel, umax2[1]); maxVel = Math.max(maxVel, umax[2]); maxVel = Math.max(maxVel, umax2[2]); let lmaxVel = Math.max(lumax[0], lumax2[0]); lmaxVel = Math.max(lmaxVel, lumax[1]); lmaxVel = Math.max(lmaxVel, lumax2[1]); lmaxVel = Math.max(lmaxVel, lumax[2]); lmaxVel = Math.max(lmaxVel, lumax2[2]); const maxRad = Math.max(rumax[0], rumax2[0]); this.maxVel = maxVel + lmaxVel + maxRad; } if (!this.useCpu) { this.internalTex0 = _createTexture(gd, precision, 1, packTextureXYZ_NXYZ(this.qLocalVelocity, this.qLocalVelocityDiv)); this.internalTex1 = _createTexture(gd, precision, 1, packTextureXYZ_NXYZ(this.qVelocity, this.qVelocityDiv)); this.internalTex2 = _createTexture(gd, precision, 1, packTexture5Floats(this.qRotSpeed, this.qScale, this.qScaleDiv, this.qRotSpeedDiv, this.qAlphaDiv)); this.internalTex3 = _createTexture(gd, precision, 1, packTexture2Floats(this.qRadialSpeed, this.qRadialSpeedDiv)); } this.colorParam = _createTexture(gd, precision, 1, packTextureRGBA(this.qColor, this.qAlpha), PIXELFORMAT_SRGBA8, 1.0, true); } _setMaterialTextures() { if (this.colorMap) { this.material.setParameter('colorMap', this.colorMap); if (this.lighting && this.normalMap) { this.material.setParameter('normalMap', this.normalMap); } } } _createMaterial() { const material = new ParticleMaterial(this); material.name = `EmitterMaterial:${this.node.name}`; material.cull = CULLFACE_NONE; material.alphaWrite = false; material.blendType = this.blendType; material.depthWrite = this.depthWrite; return material; } resetMaterial() { const material = this.material; material.setParameter('stretch', this.stretch); if (this._isAnimated()) { material.setParameter('animTexTilesParams', this.animTilesParams); material.setParameter('animTexParams', this.animParams); material.setParameter('animTexIndexParams', this.animIndexParams); } material.setParameter('colorMult', this.intensity); if (!this.useCpu) { material.setParameter('internalTex0', this.internalTex0); material.setParameter('internalTex1', this.internalTex1); material.setParameter('internalTex2', this.internalTex2); material.setParameter('internalTex3', this.internalTex3); } material.setParameter('colorParam', this.colorParam); material.setParameter('numParticles', this.numParticles); material.setParameter('numParticlesPot', this.numParticlesPot); material.setParameter('lifetime', this.lifetime); material.setParameter('rate', this.rate); material.setParameter('rateDiv', this.rate2 - this.rate); material.setParameter('seed', this.seed); material.setParameter('scaleDivMult', this.scaleUMax[0]); material.setParameter('alphaDivMult', this.alphaUMax[0]); material.setParameter('radialSpeedDivMult', this.radialSpeedUMax[0]); material.setParameter('graphNumSamples', this.precision); material.setParameter('graphSampleSize', 1.0 / this.precision); material.setParameter('emitterScale', new Float32Array([ 1, 1, 1 ])); if (this.pack8) { this._gpuUpdater._setInputBounds(); material.setParameter('inBoundsSize', this._gpuUpdater.inBoundsSizeUniform); material.setParameter('inBoundsCenter', this._gpuUpdater.inBoundsCenterUniform); material.setParameter('maxVel', this.maxVel); } if (this.wrap && this.wrapBounds) { this.wrapBoundsUniform[0] = this.wrapBounds.x; this.wrapBoundsUniform[1] = this.wrapBounds.y; this.wrapBoundsUniform[2] = this.wrapBounds.z; material.setParameter('wrapBounds', this.wrapBoundsUniform); } this._setMaterialTextures(); if (this.depthSoftening > 0) { material.setParameter('softening', 1.0 / (this.depthSoftening * this.depthSoftening * 100)); } if (this.stretch > 0.0) material.cull = CULLFACE_NONE; this._compParticleFaceParams(); } _compParticleFaceParams() { let tangent, binormal; if (this.orientation === PARTICLEORIENTATION_SCREEN) { tangent = new Float32Array([ 1, 0, 0 ]); binormal = new Float32Array([ 0, 0, 1 ]); } else { let n; if (this.orientation === PARTICLEORIENTATION_WORLD) { n = this.particleNormal.normalize(); } else { const emitterMat = this.node === null ? Mat4.IDENTITY : this.node.getWorldTransform(); n = emitterMat.transformVector(this.particleNormal).normalize(); } const t = new Vec3(1, 0, 0); if (Math.abs(t.dot(n)) === 1) { t.set(0, 0, 1); } const b = new Vec3().cross(n, t).normalize(); t.cross(b, n).normalize(); tangent = new Float32Array([ t.x, t.y, t.z ]); binormal = new Float32Array([ b.x, b.y, b.z ]); } this.material.setParameter('faceTangent', tangent); this.material.setParameter('faceBinorm', binormal); } getVertexInfo() { const elements = []; if (!this.useCpu) { elements.push({ semantic: SEMANTIC_ATTR0, components: 4, type: TYPE_FLOAT32 }); if (this.useMesh) { elements.push({ semantic: SEMANTIC_ATTR1, components: 2, type: TYPE_FLOAT32 }); } } else { elements.push({ semantic: SEMANTIC_ATTR0, components: 4, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR1, components: 4, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR2, components: 4, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR3, components: 1, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_ATTR4, components: this.useMesh ? 4 : 2, type: TYPE_FLOAT32 }); } return elements; } _allocate(numParticles) { const psysVertCount = numParticles * this.numParticleVerts; const psysIndexCount = numParticles * this.numParticleIndices; if (this.vertexBuffer === undefined || this.vertexBuffer.getNumVertices() !== psysVertCount) { const elements = this.getVertexInfo(); const vertexFormat = new VertexFormat(this.graphicsDevice, elements); this.vertexBuffer = new VertexBuffer(this.graphicsDevice, vertexFormat, psysVertCount, { usage: BUFFER_DYNAMIC }); this.indexBuffer = new IndexBuffer(this.graphicsDevice, INDEXFORMAT_UINT32, psysIndexCount); const data = new Float32Array(this.vertexBuffer.lock()); let meshData, stride, texCoordOffset; if (this.useMesh) { meshData = new Float32Array(this.mesh.vertexBuffer.lock()); stride = meshData.length / this.mesh.vertexBuffer.numVertices; for(let elem = 0; elem < this.mesh.vertexBuffer.format.elements.length; elem++){ if (this.mesh.vertexBuffer.format.elements[elem].name === SEMANTIC_TEXCOORD0) { texCoordOffset = this.mesh.vertexBuffer.format.elements[elem].offset / 4; break; } } } for(let i = 0; i < psysVertCount; i++){ const id = Math.floor(i / this.numParticleVerts); if (!this.useMesh) { const vertID = i % 4; data[i * 4] = particleVerts[vertID][0]; data[i * 4 + 1] = particleVerts[vertID][1]; data[i * 4 + 2] = 0; data[i * 4 + 3] = id; } else { const vert = i % this.numParticleVerts; data[i * 6] = meshData[vert * stride]; data[i * 6 + 1] = meshData[vert * stride + 1]; data[i * 6 + 2] = meshData[vert * stride + 2]; data[i * 6 + 3] = id; data[i * 6 + 4] = meshData[vert * stride + texCoordOffset + 0]; data[i * 6 + 5] = 1.0 - meshData[vert * stride + texCoordOffset + 1]; } } if (this.useCpu) { this.vbCPU = new Float32Array(data); this.vbOld = new Float32Array(this.vbCPU.length); } this.vertexBuffer.unlock(); if (this.useMesh) { this.mesh.vertexBuffer.unlock(); } let dst = 0; const indices = new Uint32Array(this.indexBuffer.lock()); if (this.useMesh) { const ib = this.mesh.indexBuffer[0]; meshData = new typedArrayIndexFormats[ib.format](ib.lock()); } for(let i = 0; i < numParticles; i++){ if (!this.useMesh) { const baseIndex = i * 4; indices[dst++] = baseIndex; indices[dst++] = baseIndex + 1; indices[dst++] = baseIndex + 2; indices[dst++] = baseIndex; indices[dst++] = baseIndex + 2; indices[dst++] = baseIndex + 3; } else { for(let j = 0; j < this.numParticleIndices; j++){ indices[i * this.numParticleIndices + j] = meshData[j] + i * this.numParticleVerts; } } } this.indexBuffer.unlock(); if (this.useMesh) this.mesh.indexBuffer[0].unlock(); } } reset() { this.beenReset = true; this.seed = Math.random(); this.material.setParameter('seed', this.seed); if (this.useCpu) { for(let i = 0; i < this.particleTexStart.length; i++){ this.particleTex[i] = this.particleTexStart[i]; } } else { this._setMaterialTextures(); } this.resetWorldBounds(); this.resetTime(); const origLoop = this.loop; this.loop = true; this.addTime(0, false); this.loop = origLoop; if (this.preWarm) { this.prewarm(this.lifetime); } } prewarm(time) { const lifetimeFraction = time / this.lifetime; const iterations = Math.min(Math.floor(lifetimeFraction * this.precision), this.precision); const stepDelta = time / iterations; for(let i = 0; i < iterations; i++){ this.addTime(stepDelta, false); } } resetTime() { this.endTime = calcEndTime(this); } finishFrame() { if (this.useCpu) this.vertexBuffer.unlock(); } addTime(delta, isOnStop) { const device = this.graphicsDevice; this.simTimeTotal += delta; this.calculateWorldBounds(); if (this._isAnimated()) { const tilesParams = this.animTilesParams; tilesParams[0] = 1.0 / this.animTilesX; tilesParams[1] = 1.0 / this.animTilesY; const params = this.animParams; params[0] = this.animStartFrame; params[1] = this.animNumFrames * this.animSpeed; params[2] = this.animNumFrames - 1; params[3] = this.animNumAnimations - 1; const animIndexParams = this.animIndexParams; animIndexParams[0] = this.animIndex; animIndexParams[1] = this.randomizeAnimIndex; } if (this.scene) { if (this.camera !== this.scene._activeCamera) { this.camera = this.scene._activeCamera; this.onChangeCamera(); } } if (this.emitterShape === EMITTERSHAPE_BOX) { extentsInnerRatioUniform[0] = this.emitterExtents.x !== 0 ? this.emitterExtentsInner.x / this.emitterExtents.x : 0; extentsInnerRatioUniform[1] = this.emitterExtents.y !== 0 ? this.emitterExtentsInner.y / this.emitterExtents.y : 0; extentsInnerRatioUniform[2] = this.emitterExtents.z !== 0 ? this.emitterExtentsInner.z / this.emitterExtents.z : 0; if (this.meshInstance.node === null) { spawnMatrix.setTRS(Vec3.ZERO, Quat.IDENTITY, this.emitterExtents); } else { spawnMatrix.setTRS(Vec3.ZERO, this.meshInstance.node.getRotation(), tmpVec3.copy(this.emitterExtents).mul(this.meshInstance.node.localScale)); } } let emitterPos; const emitterScale = this.meshInstance.node === null ? Vec3.ONE : this.meshInstance.node.localScale; this.emitterScaleUniform[0] = emitterScale.x; this.emitterScaleUniform[1] = emitterScale.y; this.emitterScaleUniform[2] = emitterScale.z; this.material.setParameter('emitterScale', this.emitterScaleUniform); if (this.localSpace && this.meshInstance.node) { emitterPos = this.meshInstance.node.getPosition(); this.emitterPosUniform[0] = emitterPos.x; this.emitterPosUniform[1] = emitterPos.y; this.emitterPosUniform[2] = emitterPos.z; this.material.setParameter('emitterPos', this.emitterPosUniform); } this._compParticleFaceParams(); if (!this.useCpu) { this._gpuUpdater.update(device, spawnMatrix, extentsInnerRatioUniform, delta, isOnStop); } else { const data = new Float32Array(this.vertexBuffer.lock()); this._cpuUpdater.update(data, this.vbToSort, this.particleTex, spawnMatrix, extentsInnerRatioUniform, emitterPos, delta, isOnStop); } if (!this.loop) { if (Date.now() > this.endTime) { if (this.onFinished) this.onFinished(); this.meshInstance.visible = false; } } if (this.meshInstance) { this.meshInstance.drawOrder = this.drawOrder; } } _destroyResources() { this.particleTexIN?.destroy(); this.particleTexIN = null; this.particleTexOUT?.destroy(); this.particleTexOUT = null; if (this.particleTexStart && this.particleTexStart.destroy) { this.particleTexStart.destroy(); this.particleTexStart = null; } this.rtParticleTexIN?.destroy(); this.rtParticleTexIN = null; this.rtParticleTexOUT?.destroy(); this.rtParticleTexOUT = null; this.internalTex0?.destroy(); this.internalTex0 = null; this.internalTex1?.destroy(); this.internalTex1 = null; this.internalTex2?.destroy(); this.internalTex2 = null; this.internalTex3?.destroy(); this.internalTex3 = null; this.colorParam?.destroy(); this.colorParam = null; this.vertexBuffer = undefined; this.indexBuffer = undefined; const wasVisible = this.meshInstance?.visible ?? true; this.meshInstance?.destroy(); this.meshInstance = null; this.material?.destroy(); this.material = null; return wasVisible; } destroy() { this.camera = null; this._destroyResources(); } constructor(graphicsDevice, options){ this.material = null; this.internalTex0 = null; this.internalTex1 = null; this.internalTex2 = null; this.colorParam = null; this.graphicsDevice = graphicsDevice; const gd = graphicsDevice; const precision = 32; this.precision = precision; this._addTimeTime = 0; setPropertyTarget = this; setPropertyOptions = options; setProperty('numParticles', 1); if (this.numParticles > graphicsDevice.maxTextureSize) { this.numParticles = graphicsDevice.maxTextureSize; } setProperty('rate', 1); setProperty('rate2', this.rate); setProperty('lifetime', 50); setProperty('emitterExtents', new Vec3(0, 0, 0)); setProperty('emitterExtentsInner', new Vec3(0, 0, 0)); setProperty('emitterRadius', 0); setProperty('emitterRadiusInner', 0); setProperty('emitterShape', EMITTERSHAPE_BOX); setProperty('initialVelocity', 1); setProperty('wrap', false); setProperty('localSpace', false); setProperty('screenSpace', false); setProperty('wrapBounds', null); setProperty('colorMap', this.defaultParamTexture); setProperty('normalMap', null); setProperty('loop', true); setProperty('preWarm', false); setProperty('sort', PARTICLESORT_NONE); setProperty('mode', PARTICLEMODE_GPU); setProperty('scene', null); setProperty('lighting', false); setProperty('halfLambert', false); setProperty('intensity', 1.0); setProperty('stretch', 0.0); setProperty('alignToMotion', false); setProperty('depthSoftening', 0); setProperty('mesh', null); setProperty('particleNormal', new Vec3(0, 1, 0)); setProperty('orientation', PARTICLEORIENTATION_SCREEN); setProperty('depthWrite', false); setProperty('noFog', false); setProperty('blendType', BLEND_NORMAL); setProperty('node', null); setProperty('startAngle', 0); setProperty('startAngle2', this.startAngle); setProperty('animTilesX', 1); setProperty('animTilesY', 1); setProperty('animStartFrame', 0); setProperty('animNumFrames', 1); setProperty('animNumAnimations', 1); setProperty('animIndex', 0); setProperty('randomizeAnimIndex', false); setProperty('animSpeed', 1); setProperty('animLoop', true); this._gpuUpdater = new ParticleGPUUpdater(this, gd); this._cpuUpdater = new ParticleCPUUpdater(this); this.emitterPosUniform = new Float32Array(3); this.wrapBoundsUniform = new Float32Array(3); this.emitterScaleUniform = new Float32Array([ 1, 1, 1 ]); setProperty('colorGraph', default1Curve3); setProperty('colorGraph2', this.colorGraph); setProperty('scaleGraph', default1Curve); setProperty('scaleGraph2', this.scaleGraph); setProperty('alphaGraph', default1Curve); setProperty('alphaGraph2', this.alphaGraph); setProperty('localVelocityGraph', default0Curve3); setProperty('localVelocityGraph2', this.localVelocityGraph); setProperty('velocityGraph', default0Curve3); setProperty('velocityGraph2', this.velocityGraph); setProperty('rotationSpeedGraph', default0Curve); setProperty('rotationSpeedGraph2', this.rotationSpeedGraph); setProperty('radialSpeedGraph', default0Curve); setProperty('radialSpeedGraph2', this.radialSpeedGraph); this.animTilesParams = new Float32Array(2); this.animParams = new Float32Array(4); this.animIndexParams = new Float32Array(2); this.vbToSort = null; this.vbOld = null; this.particleDistance = null; this.camera = null; this.swapTex = false; this.useMesh = true; this.useCpu = !graphicsDevice.supportsGpuParticles; this.pack8 = true; this.localBounds = new BoundingBox(); this.worldBoundsNoTrail = new BoundingBox(); this.worldBoundsTrail = [ new BoundingBox(), new BoundingBox() ]; this.worldBounds = new BoundingBox(); this.worldBoundsSize = new Vec3(); this.prevWorldBoundsSize = new Vec3(); this.prevWorldBoundsCenter = new Vec3(); this.prevEmitterExtents = this.emitterExtents; this.prevEmitterRadius = this.emitterRadius; this.worldBoundsMul = new Vec3(); this.worldBoundsAdd = new Vec3(); this.timeToSwitchBounds = 0; this.shaderParticleUpdateRespawn = null; this.shaderParticleUpdateNoRespawn = null; this.shaderParticleUpdateOnStop = null; this.numParticleVerts = 0; this.numParticleIndices = 0; this.material = null; this.meshInstance = null; this.drawOrder = 0; this.seed = Math.random(); this.fixedTimeStep = 1.0 / 60; this.maxSubSteps = 10; this.simTime = 0; this.simTimeTotal = 0; this.beenReset = false; this._layer = null; this.rebuild(); } } class ShaderGeneratorShader extends ShaderGenerator { generateKey(options) { const desc = options.shaderDesc; const vsHashGLSL = desc.vertexGLSL ? hashCode(desc.vertexGLSL) : 0; const fsHashGLSL = desc.fragmentGLSL ? hashCode(desc.fragmentGLSL) : 0; const vsHashWGSL = desc.vertexWGSL ? hashCode(desc.vertexWGSL) : 0; const fsHashWGSL = desc.fragmentWGSL ? hashCode(desc.fragmentWGSL) : 0; const definesHash = ShaderGenerator.definesHash(options.defines); const chunksKey = options.shaderChunks?.key ?? ''; let key = `${desc.uniqueName}_${definesHash}_${vsHashGLSL}_${fsHashGLSL}_${vsHashWGSL}_${fsHashWGSL}_${chunksKey}`; if (options.skin) key += '_skin'; if (options.useInstancing) key += '_inst'; if (options.useMorphPosition) key += '_morphp'; if (options.useMorphNormal) key += '_morphn'; if (options.useMorphTextureBasedInt) key += '_morphi'; return key; } createAttributesDefinition(definitionOptions, options) { const srcAttributes = options.shaderDesc.attributes; const attributes = srcAttributes ? { ...srcAttributes } : undefined; if (options.skin) { attributes.vertex_boneWeights = SEMANTIC_BLENDWEIGHT; attributes.vertex_boneIndices = SEMANTIC_BLENDINDICES; } if (options.useMorphPosition || options.useMorphNormal) { attributes.morph_vertex_id = SEMANTIC_ATTR15; } definitionOptions.attributes = attributes; } createVertexDefinition(definitionOptions, options, sharedIncludes, wgsl) { const desc = options.shaderDesc; const includes = new Map(sharedIncludes); includes.set('transformInstancingVS', ''); const defines = new Map(options.defines); if (options.skin) defines.set('SKIN', true); if (options.useInstancing) defines.set('INSTANCING', true); if (options.useMorphPosition || options.useMorphNormal) { defines.set('MORPHING', true); if (options.useMorphTextureBasedInt) defines.set('MORPHING_INT', true); if (options.useMorphPosition) defines.set('MORPHING_POSITION', true); if (options.useMorphNormal) defines.set('MORPHING_NORMAL', true); } definitionOptions.vertexCode = wgsl ? desc.vertexWGSL : desc.vertexGLSL; definitionOptions.vertexIncludes = includes; definitionOptions.vertexDefines = defines; } createFragmentDefinition(definitionOptions, options, sharedIncludes, wgsl) { const desc = options.shaderDesc; const includes = new Map(sharedIncludes); const defines = new Map(options.defines); definitionOptions.fragmentCode = wgsl ? desc.fragmentWGSL : desc.fragmentGLSL; definitionOptions.fragmentIncludes = includes; definitionOptions.fragmentDefines = defines; } createShaderDefinition(device, options) { const desc = options.shaderDesc; const wgsl = device.isWebGPU && !!desc.vertexWGSL && !!desc.fragmentWGSL && (options.shaderChunks?.useWGSL ?? true); const definitionOptions = { name: `ShaderMaterial-${desc.uniqueName}`, shaderLanguage: wgsl ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL, fragmentOutputTypes: desc.fragmentOutputTypes, meshUniformBufferFormat: desc.meshUniformBufferFormat, meshBindGroupFormat: desc.meshBindGroupFormat }; const shaderLanguage = wgsl ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL; const sharedIncludes = MapUtils.merge(ShaderChunks.get(device, shaderLanguage), options.shaderChunks[shaderLanguage]); this.createAttributesDefinition(definitionOptions, options); this.createVertexDefinition(definitionOptions, options, sharedIncludes, wgsl); this.createFragmentDefinition(definitionOptions, options, sharedIncludes, wgsl); return ShaderDefinitionUtils.createDefinition(device, definitionOptions); } } const shaderGeneratorShader = new ShaderGeneratorShader(); class ShaderMaterial extends Material { set shaderDesc(value) { this._shaderDesc = undefined; if (value) { this._shaderDesc = { uniqueName: value.uniqueName, attributes: value.attributes, fragmentOutputTypes: value.fragmentOutputTypes, vertexGLSL: value.vertexGLSL, fragmentGLSL: value.fragmentGLSL, vertexWGSL: value.vertexWGSL, fragmentWGSL: value.fragmentWGSL }; if (value.vertexCode || value.fragmentCode || value.shaderLanguage) { const language = value.shaderLanguage ?? SHADERLANGUAGE_GLSL; if (language === SHADERLANGUAGE_GLSL) { this._shaderDesc.vertexGLSL = value.vertexCode; this._shaderDesc.fragmentGLSL = value.fragmentCode; } else if (language === SHADERLANGUAGE_WGSL) { this._shaderDesc.vertexWGSL = value.vertexCode; this._shaderDesc.fragmentWGSL = value.fragmentCode; } } } this.clearVariants(); } get shaderDesc() { return this._shaderDesc; } copy(source) { super.copy(source); this.shaderDesc = source.shaderDesc; return this; } getShaderVariant(params) { const { objDefs } = params; const options = { defines: ShaderUtils.getCoreDefines(this, params), skin: (objDefs & SHADERDEF_SKIN) !== 0, useInstancing: (objDefs & SHADERDEF_INSTANCING) !== 0, useMorphPosition: (objDefs & SHADERDEF_MORPH_POSITION) !== 0, useMorphNormal: (objDefs & SHADERDEF_MORPH_NORMAL) !== 0, useMorphTextureBasedInt: (objDefs & SHADERDEF_MORPH_TEXTURE_BASED_INT) !== 0, pass: params.pass, gamma: params.cameraShaderParams.shaderOutputGamma, toneMapping: params.cameraShaderParams.toneMapping, fog: params.cameraShaderParams.fog, shaderDesc: this.shaderDesc, shaderChunks: this.shaderChunks }; const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); const library = getProgramLibrary(params.device); library.register('shader-material', shaderGeneratorShader); return library.getProgram('shader-material', options, processingOptions, this.userId); } constructor(shaderDesc){ super(); this.shaderDesc = shaderDesc; } } class GSplatParams { set colorizeLod(value) { if (this._colorizeLod !== value) { this._colorizeLod = value; this.dirty = true; } } get colorizeLod() { return this._colorizeLod; } set lodBehindPenalty(value) { if (this._lodBehindPenalty !== value) { this._lodBehindPenalty = value; this.dirty = true; } } get lodBehindPenalty() { return this._lodBehindPenalty; } set lodRangeMin(value) { if (this._lodRangeMin !== value) { this._lodRangeMin = value; this.dirty = true; } } get lodRangeMin() { return this._lodRangeMin; } set lodRangeMax(value) { if (this._lodRangeMax !== value) { this._lodRangeMax = value; this.dirty = true; } } get lodRangeMax() { return this._lodRangeMax; } set lodUnderfillLimit(value) { if (this._lodUnderfillLimit !== value) { this._lodUnderfillLimit = value; this.dirty = true; } } get lodUnderfillLimit() { return this._lodUnderfillLimit; } set splatBudget(value) {} get splatBudget() { return 0; } set colorRamp(value) { if (this._colorRamp !== value) { this._colorRamp = value; this.dirty = true; } } get colorRamp() { return this._colorRamp; } get material() { return this._material; } frameEnd() { this._material.dirty = false; this.dirty = false; } constructor(){ this._material = new ShaderMaterial(); this.debugAabbs = false; this.radialSorting = false; this.gpuSorting = false; this.debugNodeAabbs = false; this.dirty = false; this._colorizeLod = false; this.lodUpdateDistance = 1; this.lodUpdateAngle = 0; this._lodBehindPenalty = 1; this._lodRangeMin = 0; this._lodRangeMax = 10; this._lodUnderfillLimit = 0; this._colorRamp = null; this.colorRampIntensity = 1; this.colorizeColorUpdate = false; this.colorUpdateDistance = 0.2; this.colorUpdateAngle = 2; this.colorUpdateDistanceLodScale = 2; this.colorUpdateAngleLodScale = 2; this.cooldownTicks = 100; } } const decodeTable = { 'linear': 'decodeLinear', 'srgb': 'decodeGamma', 'rgbm': 'decodeRGBM', 'rgbe': 'decodeRGBE', 'rgbp': 'decodeRGBP', 'xy': 'unpackNormalXY', 'xyz': 'unpackNormalXYZ' }; const encodeTable = { 'linear': 'encodeLinear', 'srgb': 'encodeGamma', 'rgbm': 'encodeRGBM', 'rgbe': 'encodeRGBE', 'rgbp': 'encodeRGBP' }; class ChunkUtils { static decodeFunc(encoding) { return decodeTable[encoding] ?? 'decodeGamma'; } static encodeFunc(encoding) { return encodeTable[encoding] ?? 'encodeGamma'; } } const calculateNormals = (positions, indices)=>{ const triangleCount = indices.length / 3; const vertexCount = positions.length / 3; const p1 = new Vec3(); const p2 = new Vec3(); const p3 = new Vec3(); const p1p2 = new Vec3(); const p1p3 = new Vec3(); const faceNormal = new Vec3(); const normals = []; for(let i = 0; i < positions.length; i++){ normals[i] = 0; } for(let i = 0; i < triangleCount; i++){ const i1 = indices[i * 3]; const i2 = indices[i * 3 + 1]; const i3 = indices[i * 3 + 2]; p1.set(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]); p2.set(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]); p3.set(positions[i3 * 3], positions[i3 * 3 + 1], positions[i3 * 3 + 2]); p1p2.sub2(p2, p1); p1p3.sub2(p3, p1); faceNormal.cross(p1p2, p1p3).normalize(); normals[i1 * 3] += faceNormal.x; normals[i1 * 3 + 1] += faceNormal.y; normals[i1 * 3 + 2] += faceNormal.z; normals[i2 * 3] += faceNormal.x; normals[i2 * 3 + 1] += faceNormal.y; normals[i2 * 3 + 2] += faceNormal.z; normals[i3 * 3] += faceNormal.x; normals[i3 * 3 + 1] += faceNormal.y; normals[i3 * 3 + 2] += faceNormal.z; } for(let i = 0; i < vertexCount; i++){ const nx = normals[i * 3]; const ny = normals[i * 3 + 1]; const nz = normals[i * 3 + 2]; const invLen = 1 / Math.sqrt(nx * nx + ny * ny + nz * nz); normals[i * 3] *= invLen; normals[i * 3 + 1] *= invLen; normals[i * 3 + 2] *= invLen; } return normals; }; const calculateTangents = (positions, normals, uvs, indices)=>{ const triangleCount = indices.length / 3; const vertexCount = positions.length / 3; const v1 = new Vec3(); const v2 = new Vec3(); const v3 = new Vec3(); const w1 = new Vec2(); const w2 = new Vec2(); const w3 = new Vec2(); const sdir = new Vec3(); const tdir = new Vec3(); const tan1 = new Float32Array(vertexCount * 3); const tan2 = new Float32Array(vertexCount * 3); const tangents = []; for(let i = 0; i < triangleCount; i++){ const i1 = indices[i * 3]; const i2 = indices[i * 3 + 1]; const i3 = indices[i * 3 + 2]; v1.set(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]); v2.set(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]); v3.set(positions[i3 * 3], positions[i3 * 3 + 1], positions[i3 * 3 + 2]); w1.set(uvs[i1 * 2], uvs[i1 * 2 + 1]); w2.set(uvs[i2 * 2], uvs[i2 * 2 + 1]); w3.set(uvs[i3 * 2], uvs[i3 * 2 + 1]); const x1 = v2.x - v1.x; const x2 = v3.x - v1.x; const y1 = v2.y - v1.y; const y2 = v3.y - v1.y; const z1 = v2.z - v1.z; const z2 = v3.z - v1.z; const s1 = w2.x - w1.x; const s2 = w3.x - w1.x; const t1 = w2.y - w1.y; const t2 = w3.y - w1.y; const area = s1 * t2 - s2 * t1; if (area === 0) { sdir.set(0, 1, 0); tdir.set(1, 0, 0); } else { const r = 1 / area; sdir.set((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); tdir.set((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); } tan1[i1 * 3 + 0] += sdir.x; tan1[i1 * 3 + 1] += sdir.y; tan1[i1 * 3 + 2] += sdir.z; tan1[i2 * 3 + 0] += sdir.x; tan1[i2 * 3 + 1] += sdir.y; tan1[i2 * 3 + 2] += sdir.z; tan1[i3 * 3 + 0] += sdir.x; tan1[i3 * 3 + 1] += sdir.y; tan1[i3 * 3 + 2] += sdir.z; tan2[i1 * 3 + 0] += tdir.x; tan2[i1 * 3 + 1] += tdir.y; tan2[i1 * 3 + 2] += tdir.z; tan2[i2 * 3 + 0] += tdir.x; tan2[i2 * 3 + 1] += tdir.y; tan2[i2 * 3 + 2] += tdir.z; tan2[i3 * 3 + 0] += tdir.x; tan2[i3 * 3 + 1] += tdir.y; tan2[i3 * 3 + 2] += tdir.z; } const t1 = new Vec3(); const t2 = new Vec3(); const n = new Vec3(); const temp = new Vec3(); for(let i = 0; i < vertexCount; i++){ n.set(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]); t1.set(tan1[i * 3], tan1[i * 3 + 1], tan1[i * 3 + 2]); t2.set(tan2[i * 3], tan2[i * 3 + 1], tan2[i * 3 + 2]); const ndott = n.dot(t1); temp.copy(n).mulScalar(ndott); temp.sub2(t1, temp).normalize(); tangents[i * 4] = temp.x; tangents[i * 4 + 1] = temp.y; tangents[i * 4 + 2] = temp.z; temp.cross(n, t1); tangents[i * 4 + 3] = temp.dot(t2) < 0.0 ? -1 : 1.0; } return tangents; }; class Geometry { calculateNormals() { this.normals = calculateNormals(this.positions, this.indices); } calculateTangents() { this.tangents = calculateTangents(this.positions, this.normals, this.uvs, this.indices); } } const primitiveUv1Padding$1 = 8.0 / 64; const primitiveUv1PaddingScale$1 = 1.0 - primitiveUv1Padding$1 * 2; class BoxGeometry extends Geometry { constructor(opts = {}){ super(); const he = opts.halfExtents ?? new Vec3(0.5, 0.5, 0.5); const ws = opts.widthSegments ?? 1; const ls = opts.lengthSegments ?? 1; const hs = opts.heightSegments ?? 1; const yOffset = opts.yOffset ?? 0; const minY = -he.y + yOffset; const maxY = he.y + yOffset; const corners = [ new Vec3(-he.x, minY, he.z), new Vec3(he.x, minY, he.z), new Vec3(he.x, maxY, he.z), new Vec3(-he.x, maxY, he.z), new Vec3(he.x, minY, -he.z), new Vec3(-he.x, minY, -he.z), new Vec3(-he.x, maxY, -he.z), new Vec3(he.x, maxY, -he.z) ]; const faceAxes = [ [ 0, 1, 3 ], [ 4, 5, 7 ], [ 3, 2, 6 ], [ 1, 0, 4 ], [ 1, 4, 2 ], [ 5, 0, 6 ] ]; const faceNormals = [ [ 0, 0, 1 ], [ 0, 0, -1 ], [ 0, 1, 0 ], [ 0, -1, 0 ], [ 1, 0, 0 ], [ -1, 0, 0 ] ]; const sides = { FRONT: 0, BACK: 1, TOP: 2, BOTTOM: 3, RIGHT: 4, LEFT: 5 }; const positions = []; const normals = []; const uvs = []; const uvs1 = []; const indices = []; let vcounter = 0; const generateFace = (side, uSegments, vSegments)=>{ const temp1 = new Vec3(); const temp2 = new Vec3(); const temp3 = new Vec3(); const r = new Vec3(); for(let i = 0; i <= uSegments; i++){ for(let j = 0; j <= vSegments; j++){ temp1.lerp(corners[faceAxes[side][0]], corners[faceAxes[side][1]], i / uSegments); temp2.lerp(corners[faceAxes[side][0]], corners[faceAxes[side][2]], j / vSegments); temp3.sub2(temp2, corners[faceAxes[side][0]]); r.add2(temp1, temp3); let u = i / uSegments; let v = j / vSegments; positions.push(r.x, r.y, r.z); normals.push(faceNormals[side][0], faceNormals[side][1], faceNormals[side][2]); uvs.push(u, 1 - v); u = u * primitiveUv1PaddingScale$1 + primitiveUv1Padding$1; v = v * primitiveUv1PaddingScale$1 + primitiveUv1Padding$1; u /= 3; v /= 3; u += side % 3 / 3; v += Math.floor(side / 3) / 3; uvs1.push(u, 1 - v); if (i < uSegments && j < vSegments) { indices.push(vcounter + vSegments + 1, vcounter + 1, vcounter); indices.push(vcounter + vSegments + 1, vcounter + vSegments + 2, vcounter + 1); } vcounter++; } } }; generateFace(sides.FRONT, ws, hs); generateFace(sides.BACK, ws, hs); generateFace(sides.TOP, ws, ls); generateFace(sides.BOTTOM, ws, ls); generateFace(sides.RIGHT, ls, hs); generateFace(sides.LEFT, ls, hs); this.positions = positions; this.normals = normals; this.uvs = uvs; this.uvs1 = uvs1; this.indices = indices; if (opts.calculateTangents) { this.tangents = calculateTangents(positions, normals, uvs, indices); } } } class SphereGeometry extends Geometry { constructor(opts = {}){ super(); const radius = opts.radius ?? 0.5; const latitudeBands = opts.latitudeBands ?? 16; const longitudeBands = opts.longitudeBands ?? 16; const positions = []; const normals = []; const uvs = []; const indices = []; for(let lat = 0; lat <= latitudeBands; lat++){ const theta = lat * Math.PI / latitudeBands; const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); for(let lon = 0; lon <= longitudeBands; lon++){ const phi = lon * 2 * Math.PI / longitudeBands - Math.PI / 2; const sinPhi = Math.sin(phi); const cosPhi = Math.cos(phi); const x = cosPhi * sinTheta; const y = cosTheta; const z = sinPhi * sinTheta; const u = 1 - lon / longitudeBands; const v = 1 - lat / latitudeBands; positions.push(x * radius, y * radius, z * radius); normals.push(x, y, z); uvs.push(u, 1 - v); } } for(let lat = 0; lat < latitudeBands; ++lat){ for(let lon = 0; lon < longitudeBands; ++lon){ const first = lat * (longitudeBands + 1) + lon; const second = first + longitudeBands + 1; indices.push(first + 1, second, first); indices.push(first + 1, second + 1, second); } } this.positions = positions; this.normals = normals; this.uvs = uvs; this.uvs1 = uvs; this.indices = indices; if (opts.calculateTangents) { this.tangents = calculateTangents(positions, normals, uvs, indices); } } } class DomeGeometry extends SphereGeometry { constructor(opts = {}){ const radius = 0.5; const latitudeBands = opts.latitudeBands ?? 16; const longitudeBands = opts.longitudeBands ?? 16; super({ radius, latitudeBands, longitudeBands }); const bottomLimit = 0.1; const curvatureRadius = 0.95; const curvatureRadiusSq = curvatureRadius * curvatureRadius; const positions = this.positions; for(let i = 0; i < positions.length; i += 3){ const x = positions[i] / radius; let y = positions[i + 1] / radius; const z = positions[i + 2] / radius; if (y < 0) { y *= 0.3; if (x * x + z * z < curvatureRadiusSq) { y = -bottomLimit; } } y += bottomLimit; y *= radius; positions[i + 1] = y; } } } class SkyGeometry { static create(device, type) { switch(type){ case SKYTYPE_BOX: return SkyGeometry.box(device); case SKYTYPE_DOME: return SkyGeometry.dome(device); } return SkyGeometry.infinite(device); } static infinite(device) { return Mesh.fromGeometry(device, new BoxGeometry(device)); } static box(device) { return Mesh.fromGeometry(device, new BoxGeometry({ yOffset: 0.5 })); } static dome(device) { const geom = new DomeGeometry({ latitudeBands: 50, longitudeBands: 50 }); geom.normals = undefined; geom.uvs = undefined; return Mesh.fromGeometry(device, geom); } } class SkyMesh { destroy() { if (this.meshInstance) { if (this.skyLayer) { this.skyLayer.removeMeshInstances([ this.meshInstance ]); } this.meshInstance.destroy(); this.meshInstance = null; } } set depthWrite(value) { this._depthWrite = value; if (this.meshInstance) { this.meshInstance.material.depthWrite = value; } } get depthWrite() { return this._depthWrite; } constructor(device, scene, node, texture, type){ this.meshInstance = null; this._depthWrite = false; const material = new ShaderMaterial({ uniqueName: 'SkyMaterial', vertexGLSL: ShaderChunks.get(device, SHADERLANGUAGE_GLSL).get('skyboxVS'), fragmentGLSL: ShaderChunks.get(device, SHADERLANGUAGE_GLSL).get('skyboxPS'), vertexWGSL: ShaderChunks.get(device, SHADERLANGUAGE_WGSL).get('skyboxVS'), fragmentWGSL: ShaderChunks.get(device, SHADERLANGUAGE_WGSL).get('skyboxPS'), attributes: { aPosition: SEMANTIC_POSITION } }); material.setDefine('{SKYBOX_DECODE_FNC}', ChunkUtils.decodeFunc(texture.encoding)); if (type !== SKYTYPE_INFINITE) material.setDefine('SKYMESH', ''); if (texture.cubemap) material.setDefine('SKY_CUBEMAP', ''); material.setParameter('skyboxHighlightMultiplier', scene.skyboxHighlightMultiplier); if (texture.cubemap) { material.setParameter('texture_cubeMap', texture); } else { material.setParameter('texture_envAtlas', texture); material.setParameter('mipLevel', scene.skyboxMip); } material.cull = CULLFACE_FRONT; material.depthWrite = this._depthWrite; const skyLayer = scene.layers.getLayerById(LAYERID_SKYBOX); if (skyLayer) { const mesh = SkyGeometry.create(device, type); const meshInstance = new MeshInstance(mesh, material, node); this.meshInstance = meshInstance; meshInstance.cull = false; meshInstance.pick = false; skyLayer.addMeshInstances([ meshInstance ]); this.skyLayer = skyLayer; } } } class Sky { applySettings(render) { this.type = render.skyType ?? SKYTYPE_INFINITE; this.node.setLocalPosition(new Vec3(render.skyMeshPosition ?? [ 0, 0, 0 ])); this.node.setLocalEulerAngles(new Vec3(render.skyMeshRotation ?? [ 0, 0, 0 ])); this.node.setLocalScale(new Vec3(render.skyMeshScale ?? [ 1, 1, 1 ])); if (render.skyCenter) { this._center = new Vec3(render.skyCenter); } } set type(value) { if (this._type !== value) { this._type = value; this.scene.updateShaders = true; this.updateSkyMesh(); } } get type() { return this._type; } set center(value) { this._center.copy(value); } get center() { return this._center; } set depthWrite(value) { if (this._depthWrite !== value) { this._depthWrite = value; if (this.skyMesh) { this.skyMesh.depthWrite = value; } } } get depthWrite() { return this._depthWrite; } updateSkyMesh() { const texture = this.scene._getSkyboxTex(); if (texture) { this.resetSkyMesh(); this.skyMesh = new SkyMesh(this.device, this.scene, this.node, texture, this.type); this.skyMesh.depthWrite = this._depthWrite; this.scene.fire('set:skybox', texture); } } resetSkyMesh() { this.skyMesh?.destroy(); this.skyMesh = null; } update() { if (this.type !== SKYTYPE_INFINITE) { const { center, centerArray } = this; const temp = new Vec3(); this.node.getWorldTransform().transformPoint(center, temp); centerArray[0] = temp.x; centerArray[1] = temp.y; centerArray[2] = temp.z; this.projectedSkydomeCenterId.setValue(centerArray); } } constructor(scene){ this._type = SKYTYPE_INFINITE; this._center = new Vec3(0, 1, 0); this.skyMesh = null; this._depthWrite = false; this.node = new GraphNode('SkyMeshNode'); this.device = scene.device; this.scene = scene; this.center = new Vec3(0, 1, 0); this.centerArray = new Float32Array(3); this.projectedSkydomeCenterId = this.device.scope.resolve('projectedSkydomeCenter'); } } const identityGraphNode = new GraphNode(); identityGraphNode.worldTransform = Mat4.IDENTITY; identityGraphNode._dirtyWorld = identityGraphNode._dirtyNormal = false; class ImmediateBatch { addLines(positions, color) { const destPos = this.positions; const count = positions.length; for(let i = 0; i < count; i++){ const pos = positions[i]; destPos.push(pos.x, pos.y, pos.z); } const destCol = this.colors; if (color.length) { for(let i = 0; i < count; i++){ const col = color[i]; destCol.push(col.r, col.g, col.b, col.a); } } else { for(let i = 0; i < count; i++){ destCol.push(color.r, color.g, color.b, color.a); } } } addLinesArrays(positions, color) { const destPos = this.positions; for(let i = 0; i < positions.length; i += 3){ destPos.push(positions[i], positions[i + 1], positions[i + 2]); } const destCol = this.colors; if (color.length) { for(let i = 0; i < color.length; i += 4){ destCol.push(color[i], color[i + 1], color[i + 2], color[i + 3]); } } else { const count = positions.length / 3; for(let i = 0; i < count; i++){ destCol.push(color.r, color.g, color.b, color.a); } } } onPreRender(visibleList, transparent) { if (this.positions.length > 0 && this.material.transparent === transparent) { this.mesh.setPositions(this.positions); this.mesh.setColors(this.colors); this.mesh.update(PRIMITIVE_LINES, false); if (!this.meshInstance) { this.meshInstance = new MeshInstance(this.mesh, this.material, identityGraphNode); } visibleList.push(this.meshInstance); } } clear() { this.positions.length = 0; this.colors.length = 0; } constructor(device, material, layer){ this.material = material; this.layer = layer; this.positions = []; this.colors = []; this.mesh = new Mesh(device); this.meshInstance = null; } } class ImmediateBatches { getBatch(material, layer) { let batch = this.map.get(material); if (!batch) { batch = new ImmediateBatch(this.device, material, layer); this.map.set(material, batch); } return batch; } onPreRender(visibleList, transparent) { this.map.forEach((batch)=>{ batch.onPreRender(visibleList, transparent); }); } clear() { this.map.forEach((batch)=>batch.clear()); } constructor(device){ this.device = device; this.map = new Map(); } } const tempPoints = []; const vec$3 = new Vec3(); class Immediate { createMaterial(depthTest) { const material = new ShaderMaterial({ uniqueName: 'ImmediateLine', vertexGLSL: ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).get('immediateLineVS'), fragmentGLSL: ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).get('immediateLinePS'), vertexWGSL: ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).get('immediateLineVS'), fragmentWGSL: ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).get('immediateLinePS'), attributes: { vertex_position: SEMANTIC_POSITION, vertex_color: SEMANTIC_COLOR } }); material.blendType = BLEND_NORMAL; material.depthTest = depthTest; material.update(); return material; } get materialDepth() { if (!this._materialDepth) { this._materialDepth = this.createMaterial(true); } return this._materialDepth; } get materialNoDepth() { if (!this._materialNoDepth) { this._materialNoDepth = this.createMaterial(false); } return this._materialNoDepth; } getBatch(layer, depthTest) { let batches = this.batchesMap.get(layer); if (!batches) { batches = new ImmediateBatches(this.device); this.batchesMap.set(layer, batches); } this.allBatches.add(batches); const material = depthTest ? this.materialDepth : this.materialNoDepth; return batches.getBatch(material, layer); } getShaderDesc(id, fragmentGLSL, fragmentWGSL) { if (!this.shaderDescs.has(id)) { this.shaderDescs.set(id, { uniqueName: `DebugShader:${id}`, vertexGLSL: ` attribute vec2 vertex_position; uniform mat4 matrix_model; varying vec2 uv0; void main(void) { gl_Position = matrix_model * vec4(vertex_position, 0, 1); uv0 = vertex_position.xy + 0.5; } `, vertexWGSL: ` attribute vertex_position: vec2f; uniform matrix_model: mat4x4f; varying uv0: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = uniform.matrix_model * vec4f(input.vertex_position, 0.0, 1.0); output.uv0 = input.vertex_position.xy + vec2f(0.5); return output; } `, fragmentGLSL: fragmentGLSL, fragmentWGSL: fragmentWGSL, attributes: { vertex_position: SEMANTIC_POSITION } }); } return this.shaderDescs.get(id); } getTextureShaderDesc(encoding) { const decodeFunc = ChunkUtils.decodeFunc(encoding); return this.getShaderDesc(`textureShader-${encoding}`, ` #include "gammaPS" varying vec2 uv0; uniform sampler2D colorMap; void main (void) { vec3 linearColor = ${decodeFunc}(texture2D(colorMap, uv0)); gl_FragColor = vec4(gammaCorrectOutput(linearColor), 1); } `, ` #include "gammaPS" varying uv0: vec2f; var colorMap: texture_2d; var colorMapSampler: sampler; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; let sampledTex = textureSample(colorMap, colorMapSampler, input.uv0); let linearColor: vec3f = ${decodeFunc}(sampledTex); output.color = vec4f(gammaCorrectOutput(linearColor), 1.0); return output; } `); } getUnfilterableTextureShaderDesc() { return this.getShaderDesc('textureShaderUnfilterable', ` varying vec2 uv0; uniform highp sampler2D colorMap; void main (void) { ivec2 uv = ivec2(uv0 * textureSize(colorMap, 0)); gl_FragColor = vec4(texelFetch(colorMap, uv, 0).xyz, 1); } `, ` varying uv0: vec2f; var colorMap: texture_2d; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; let uv : vec2 = vec2(input.uv0 * vec2f(textureDimensions(colorMap, 0))); let fetchedColor : vec4f = textureLoad(colorMap, uv, 0); output.color = vec4f(fetchedColor.xyz, 1.0); return output; } `); } getDepthTextureShaderDesc() { return this.getShaderDesc('depthTextureShader', ` #include "screenDepthPS" #include "gammaPS" varying vec2 uv0; void main() { float depth = getLinearScreenDepth(getImageEffectUV(uv0)) * camera_params.x; gl_FragColor = vec4(gammaCorrectOutput(vec3(depth)), 1.0); } `, ` #include "screenDepthPS" #include "gammaPS" varying uv0: vec2f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let depth: f32 = getLinearScreenDepth(getImageEffectUV(input.uv0)) * uniform.camera_params.x; output.color = vec4f(gammaCorrectOutput(vec3f(depth)), 1.0); return output; } `); } getQuadMesh() { if (!this.quadMesh) { this.quadMesh = new Mesh(this.device); this.quadMesh.setPositions([ -0.5, -0.5, 0, 0.5, -0.5, 0, -0.5, 0.5, 0, 0.5, 0.5, 0 ]); this.quadMesh.update(PRIMITIVE_TRISTRIP); } return this.quadMesh; } drawMesh(material, matrix, mesh, meshInstance, layer) { if (!meshInstance) { const graphNode = this.getGraphNode(matrix); meshInstance = new MeshInstance(mesh, material, graphNode); } let layerMeshInstances = this.layerMeshInstances.get(layer); if (!layerMeshInstances) { layerMeshInstances = []; this.layerMeshInstances.set(layer, layerMeshInstances); } layerMeshInstances.push(meshInstance); } drawWireAlignedBox(min, max, color, depthTest, layer, mat) { if (mat) { const mulPoint = (x, y, z)=>{ vec$3.set(x, y, z); mat.transformPoint(vec$3, vec$3); tempPoints.push(vec$3.x, vec$3.y, vec$3.z); }; mulPoint(min.x, min.y, min.z); mulPoint(min.x, max.y, min.z); mulPoint(min.x, max.y, min.z); mulPoint(max.x, max.y, min.z); mulPoint(max.x, max.y, min.z); mulPoint(max.x, min.y, min.z); mulPoint(max.x, min.y, min.z); mulPoint(min.x, min.y, min.z); mulPoint(min.x, min.y, max.z); mulPoint(min.x, max.y, max.z); mulPoint(min.x, max.y, max.z); mulPoint(max.x, max.y, max.z); mulPoint(max.x, max.y, max.z); mulPoint(max.x, min.y, max.z); mulPoint(max.x, min.y, max.z); mulPoint(min.x, min.y, max.z); mulPoint(min.x, min.y, min.z); mulPoint(min.x, min.y, max.z); mulPoint(min.x, max.y, min.z); mulPoint(min.x, max.y, max.z); mulPoint(max.x, max.y, min.z); mulPoint(max.x, max.y, max.z); mulPoint(max.x, min.y, min.z); mulPoint(max.x, min.y, max.z); } else { tempPoints.push(min.x, min.y, min.z, min.x, max.y, min.z, min.x, max.y, min.z, max.x, max.y, min.z, max.x, max.y, min.z, max.x, min.y, min.z, max.x, min.y, min.z, min.x, min.y, min.z, min.x, min.y, max.z, min.x, max.y, max.z, min.x, max.y, max.z, max.x, max.y, max.z, max.x, max.y, max.z, max.x, min.y, max.z, max.x, min.y, max.z, min.x, min.y, max.z, min.x, min.y, min.z, min.x, min.y, max.z, min.x, max.y, min.z, min.x, max.y, max.z, max.x, max.y, min.z, max.x, max.y, max.z, max.x, min.y, min.z, max.x, min.y, max.z); } const batch = this.getBatch(layer, depthTest); batch.addLinesArrays(tempPoints, color); tempPoints.length = 0; } drawWireSphere(center, radius, color, numSegments, depthTest, layer) { const step = 2 * Math.PI / numSegments; let angle = 0; for(let i = 0; i < numSegments; i++){ const sin0 = Math.sin(angle); const cos0 = Math.cos(angle); angle += step; const sin1 = Math.sin(angle); const cos1 = Math.cos(angle); tempPoints.push(center.x + radius * sin0, center.y, center.z + radius * cos0); tempPoints.push(center.x + radius * sin1, center.y, center.z + radius * cos1); tempPoints.push(center.x + radius * sin0, center.y + radius * cos0, center.z); tempPoints.push(center.x + radius * sin1, center.y + radius * cos1, center.z); tempPoints.push(center.x, center.y + radius * sin0, center.z + radius * cos0); tempPoints.push(center.x, center.y + radius * sin1, center.z + radius * cos1); } const batch = this.getBatch(layer, depthTest); batch.addLinesArrays(tempPoints, color); tempPoints.length = 0; } getGraphNode(matrix) { const graphNode = new GraphNode('ImmediateDebug'); graphNode.worldTransform = matrix; graphNode._dirtyWorld = graphNode._dirtyNormal = false; return graphNode; } onPreRenderLayer(layer, visibleList, transparent) { this.batchesMap.forEach((batches, batchLayer)=>{ if (batchLayer === layer) { batches.onPreRender(visibleList, transparent); } }); if (!this.updatedLayers.has(layer)) { this.updatedLayers.add(layer); const meshInstances = this.layerMeshInstances.get(layer); if (meshInstances) { for(let i = 0; i < meshInstances.length; i++){ visibleList.push(meshInstances[i]); } meshInstances.length = 0; } } } onPostRender() { this.allBatches.forEach((batch)=>batch.clear()); this.allBatches.clear(); this.updatedLayers.clear(); } constructor(device){ this.shaderDescs = new Map(); this.device = device; this.quadMesh = null; this.textureShader = null; this.depthTextureShader = null; this.cubeLocalPos = null; this.cubeWorldPos = null; this.batchesMap = new Map(); this.allBatches = new Set(); this.updatedLayers = new Set(); this._materialDepth = null; this._materialNoDepth = null; this.layerMeshInstances = new Map(); } } const _goldenAngle = 2.399963229728653; const random = { circlePoint (point) { const r = Math.sqrt(Math.random()); const theta = Math.random() * 2 * Math.PI; point.x = r * Math.cos(theta); point.y = r * Math.sin(theta); }, circlePointDeterministic (point, index, numPoints) { const theta = index * _goldenAngle; const r = Math.sqrt(index / numPoints); point.x = r * Math.cos(theta); point.y = r * Math.sin(theta); }, spherePointDeterministic (point, index, numPoints, start = 0, end = 1) { start = 1 - 2 * start; end = 1 - 2 * end; const y = math.lerp(start, end, index / numPoints); const radius = Math.sqrt(1 - y * y); const theta = _goldenAngle * index; point.x = Math.cos(theta) * radius; point.y = y; point.z = Math.sin(theta) * radius; }, radicalInverse (i) { let bits = (i << 16 | i >>> 16) >>> 0; bits = ((bits & 0x55555555) << 1 | (bits & 0xAAAAAAAA) >>> 1) >>> 0; bits = ((bits & 0x33333333) << 2 | (bits & 0xCCCCCCCC) >>> 2) >>> 0; bits = ((bits & 0x0F0F0F0F) << 4 | (bits & 0xF0F0F0F0) >>> 4) >>> 0; bits = ((bits & 0x00FF00FF) << 8 | (bits & 0xFF00FF00) >>> 8) >>> 0; return bits * 2.3283064365386963e-10; } }; const getProjectionName = (projection)=>{ switch(projection){ case TEXTUREPROJECTION_CUBE: return 'Cubemap'; case TEXTUREPROJECTION_OCTAHEDRAL: return 'Octahedral'; default: return 'Equirect'; } }; const packFloat32ToRGBA8 = (value, array, offset)=>{ if (value <= 0) { array[offset + 0] = 0; array[offset + 1] = 0; array[offset + 2] = 0; array[offset + 3] = 0; } else if (value >= 1.0) { array[offset + 0] = 255; array[offset + 1] = 0; array[offset + 2] = 0; array[offset + 3] = 0; } else { let encX = 1 * value % 1; let encY = 255 * value % 1; let encZ = 65025 * value % 1; const encW = 16581375.0 * value % 1; encX -= encY / 255; encY -= encZ / 255; encZ -= encW / 255; array[offset + 0] = Math.min(255, Math.floor(encX * 256)); array[offset + 1] = Math.min(255, Math.floor(encY * 256)); array[offset + 2] = Math.min(255, Math.floor(encZ * 256)); array[offset + 3] = Math.min(255, Math.floor(encW * 256)); } }; const packSamples = (samples)=>{ const numSamples = samples.length; const w = Math.min(numSamples, 512); const h = Math.ceil(numSamples / w); const data = new Uint8Array(w * h * 4); let off = 0; for(let i = 0; i < numSamples; i += 4){ packFloat32ToRGBA8(samples[i + 0] * 0.5 + 0.5, data, off + 0); packFloat32ToRGBA8(samples[i + 1] * 0.5 + 0.5, data, off + 4); packFloat32ToRGBA8(samples[i + 2] * 0.5 + 0.5, data, off + 8); packFloat32ToRGBA8(samples[i + 3] / 8, data, off + 12); off += 16; } return { width: w, height: h, data: data }; }; const hemisphereSamplePhong = (dstVec, x, y, specularPower)=>{ const phi = y * 2 * Math.PI; const cosTheta = Math.pow(1 - x, 1 / (specularPower + 1)); const sinTheta = Math.sqrt(1 - cosTheta * cosTheta); dstVec.set(Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta).normalize(); }; const hemisphereSampleLambert = (dstVec, x, y)=>{ const phi = y * 2 * Math.PI; const cosTheta = Math.sqrt(1 - x); const sinTheta = Math.sqrt(x); dstVec.set(Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta).normalize(); }; const hemisphereSampleGGX = (dstVec, x, y, a)=>{ const phi = y * 2 * Math.PI; const cosTheta = Math.sqrt((1 - x) / (1 + (a * a - 1) * x)); const sinTheta = Math.sqrt(1 - cosTheta * cosTheta); dstVec.set(Math.cos(phi) * sinTheta, Math.sin(phi) * sinTheta, cosTheta).normalize(); }; const D_GGX = (NoH, linearRoughness)=>{ const a = NoH * linearRoughness; const k = linearRoughness / (1.0 - NoH * NoH + a * a); return k * k * (1 / Math.PI); }; const generatePhongSamples = (numSamples, specularPower)=>{ const H = new Vec3(); const result = []; for(let i = 0; i < numSamples; ++i){ hemisphereSamplePhong(H, i / numSamples, random.radicalInverse(i), specularPower); result.push(H.x, H.y, H.z, 0); } return result; }; const generateLambertSamples = (numSamples, sourceTotalPixels)=>{ const pixelsPerSample = sourceTotalPixels / numSamples; const H = new Vec3(); const result = []; for(let i = 0; i < numSamples; ++i){ hemisphereSampleLambert(H, i / numSamples, random.radicalInverse(i)); const pdf = H.z / Math.PI; const mipLevel = 0.5 * Math.log2(pixelsPerSample / pdf); result.push(H.x, H.y, H.z, mipLevel); } return result; }; const requiredSamplesGGX = { '16': { '2': 26, '8': 20, '32': 17, '128': 16, '512': 16 }, '32': { '2': 53, '8': 40, '32': 34, '128': 32, '512': 32 }, '128': { '2': 214, '8': 163, '32': 139, '128': 130, '512': 128 }, '1024': { '2': 1722, '8': 1310, '32': 1114, '128': 1041, '512': 1025 } }; const getRequiredSamplesGGX = (numSamples, specularPower)=>{ const table = requiredSamplesGGX[numSamples]; return table && table[specularPower] || numSamples; }; const generateGGXSamples = (numSamples, specularPower, sourceTotalPixels)=>{ const pixelsPerSample = sourceTotalPixels / numSamples; const roughness = 1 - Math.log2(specularPower) / 11.0; const a = roughness * roughness; const H = new Vec3(); const L = new Vec3(); const N = new Vec3(0, 0, 1); const result = []; const requiredSamples = getRequiredSamplesGGX(numSamples, specularPower); for(let i = 0; i < requiredSamples; ++i){ hemisphereSampleGGX(H, i / requiredSamples, random.radicalInverse(i), a); const NoH = H.z; L.set(H.x, H.y, H.z).mulScalar(2 * NoH).sub(N); if (L.z > 0) { const pdf = D_GGX(Math.min(1, NoH), a) / 4 + 0.001; const mipLevel = 0.5 * Math.log2(pixelsPerSample / pdf); result.push(L.x, L.y, L.z, mipLevel); } } while(result.length < numSamples * 4){ result.push(0, 0, 0, 0); } return result; }; const createSamplesTex = (device, name, samples)=>{ const packedSamples = packSamples(samples); return new Texture(device, { name: name, width: packedSamples.width, height: packedSamples.height, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, levels: [ packedSamples.data ] }); }; class SimpleCache { destroy() { if (this.destroyContent) { this.map.forEach((value, key)=>{ value.destroy(); }); } } get(key, missFunc) { if (!this.map.has(key)) { const result = missFunc(); this.map.set(key, result); return result; } return this.map.get(key); } constructor(destroyContent = true){ this.map = new Map(); this.destroyContent = destroyContent; } } const samplesCache = new SimpleCache(false); const deviceCache$1 = new DeviceCache(); const getCachedTexture = (device, key, getSamplesFnc)=>{ const cache = deviceCache$1.get(device, ()=>{ return new SimpleCache(); }); return cache.get(key, ()=>{ return createSamplesTex(device, key, samplesCache.get(key, getSamplesFnc)); }); }; const generateLambertSamplesTex = (device, numSamples, sourceTotalPixels)=>{ const key = `lambert-samples-${numSamples}-${sourceTotalPixels}`; return getCachedTexture(device, key, ()=>{ return generateLambertSamples(numSamples, sourceTotalPixels); }); }; const generatePhongSamplesTex = (device, numSamples, specularPower)=>{ const key = `phong-samples-${numSamples}-${specularPower}`; return getCachedTexture(device, key, ()=>{ return generatePhongSamples(numSamples, specularPower); }); }; const generateGGXSamplesTex = (device, numSamples, specularPower, sourceTotalPixels)=>{ const key = `ggx-samples-${numSamples}-${specularPower}-${sourceTotalPixels}`; return getCachedTexture(device, key, ()=>{ return generateGGXSamples(numSamples, specularPower, sourceTotalPixels); }); }; function reprojectTexture(source, target, options = {}) { const seamPixels = options.seamPixels ?? 0; const innerWidth = (options.rect?.z ?? target.width) - seamPixels * 2; const innerHeight = (options.rect?.w ?? target.height) - seamPixels * 2; if (innerWidth < 1 || innerHeight < 1) { return false; } const funcNames = { 'none': 'reproject', 'lambert': 'prefilterSamplesUnweighted', 'phong': 'prefilterSamplesUnweighted', 'ggx': 'prefilterSamples' }; const specularPower = options.hasOwnProperty('specularPower') ? options.specularPower : 1; const face = options.hasOwnProperty('face') ? options.face : null; const distribution = options.hasOwnProperty('distribution') ? options.distribution : specularPower === 1 ? 'none' : 'phong'; const processFunc = funcNames[distribution] || 'reproject'; const prefilterSamples = processFunc.startsWith('prefilterSamples'); const decodeFunc = ChunkUtils.decodeFunc(source.encoding); const encodeFunc = ChunkUtils.encodeFunc(target.encoding); const sourceFunc = `sample${getProjectionName(source.projection)}`; const targetFunc = `getDirection${getProjectionName(target.projection)}`; const numSamples = options.hasOwnProperty('numSamples') ? options.numSamples : 1024; const shaderKey = `ReprojectShader:${processFunc}_${decodeFunc}_${encodeFunc}_${sourceFunc}_${targetFunc}_${numSamples}`; const device = source.device; let shader = getProgramLibrary(device).getCachedShader(shaderKey); if (!shader) { const defines = new Map(); if (prefilterSamples) defines.set('USE_SAMPLES_TEX', ''); if (source.cubemap) defines.set('CUBEMAP_SOURCE', ''); defines.set('{PROCESS_FUNC}', processFunc); defines.set('{DECODE_FUNC}', decodeFunc); defines.set('{ENCODE_FUNC}', encodeFunc); defines.set('{SOURCE_FUNC}', sourceFunc); defines.set('{TARGET_FUNC}', targetFunc); defines.set('{NUM_SAMPLES}', numSamples); defines.set('{NUM_SAMPLES_SQRT}', Math.round(Math.sqrt(numSamples)).toFixed(1)); const wgsl = device.isWebGPU; const chunks = ShaderChunks.get(device, wgsl ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL); const includes = new Map(); includes.set('decodePS', chunks.get('decodePS')); includes.set('encodePS', chunks.get('encodePS')); shader = ShaderUtils.createShader(device, { uniqueName: shaderKey, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'reprojectVS', fragmentChunk: 'reprojectPS', fragmentIncludes: includes, fragmentDefines: defines }); } device.setBlendState(BlendState.NOBLEND); const constantSource = device.scope.resolve(source.cubemap ? 'sourceCube' : 'sourceTex'); constantSource.setValue(source); const constantParams = device.scope.resolve('params'); const uvModParam = device.scope.resolve('uvMod'); if (seamPixels > 0) { uvModParam.setValue([ (innerWidth + seamPixels * 2) / innerWidth, (innerHeight + seamPixels * 2) / innerHeight, -seamPixels / innerWidth, -seamPixels / innerHeight ]); } else { uvModParam.setValue([ 1, 1, 0, 0 ]); } const params = [ 0, target.width * target.height * (target.cubemap ? 6 : 1), source.width * source.height * (source.cubemap ? 6 : 1) ]; if (prefilterSamples) { const sourceTotalPixels = source.width * source.height * (source.cubemap ? 6 : 1); const samplesTex = distribution === 'ggx' ? generateGGXSamplesTex(device, numSamples, specularPower, sourceTotalPixels) : distribution === 'lambert' ? generateLambertSamplesTex(device, numSamples, sourceTotalPixels) : generatePhongSamplesTex(device, numSamples, specularPower); device.scope.resolve('samplesTex').setValue(samplesTex); device.scope.resolve('samplesTexInverseSize').setValue([ 1.0 / samplesTex.width, 1.0 / samplesTex.height ]); } for(let f = 0; f < (target.cubemap ? 6 : 1); f++){ if (face === null || f === face) { const renderTarget = new RenderTarget({ colorBuffer: target, face: f, depth: false, flipY: device.isWebGPU }); params[0] = f; constantParams.setValue(params); drawQuadWithShader(device, renderTarget, shader, options?.rect); renderTarget.destroy(); } } return true; } const calcLevels = (width, height = 0)=>{ return 1 + Math.floor(Math.log2(Math.max(width, height))); }; const supportsFloat16 = (device)=>{ return device.textureHalfFloatRenderable; }; const supportsFloat32 = (device)=>{ return device.textureFloatRenderable; }; const lightingSourcePixelFormat = (device)=>{ return supportsFloat16(device) ? PIXELFORMAT_RGBA16F : supportsFloat32(device) ? PIXELFORMAT_RGBA32F : PIXELFORMAT_RGBA8; }; const lightingPixelFormat = (device)=>{ return PIXELFORMAT_RGBA8; }; const createCubemap = (device, size, format, mipmaps)=>{ return new Texture(device, { name: `lighting-${size}`, cubemap: true, width: size, height: size, format: format, type: TEXTURETYPE_RGBP , addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, mipmaps: false }); }; class EnvLighting { static generateSkyboxCubemap(source, size) { const device = source.device; const result = createCubemap(device, size || (source.cubemap ? source.width : source.width / 4), PIXELFORMAT_RGBA8); reprojectTexture(source, result, { numSamples: 1024 }); return result; } static generateLightingSource(source, options) { const device = source.device; const format = lightingSourcePixelFormat(device); const result = options?.target || new Texture(device, { name: 'lighting-source', cubemap: true, width: options?.size || 128, height: options?.size || 128, format: format, type: format === PIXELFORMAT_RGBA8 ? TEXTURETYPE_RGBP : TEXTURETYPE_DEFAULT, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, mipmaps: true }); reprojectTexture(source, result, { numSamples: source.mipmaps ? 1 : 1024 }); return result; } static generateAtlas(source, options) { const device = source.device; const format = lightingPixelFormat(); const result = options?.target || new Texture(device, { name: 'envAtlas', width: options?.size || 512, height: options?.size || 512, format: format, type: TEXTURETYPE_RGBP , projection: TEXTUREPROJECTION_EQUIRECT, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, mipmaps: false }); const s = result.width / 512; const rect = new Vec4(0, 0, 512 * s, 256 * s); const levels = calcLevels(256) - calcLevels(4); for(let i = 0; i < levels; ++i){ reprojectTexture(source, result, { numSamples: 1, rect: rect, seamPixels: s }); rect.x += rect.w; rect.y += rect.w; rect.z = Math.max(1, Math.floor(rect.z * 0.5)); rect.w = Math.max(1, Math.floor(rect.w * 0.5)); } rect.set(0, 256 * s, 256 * s, 128 * s); for(let i = 1; i < 7; ++i){ reprojectTexture(source, result, { numSamples: options?.numReflectionSamples || 1024, distribution: options?.distribution || 'ggx', specularPower: Math.max(1, 2048 >> i * 2), rect: rect, seamPixels: s }); rect.y += rect.w; rect.z = Math.max(1, Math.floor(rect.z * 0.5)); rect.w = Math.max(1, Math.floor(rect.w * 0.5)); } rect.set(128 * s, (256 + 128) * s, 64 * s, 32 * s); reprojectTexture(source, result, { numSamples: options?.numAmbientSamples || 2048, distribution: 'lambert', rect: rect, seamPixels: s }); return result; } static generatePrefilteredAtlas(sources, options) { const device = sources[0].device; const format = sources[0].format; const type = sources[0].type; const result = options?.target || new Texture(device, { name: 'envPrefilteredAtlas', width: options?.size || 512, height: options?.size || 512, format: format, type: type, projection: TEXTUREPROJECTION_EQUIRECT, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, mipmaps: false }); const s = result.width / 512; const rect = new Vec4(0, 0, 512 * s, 256 * s); const levels = calcLevels(512); for(let i = 0; i < levels; ++i){ reprojectTexture(sources[0], result, { numSamples: 1, rect: rect, seamPixels: s }); rect.x += rect.w; rect.y += rect.w; rect.z = Math.max(1, Math.floor(rect.z * 0.5)); rect.w = Math.max(1, Math.floor(rect.w * 0.5)); } rect.set(0, 256 * s, 256 * s, 128 * s); for(let i = 1; i < sources.length; ++i){ reprojectTexture(sources[i], result, { numSamples: 1, rect: rect, seamPixels: s }); rect.y += rect.w; rect.z = Math.max(1, Math.floor(rect.z * 0.5)); rect.w = Math.max(1, Math.floor(rect.w * 0.5)); } rect.set(128 * s, (256 + 128) * s, 64 * s, 32 * s); if (options?.legacyAmbient) { reprojectTexture(sources[5], result, { numSamples: 1, rect: rect, seamPixels: s }); } else { reprojectTexture(sources[0], result, { numSamples: options?.numSamples || 2048, distribution: 'lambert', rect: rect, seamPixels: s }); } return result; } } class FogParams { constructor(){ this.type = FOG_NONE; this.color = new Color(0, 0, 0); this.density = 0; this.start = 1; this.end = 1000; } } class Scene extends EventHandler { get defaultDrawLayer() { return this.layers.getLayerById(LAYERID_IMMEDIATE); } set ambientBakeNumSamples(value) { this._ambientBakeNumSamples = math.clamp(Math.floor(value), 1, 255); } get ambientBakeNumSamples() { return this._ambientBakeNumSamples; } set ambientBakeSpherePart(value) { this._ambientBakeSpherePart = math.clamp(value, 0.001, 1); } get ambientBakeSpherePart() { return this._ambientBakeSpherePart; } set clusteredLightingEnabled(value) { if (this.device.isWebGPU && !value) { return; } if (!this._clusteredLightingEnabled && value) { console.error('Turning on disabled clustered lighting is not currently supported'); return; } this._clusteredLightingEnabled = value; } get clusteredLightingEnabled() { return this._clusteredLightingEnabled; } set envAtlas(value) { if (value !== this._envAtlas) { this._envAtlas = value; if (value) { value.addressU = ADDRESS_CLAMP_TO_EDGE; value.addressV = ADDRESS_CLAMP_TO_EDGE; value.minFilter = FILTER_LINEAR; value.magFilter = FILTER_LINEAR; value.mipmaps = false; } this._prefilteredCubemaps = []; if (this._internalEnvAtlas) { this._internalEnvAtlas.destroy(); this._internalEnvAtlas = null; } this._resetSkyMesh(); } } get envAtlas() { return this._envAtlas; } set layers(layers) { const prev = this._layers; this._layers = layers; this.fire('set:layers', prev, layers); } get layers() { return this._layers; } get sky() { return this._sky; } get lighting() { return this._lightingParams; } get gsplat() { return this._gsplatParams; } get fog() { return this._fogParams; } set lightmapFilterRange(value) { this._lightmapFilterRange = Math.max(value, 0.001); } get lightmapFilterRange() { return this._lightmapFilterRange; } set lightmapFilterSmoothness(value) { this._lightmapFilterSmoothness = Math.max(value, 0.001); } get lightmapFilterSmoothness() { return this._lightmapFilterSmoothness; } set prefilteredCubemaps(value) { value = value || []; const cubemaps = this._prefilteredCubemaps; const changed = cubemaps.length !== value.length || cubemaps.some((c, i)=>c !== value[i]); if (changed) { const complete = value.length === 6 && value.every((c)=>!!c); if (complete) { this._internalEnvAtlas = EnvLighting.generatePrefilteredAtlas(value, { target: this._internalEnvAtlas }); this._envAtlas = this._internalEnvAtlas; } else { if (this._internalEnvAtlas) { this._internalEnvAtlas.destroy(); this._internalEnvAtlas = null; } this._envAtlas = null; } this._prefilteredCubemaps = value.slice(); this._resetSkyMesh(); } } get prefilteredCubemaps() { return this._prefilteredCubemaps; } set skybox(value) { if (value !== this._skyboxCubeMap) { this._skyboxCubeMap = value; this._resetSkyMesh(); } } get skybox() { return this._skyboxCubeMap; } set skyboxIntensity(value) { if (value !== this._skyboxIntensity) { this._skyboxIntensity = value; this._resetSkyMesh(); } } get skyboxIntensity() { return this._skyboxIntensity; } set skyboxLuminance(value) { if (value !== this._skyboxLuminance) { this._skyboxLuminance = value; this._resetSkyMesh(); } } get skyboxLuminance() { return this._skyboxLuminance; } set skyboxMip(value) { if (value !== this._skyboxMip) { this._skyboxMip = value; this._resetSkyMesh(); } } get skyboxMip() { return this._skyboxMip; } set skyboxHighlightMultiplier(value) { if (value !== this._skyboxHighlightMultiplier) { this._skyboxHighlightMultiplier = value; this._resetSkyMesh(); } } get skyboxHighlightMultiplier() { return this._skyboxHighlightMultiplier; } set skyboxRotation(value) { if (!this._skyboxRotation.equals(value)) { const isIdentity = value.equals(Quat.IDENTITY); this._skyboxRotation.copy(value); if (isIdentity) { this._skyboxRotationMat3.setIdentity(); } else { this._skyboxRotationMat4.setTRS(Vec3.ZERO, value, Vec3.ONE); this._skyboxRotationMat3.invertMat4(this._skyboxRotationMat4); } if (!this._skyboxRotationShaderInclude && !isIdentity) { this._skyboxRotationShaderInclude = true; this._resetSkyMesh(); } } } get skyboxRotation() { return this._skyboxRotation; } destroy() { this._resetSkyMesh(); this.root = null; this.off(); } drawLine(start, end, color = Color.WHITE, depthTest = true, layer = this.defaultDrawLayer) { const batch = this.immediate.getBatch(layer, depthTest); batch.addLines([ start, end ], [ color, color ]); } drawLines(positions, colors, depthTest = true, layer = this.defaultDrawLayer) { const batch = this.immediate.getBatch(layer, depthTest); batch.addLines(positions, colors); } drawLineArrays(positions, colors, depthTest = true, layer = this.defaultDrawLayer) { const batch = this.immediate.getBatch(layer, depthTest); batch.addLinesArrays(positions, colors); } applySettings(settings) { const physics = settings.physics; const render = settings.render; this._gravity.set(physics.gravity[0], physics.gravity[1], physics.gravity[2]); this.ambientLight.set(render.global_ambient[0], render.global_ambient[1], render.global_ambient[2]); this.ambientLuminance = render.ambientLuminance; this.fog.type = render.fog; this.fog.color.set(render.fog_color[0], render.fog_color[1], render.fog_color[2]); this.fog.start = render.fog_start; this.fog.end = render.fog_end; this.fog.density = render.fog_density; this.lightmapSizeMultiplier = render.lightmapSizeMultiplier; this.lightmapMaxResolution = render.lightmapMaxResolution; this.lightmapMode = render.lightmapMode; this.exposure = render.exposure; this._skyboxIntensity = render.skyboxIntensity ?? 1; this._skyboxLuminance = render.skyboxLuminance ?? 20000; this._skyboxMip = render.skyboxMip ?? 0; if (render.skyboxRotation) { this.skyboxRotation = new Quat().setFromEulerAngles(render.skyboxRotation[0], render.skyboxRotation[1], render.skyboxRotation[2]); } this.sky.applySettings(render); this.clusteredLightingEnabled = render.clusteredLightingEnabled ?? false; this.lighting.applySettings(render); [ 'lightmapFilterEnabled', 'lightmapFilterRange', 'lightmapFilterSmoothness', 'ambientBake', 'ambientBakeNumSamples', 'ambientBakeSpherePart', 'ambientBakeOcclusionBrightness', 'ambientBakeOcclusionContrast' ].forEach((setting)=>{ if (render.hasOwnProperty(setting)) { this[setting] = render[setting]; } }); this._resetSkyMesh(); } _getSkyboxTex() { const cubemaps = this._prefilteredCubemaps; if (this._skyboxMip) { const skyboxMapping = [ 0, 1, 3, 4, 5, 6 ]; return cubemaps[skyboxMapping[this._skyboxMip]] || this._envAtlas || cubemaps[0] || this._skyboxCubeMap; } return this._skyboxCubeMap || cubemaps[0] || this._envAtlas; } _updateSkyMesh() { if (!this.sky.skyMesh) { this.sky.updateSkyMesh(); } this.sky.update(); } _resetSkyMesh() { this.sky.resetSkyMesh(); this.updateShaders = true; } setSkybox(cubemaps) { if (!cubemaps) { this.skybox = null; this.envAtlas = null; } else { this.skybox = cubemaps[0] || null; if (cubemaps[1] && !cubemaps[1].cubemap) { this.envAtlas = cubemaps[1]; } else { this.prefilteredCubemaps = cubemaps.slice(1); } } } get lightmapPixelFormat() { return this.lightmapHDR && this.device.getRenderableHdrFormat() || PIXELFORMAT_RGBA8; } constructor(graphicsDevice){ super(), this.ambientBake = false, this.ambientBakeOcclusionBrightness = 0, this.ambientBakeOcclusionContrast = 0, this.ambientLight = new Color(0, 0, 0), this.ambientLuminance = 0, this.exposure = 1, this.lightmapSizeMultiplier = 1, this.lightmapMaxResolution = 2048, this.lightmapMode = BAKE_COLORDIR, this.lightmapFilterEnabled = false, this.lightmapHDR = false, this.root = null, this.physicalUnits = false, this._envAtlas = null, this._skyboxCubeMap = null, this._fogParams = new FogParams(), this.forcePassThroughSpecular = false; this.device = graphicsDevice; this._gravity = new Vec3(0, -9.8, 0); this._layers = null; this._prefilteredCubemaps = []; this._internalEnvAtlas = null; this._skyboxIntensity = 1; this._skyboxLuminance = 0; this._skyboxMip = 0; this._skyboxHighlightMultiplier = 1; this._skyboxRotationShaderInclude = false; this._skyboxRotation = new Quat(); this._skyboxRotationMat3 = new Mat3(); this._skyboxRotationMat4 = new Mat4(); this._ambientBakeNumSamples = 1; this._ambientBakeSpherePart = 0.4; this._lightmapFilterRange = 10; this._lightmapFilterSmoothness = 0.2; this._clusteredLightingEnabled = true; this._lightingParams = new LightingParams(this.device.supportsAreaLights, this.device.maxTextureSize, ()=>{ this.updateShaders = true; }); this._gsplatParams = new GSplatParams(); this._sky = new Sky(this); this._stats = { meshInstances: 0, lights: 0, dynamicLights: 0, bakedLights: 0, updateShadersTime: 0 }; this.updateShaders = true; this._shaderVersion = 0; this.immediate = new Immediate(this.device); } } Scene.EVENT_SETLAYERS = 'set:layers'; Scene.EVENT_SETSKYBOX = 'set:skybox'; Scene.EVENT_PRERENDER = 'prerender'; Scene.EVENT_POSTRENDER = 'postrender'; Scene.EVENT_PRERENDER_LAYER = 'prerender:layer'; Scene.EVENT_POSTRENDER_LAYER = 'postrender:layer'; Scene.EVENT_PRECULL = 'precull'; Scene.EVENT_POSTCULL = 'postcull'; class Skin { constructor(graphicsDevice, ibp, boneNames){ this.device = graphicsDevice; this.inverseBindPose = ibp; this.boneNames = boneNames; } } const spriteNormals = [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1 ]; const spriteIndices = [ 0, 1, 3, 2, 3, 1 ]; class Sprite extends EventHandler { set frameKeys(value) { this._frameKeys = value; if (this._atlas && this._frameKeys) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } this.fire('set:frameKeys', value); } get frameKeys() { return this._frameKeys; } set atlas(value) { if (value === this._atlas) return; if (this._atlas) { this._atlas.off('set:frames', this._onSetFrames, this); this._atlas.off('set:frame', this._onFrameChanged, this); this._atlas.off('remove:frame', this._onFrameRemoved, this); } this._atlas = value; if (this._atlas && this._frameKeys) { this._atlas.on('set:frames', this._onSetFrames, this); this._atlas.on('set:frame', this._onFrameChanged, this); this._atlas.on('remove:frame', this._onFrameRemoved, this); if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } this.fire('set:atlas', value); } get atlas() { return this._atlas; } set pixelsPerUnit(value) { if (this._pixelsPerUnit === value) return; this._pixelsPerUnit = value; this.fire('set:pixelsPerUnit', value); if (this._atlas && this._frameKeys && this.renderMode === SPRITE_RENDERMODE_SIMPLE) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } } get pixelsPerUnit() { return this._pixelsPerUnit; } set renderMode(value) { if (this._renderMode === value) { return; } const prev = this._renderMode; this._renderMode = value; this.fire('set:renderMode', value); if (prev === SPRITE_RENDERMODE_SIMPLE || value === SPRITE_RENDERMODE_SIMPLE) { if (this._atlas && this._frameKeys) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } } } get renderMode() { return this._renderMode; } get meshes() { return this._meshes; } _createMeshes() { const len = this._meshes.length; for(let i = 0; i < len; i++){ const mesh = this._meshes[i]; if (mesh) { mesh.destroy(); } } const count = this._frameKeys.length; this._meshes = new Array(count); const createMeshFunc = this.renderMode === SPRITE_RENDERMODE_SLICED || this._renderMode === SPRITE_RENDERMODE_TILED ? this._create9SliceMesh : this._createSimpleMesh; for(let i = 0; i < count; i++){ const frame = this._atlas.frames[this._frameKeys[i]]; this._meshes[i] = frame ? createMeshFunc.call(this, frame) : null; } this.fire('set:meshes'); } _createSimpleMesh(frame) { const rect = frame.rect; const texWidth = this._atlas.texture.width; const texHeight = this._atlas.texture.height; const w = rect.z / this._pixelsPerUnit; const h = rect.w / this._pixelsPerUnit; const hp = frame.pivot.x; const vp = frame.pivot.y; const positions = [ -hp * w, -vp * h, 0, (1 - hp) * w, -vp * h, 0, (1 - hp) * w, (1 - vp) * h, 0, -hp * w, (1 - vp) * h, 0 ]; const lu = rect.x / texWidth; const bv = 1.0 - rect.y / texHeight; const ru = (rect.x + rect.z) / texWidth; const tv = 1.0 - (rect.y + rect.w) / texHeight; const uvs = [ lu, bv, ru, bv, ru, tv, lu, tv ]; const geom = new Geometry(); geom.positions = positions; geom.normals = spriteNormals; geom.uvs = uvs; geom.indices = spriteIndices; return Mesh.fromGeometry(this._device, geom); } _create9SliceMesh() { const he = Vec2.ONE; const ws = 3; const ls = 3; const positions = []; const normals = []; const uvs = []; const indices = []; let vcounter = 0; for(let i = 0; i <= ws; i++){ const u = i === 0 || i === ws ? 0 : 1; for(let j = 0; j <= ls; j++){ const x = -he.x + 2.0 * he.x * (i <= 1 ? 0 : 3) / ws; const y = 0.0; const z = -(-he.y + 2.0 * he.y * (j <= 1 ? 0 : 3) / ls); const v = j === 0 || j === ls ? 0 : 1; positions.push(-x, y, z); normals.push(0.0, 1.0, 0.0); uvs.push(u, v); if (i < ws && j < ls) { indices.push(vcounter + ls + 1, vcounter + 1, vcounter); indices.push(vcounter + ls + 1, vcounter + ls + 2, vcounter + 1); } vcounter++; } } const geom = new Geometry(); geom.positions = positions; geom.normals = normals; geom.uvs = uvs; geom.indices = indices; return Mesh.fromGeometry(this._device, geom); } _onSetFrames(frames) { if (this._updatingProperties) { this._meshesDirty = true; } else { this._createMeshes(); } } _onFrameChanged(frameKey, frame) { const idx = this._frameKeys.indexOf(frameKey); if (idx < 0) return; if (frame) { if (this.renderMode === SPRITE_RENDERMODE_SIMPLE) { this._meshes[idx] = this._createSimpleMesh(frame); } } else { this._meshes[idx] = null; } this.fire('set:meshes'); } _onFrameRemoved(frameKey) { const idx = this._frameKeys.indexOf(frameKey); if (idx < 0) return; this._meshes[idx] = null; this.fire('set:meshes'); } startUpdate() { this._updatingProperties = true; this._meshesDirty = false; } endUpdate() { this._updatingProperties = false; if (this._meshesDirty && this._atlas && this._frameKeys) { this._createMeshes(); } this._meshesDirty = false; } destroy() { for (const mesh of this._meshes){ if (mesh) { mesh.destroy(); } } this._meshes.length = 0; } constructor(device, options){ super(); this._device = device; this._pixelsPerUnit = options && options.pixelsPerUnit !== undefined ? options.pixelsPerUnit : 1; this._renderMode = options && options.renderMode !== undefined ? options.renderMode : SPRITE_RENDERMODE_SIMPLE; this._atlas = options && options.atlas !== undefined ? options.atlas : null; this._frameKeys = options && options.frameKeys !== undefined ? options.frameKeys : null; this._meshes = []; this._updatingProperties = false; this._meshesDirty = false; if (this._atlas && this._frameKeys) { this._createMeshes(); } } } class TextureAtlas extends EventHandler { set texture(value) { this._texture = value; this.fire('set:texture', value); } get texture() { return this._texture; } set frames(value) { this._frames = value; this.fire('set:frames', value); } get frames() { return this._frames; } setFrame(key, data) { let frame = this._frames[key]; if (!frame) { frame = { rect: data.rect.clone(), pivot: data.pivot.clone(), border: data.border.clone() }; this._frames[key] = frame; } else { frame.rect.copy(data.rect); frame.pivot.copy(data.pivot); frame.border.copy(data.border); } this.fire('set:frame', key.toString(), frame); } removeFrame(key) { const frame = this._frames[key]; if (frame) { delete this._frames[key]; this.fire('remove:frame', key.toString(), frame); } } destroy() { if (this._texture) { this._texture.destroy(); } } constructor(){ super(); this._texture = null; this._frames = null; } } class AnimationKey { constructor(time, position, rotation, scale){ this.time = time; this.position = position; this.rotation = rotation; this.scale = scale; } } class AnimationNode { constructor(){ this._name = ''; this._keys = []; } } class Animation { getNode(name) { return this._nodeDict[name]; } addNode(node) { this._nodes.push(node); this._nodeDict[node._name] = node; } get nodes() { return this._nodes; } constructor(){ this.name = ''; this.duration = 0; this._nodes = []; this._nodeDict = {}; } } class InterpolatedKey { getTarget() { return this._targetNode; } setTarget(node) { this._targetNode = node; } constructor(){ this._written = false; this._name = ''; this._keyFrames = []; this._quat = new Quat(); this._pos = new Vec3(); this._scale = new Vec3(); this._targetNode = null; } } class Skeleton { set animation(value) { this._animation = value; this.currentTime = 0; } get animation() { return this._animation; } set currentTime(value) { this._time = value; const numNodes = this._interpolatedKeys.length; for(let i = 0; i < numNodes; i++){ const node = this._interpolatedKeys[i]; const nodeName = node._name; this._currKeyIndices[nodeName] = 0; } this.addTime(0); this.updateGraph(); } get currentTime() { return this._time; } get numNodes() { return this._interpolatedKeys.length; } addTime(delta) { if (this._animation !== null) { const nodes = this._animation._nodes; const duration = this._animation.duration; if (this._time === duration && !this.looping) { return; } this._time += delta; if (this._time > duration) { this._time = this.looping ? 0.0 : duration; for(let i = 0; i < nodes.length; i++){ const node = nodes[i]; const nodeName = node._name; this._currKeyIndices[nodeName] = 0; } } else if (this._time < 0) { this._time = this.looping ? duration : 0.0; for(let i = 0; i < nodes.length; i++){ const node = nodes[i]; const nodeName = node._name; this._currKeyIndices[nodeName] = node._keys.length - 2; } } const offset = delta >= 0 ? 1 : -1; for(let i = 0; i < nodes.length; i++){ const node = nodes[i]; const nodeName = node._name; const keys = node._keys; const interpKey = this._interpolatedKeyDict[nodeName]; if (interpKey === undefined) { continue; } let foundKey = false; if (keys.length !== 1) { for(let currKeyIndex = this._currKeyIndices[nodeName]; currKeyIndex < keys.length - 1 && currKeyIndex >= 0; currKeyIndex += offset){ const k1 = keys[currKeyIndex]; const k2 = keys[currKeyIndex + 1]; if (k1.time <= this._time && k2.time >= this._time) { const alpha = (this._time - k1.time) / (k2.time - k1.time); interpKey._pos.lerp(k1.position, k2.position, alpha); interpKey._quat.slerp(k1.rotation, k2.rotation, alpha); interpKey._scale.lerp(k1.scale, k2.scale, alpha); interpKey._written = true; this._currKeyIndices[nodeName] = currKeyIndex; foundKey = true; break; } } } if (keys.length === 1 || !foundKey && this._time === 0.0 && this.looping) { interpKey._pos.copy(keys[0].position); interpKey._quat.copy(keys[0].rotation); interpKey._scale.copy(keys[0].scale); interpKey._written = true; } } } } blend(skel1, skel2, alpha) { const numNodes = this._interpolatedKeys.length; for(let i = 0; i < numNodes; i++){ const key1 = skel1._interpolatedKeys[i]; const key2 = skel2._interpolatedKeys[i]; const dstKey = this._interpolatedKeys[i]; if (key1._written && key2._written) { dstKey._quat.slerp(key1._quat, skel2._interpolatedKeys[i]._quat, alpha); dstKey._pos.lerp(key1._pos, skel2._interpolatedKeys[i]._pos, alpha); dstKey._scale.lerp(key1._scale, key2._scale, alpha); dstKey._written = true; } else if (key1._written) { dstKey._quat.copy(key1._quat); dstKey._pos.copy(key1._pos); dstKey._scale.copy(key1._scale); dstKey._written = true; } else if (key2._written) { dstKey._quat.copy(key2._quat); dstKey._pos.copy(key2._pos); dstKey._scale.copy(key2._scale); dstKey._written = true; } } } setGraph(graph) { this.graph = graph; if (graph) { for(let i = 0; i < this._interpolatedKeys.length; i++){ const interpKey = this._interpolatedKeys[i]; const graphNode = graph.findByName(interpKey._name); this._interpolatedKeys[i].setTarget(graphNode); } } else { for(let i = 0; i < this._interpolatedKeys.length; i++){ this._interpolatedKeys[i].setTarget(null); } } } updateGraph() { if (this.graph) { for(let i = 0; i < this._interpolatedKeys.length; i++){ const interpKey = this._interpolatedKeys[i]; if (interpKey._written) { const transform = interpKey.getTarget(); transform.localPosition.copy(interpKey._pos); transform.localRotation.copy(interpKey._quat); transform.localScale.copy(interpKey._scale); if (!transform._dirtyLocal) { transform._dirtifyLocal(); } interpKey._written = false; } } } } constructor(graph){ this.looping = true; this._animation = null; this._time = 0; this._interpolatedKeys = []; this._interpolatedKeyDict = {}; this._currKeyIndices = {}; this.graph = null; const addInterpolatedKeys = (node)=>{ const interpKey = new InterpolatedKey(); interpKey._name = node.name; this._interpolatedKeys.push(interpKey); this._interpolatedKeyDict[node.name] = interpKey; this._currKeyIndices[node.name] = 0; for(let i = 0; i < node._children.length; i++){ addInterpolatedKeys(node._children[i]); } }; addInterpolatedKeys(graph); } } const prefixSumSource = ` @group(0) @binding(0) var items: array; @group(0) @binding(1) var blockSums: array; struct PrefixSumUniforms { elementCount: u32 }; @group(0) @binding(2) var uniforms: PrefixSumUniforms; const WORKGROUP_SIZE_X: u32 = {WORKGROUP_SIZE_X}u; const WORKGROUP_SIZE_Y: u32 = {WORKGROUP_SIZE_Y}u; const THREADS_PER_WORKGROUP: u32 = {THREADS_PER_WORKGROUP}u; const ITEMS_PER_WORKGROUP: u32 = {ITEMS_PER_WORKGROUP}u; var temp: array; @compute @workgroup_size(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, 1) fn reduce_downsweep( @builtin(workgroup_id) w_id: vec3, @builtin(num_workgroups) w_dim: vec3, @builtin(local_invocation_index) TID: u32, ) { let WORKGROUP_ID = w_id.x + w_id.y * w_dim.x; let WID = WORKGROUP_ID * THREADS_PER_WORKGROUP; let GID = WID + TID; let ELM_TID = TID * 2; let ELM_GID = GID * 2; temp[ELM_TID] = select(items[ELM_GID], 0u, ELM_GID >= uniforms.elementCount); temp[ELM_TID + 1u] = select(items[ELM_GID + 1u], 0u, ELM_GID + 1u >= uniforms.elementCount); var offset: u32 = 1u; for (var d: u32 = ITEMS_PER_WORKGROUP >> 1u; d > 0u; d >>= 1u) { workgroupBarrier(); if (TID < d) { var ai: u32 = offset * (ELM_TID + 1u) - 1u; var bi: u32 = offset * (ELM_TID + 2u) - 1u; temp[bi] += temp[ai]; } offset *= 2u; } if (TID == 0u) { let last_offset = ITEMS_PER_WORKGROUP - 1u; blockSums[WORKGROUP_ID] = temp[last_offset]; temp[last_offset] = 0u; } for (var d: u32 = 1u; d < ITEMS_PER_WORKGROUP; d *= 2u) { offset >>= 1u; workgroupBarrier(); if (TID < d) { var ai: u32 = offset * (ELM_TID + 1u) - 1u; var bi: u32 = offset * (ELM_TID + 2u) - 1u; let t: u32 = temp[ai]; temp[ai] = temp[bi]; temp[bi] += t; } } workgroupBarrier(); if (ELM_GID < uniforms.elementCount) { items[ELM_GID] = temp[ELM_TID]; } if (ELM_GID + 1u < uniforms.elementCount) { items[ELM_GID + 1u] = temp[ELM_TID + 1u]; } } @compute @workgroup_size(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, 1) fn add_block_sums( @builtin(workgroup_id) w_id: vec3, @builtin(num_workgroups) w_dim: vec3, @builtin(local_invocation_index) TID: u32, ) { let WORKGROUP_ID = w_id.x + w_id.y * w_dim.x; let WID = WORKGROUP_ID * THREADS_PER_WORKGROUP; let GID = WID + TID; let ELM_ID = GID * 2u; if (ELM_ID >= uniforms.elementCount) { return; } let blockSum = blockSums[WORKGROUP_ID]; items[ELM_ID] += blockSum; if (ELM_ID + 1u >= uniforms.elementCount) { return; } items[ELM_ID + 1u] += blockSum; } `; const WORKGROUP_SIZE_X$2 = 16; const WORKGROUP_SIZE_Y$2 = 16; const THREADS_PER_WORKGROUP$2 = WORKGROUP_SIZE_X$2 * WORKGROUP_SIZE_Y$2; const ITEMS_PER_WORKGROUP = 2 * THREADS_PER_WORKGROUP$2; class PrefixSumKernel { destroy() { this._destroyPasses(); this._scanShader?.destroy(); this._addBlockShader?.destroy(); this._bindGroupFormat?.destroy(); this._scanShader = null; this._addBlockShader = null; this._bindGroupFormat = null; this._uniformBufferFormat = null; } _createFormatsAndShaders() { this._uniformBufferFormat = new UniformBufferFormat(this.device, [ new UniformFormat('elementCount', UNIFORMTYPE_UINT) ]); this._bindGroupFormat = new BindGroupFormat(this.device, [ new BindStorageBufferFormat('items', SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat('blockSums', SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE) ]); this._scanShader = this._createShader('PrefixSumScan', 'reduce_downsweep'); this._addBlockShader = this._createShader('PrefixSumAddBlock', 'add_block_sums'); } createPassesRecursive(dataBuffer, count) { const workgroupCount = Math.ceil(count / ITEMS_PER_WORKGROUP); const { x: dispatchX, y: dispatchY } = this.findOptimalDispatchSize(workgroupCount); const blockSumBuffer = new StorageBuffer(this.device, workgroupCount * 4); const scanCompute = new Compute(this.device, this._scanShader, 'PrefixSumScan'); scanCompute.setParameter('items', dataBuffer); scanCompute.setParameter('blockSums', blockSumBuffer); const pass = { scanCompute, addBlockCompute: null, blockSumBuffer, dispatchX, dispatchY, count, allocatedCount: count }; this.passes.push(pass); if (workgroupCount > 1) { this.createPassesRecursive(blockSumBuffer, workgroupCount); const addBlockCompute = new Compute(this.device, this._addBlockShader, 'PrefixSumAddBlock'); addBlockCompute.setParameter('items', dataBuffer); addBlockCompute.setParameter('blockSums', blockSumBuffer); pass.addBlockCompute = addBlockCompute; } } _createShader(name, entryPoint) { const cdefines = new Map(); cdefines.set('{WORKGROUP_SIZE_X}', WORKGROUP_SIZE_X$2); cdefines.set('{WORKGROUP_SIZE_Y}', WORKGROUP_SIZE_Y$2); cdefines.set('{THREADS_PER_WORKGROUP}', THREADS_PER_WORKGROUP$2); cdefines.set('{ITEMS_PER_WORKGROUP}', ITEMS_PER_WORKGROUP); return new Shader(this.device, { name: name, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: prefixSumSource, cdefines: cdefines, computeEntryPoint: entryPoint, computeBindGroupFormat: this._bindGroupFormat, computeUniformBufferFormats: { uniforms: this._uniformBufferFormat } }); } findOptimalDispatchSize(workgroupCount) { const maxDimension = this.device.limits.maxComputeWorkgroupsPerDimension || 65535; if (workgroupCount <= maxDimension) { return { x: workgroupCount, y: 1 }; } const x = Math.floor(Math.sqrt(workgroupCount)); const y = Math.ceil(workgroupCount / x); return { x, y }; } resize(dataBuffer, count) { const requiredPasses = this._countPassesNeeded(count); const currentPasses = this.passes.length; if (requiredPasses > currentPasses) { this._destroyPasses(); this.createPassesRecursive(dataBuffer, count); return; } let levelCount = count; for(let i = 0; i < this.passes.length; i++){ const workgroupCount = Math.ceil(levelCount / ITEMS_PER_WORKGROUP); const { x: dispatchX, y: dispatchY } = this.findOptimalDispatchSize(workgroupCount); this.passes[i].count = levelCount; this.passes[i].dispatchX = dispatchX; this.passes[i].dispatchY = dispatchY; levelCount = workgroupCount; if (workgroupCount <= 1) { break; } } } _destroyPasses() { for (const pass of this.passes){ pass.blockSumBuffer?.destroy(); } this.passes.length = 0; } _countPassesNeeded(count) { let passes = 0; let levelCount = count; while(levelCount > 0){ passes++; const workgroupCount = Math.ceil(levelCount / ITEMS_PER_WORKGROUP); if (workgroupCount <= 1) break; levelCount = workgroupCount; } return passes; } dispatch(device) { for(let i = 0; i < this.passes.length; i++){ const pass = this.passes[i]; pass.scanCompute.setParameter('elementCount', pass.count); pass.scanCompute.setupDispatch(pass.dispatchX, pass.dispatchY, 1); device.computeDispatch([ pass.scanCompute ], 'PrefixSumScan'); } for(let i = this.passes.length - 1; i >= 0; i--){ const pass = this.passes[i]; if (pass.addBlockCompute) { pass.addBlockCompute.setParameter('elementCount', pass.count); pass.addBlockCompute.setupDispatch(pass.dispatchX, pass.dispatchY, 1); device.computeDispatch([ pass.addBlockCompute ], 'PrefixSumAddBlock'); } } } constructor(device){ this.passes = []; this._uniformBufferFormat = null; this._bindGroupFormat = null; this._scanShader = null; this._addBlockShader = null; this.device = device; this._createFormatsAndShaders(); } } const radixSort4bitSource = ` @group(0) @binding(0) var input: array; @group(0) @binding(1) var local_prefix_sums: array; @group(0) @binding(2) var block_sums: array; struct RadixSortUniforms { workgroupCount: u32, elementCount: u32 }; @group(0) @binding(3) var uniforms: RadixSortUniforms; const THREADS_PER_WORKGROUP: u32 = {THREADS_PER_WORKGROUP}u; const WORKGROUP_SIZE_X: u32 = {WORKGROUP_SIZE_X}u; const WORKGROUP_SIZE_Y: u32 = {WORKGROUP_SIZE_Y}u; const CURRENT_BIT: u32 = {CURRENT_BIT}u; var histogram: array, 16>; var thread_digits: array; @compute @workgroup_size(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, 1) fn radix_sort( @builtin(workgroup_id) w_id: vec3, @builtin(num_workgroups) w_dim: vec3, @builtin(local_invocation_index) TID: u32, ) { let WORKGROUP_ID = w_id.x + w_id.y * w_dim.x; let WID = WORKGROUP_ID * THREADS_PER_WORKGROUP; let GID = WID + TID; if (TID < 16u) { atomicStore(&histogram[TID], 0u); } workgroupBarrier(); let is_valid = GID < uniforms.elementCount && WORKGROUP_ID < uniforms.workgroupCount; let elm = select(0u, input[GID], is_valid); let digit: u32 = select(16u, (elm >> CURRENT_BIT) & 0xFu, is_valid); thread_digits[TID] = digit; if (is_valid) { atomicAdd(&histogram[digit], 1u); } workgroupBarrier(); var local_prefix: u32 = 0u; if (is_valid) { let digit_vec = vec4(digit, digit, digit, digit); let ones = vec4(1u, 1u, 1u, 1u); let zeros = vec4(0u, 0u, 0u, 0u); var i: u32 = 0u; let limit = TID & ~3u; for (; i < limit; i += 4u) { let d = vec4( thread_digits[i], thread_digits[i + 1u], thread_digits[i + 2u], thread_digits[i + 3u] ); let matches = select(zeros, ones, d == digit_vec); local_prefix += matches.x + matches.y + matches.z + matches.w; } for (; i < TID; i++) { local_prefix += select(0u, 1u, thread_digits[i] == digit); } } if (is_valid) { local_prefix_sums[GID] = local_prefix; } if (TID < 16u && WORKGROUP_ID < uniforms.workgroupCount) { block_sums[TID * uniforms.workgroupCount + WORKGROUP_ID] = atomicLoad(&histogram[TID]); } } `; const radixSortReorderSource = ` @group(0) @binding(0) var inputKeys: array; @group(0) @binding(1) var outputKeys: array; @group(0) @binding(2) var local_prefix_sum: array; @group(0) @binding(3) var prefix_block_sum: array; @group(0) @binding(4) var inputValues: array; @group(0) @binding(5) var outputValues: array; struct RadixSortUniforms { workgroupCount: u32, elementCount: u32 }; @group(0) @binding(6) var uniforms: RadixSortUniforms; const THREADS_PER_WORKGROUP: u32 = {THREADS_PER_WORKGROUP}u; const WORKGROUP_SIZE_X: u32 = {WORKGROUP_SIZE_X}u; const WORKGROUP_SIZE_Y: u32 = {WORKGROUP_SIZE_Y}u; const CURRENT_BIT: u32 = {CURRENT_BIT}u; const IS_FIRST_PASS: u32 = {IS_FIRST_PASS}u; @compute @workgroup_size(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, 1) fn radix_sort_reorder( @builtin(workgroup_id) w_id: vec3, @builtin(num_workgroups) w_dim: vec3, @builtin(local_invocation_index) TID: u32, ) { let WORKGROUP_ID = w_id.x + w_id.y * w_dim.x; let WID = WORKGROUP_ID * THREADS_PER_WORKGROUP; let GID = WID + TID; if (GID >= uniforms.elementCount) { return; } let k = inputKeys[GID]; let v = select(inputValues[GID], GID, IS_FIRST_PASS == 1u); let local_prefix = local_prefix_sum[GID]; let extract_bits = (k >> CURRENT_BIT) & 0xFu; let pid = extract_bits * uniforms.workgroupCount + WORKGROUP_ID; let sorted_position = prefix_block_sum[pid] + local_prefix; outputKeys[sorted_position] = k; outputValues[sorted_position] = v; } `; const BITS_PER_PASS = 4; const BUCKET_COUNT = 16; const WORKGROUP_SIZE_X$1 = 16; const WORKGROUP_SIZE_Y$1 = 16; const THREADS_PER_WORKGROUP$1 = WORKGROUP_SIZE_X$1 * WORKGROUP_SIZE_Y$1; class ComputeRadixSort { destroy() { this._destroyBuffers(); this._destroyPasses(); this._blockSumBindGroupFormat?.destroy(); this._reorderBindGroupFormat?.destroy(); this._blockSumBindGroupFormat = null; this._reorderBindGroupFormat = null; this._uniformBufferFormat = null; } _destroyPasses() { for (const pass of this._passes){ pass.blockSumCompute.shader?.destroy(); pass.reorderCompute.shader?.destroy(); } this._passes.length = 0; this._numBits = 0; } _destroyBuffers() { this._keys0?.destroy(); this._keys1?.destroy(); this._values0?.destroy(); this._values1?.destroy(); this._localPrefixSums?.destroy(); this._blockSums?.destroy(); this._sortedIndices?.destroy(); this._prefixSumKernel?.destroy(); this._keys0 = null; this._keys1 = null; this._values0 = null; this._values1 = null; this._localPrefixSums = null; this._blockSums = null; this._sortedIndices = null; this._prefixSumKernel = null; this._workgroupCount = 0; this._allocatedWorkgroupCount = 0; } get sortedIndices() { return this._sortedIndices; } _createBindGroupFormats() { const device = this.device; this._uniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat('workgroupCount', UNIFORMTYPE_UINT), new UniformFormat('elementCount', UNIFORMTYPE_UINT) ]); this._blockSumBindGroupFormat = new BindGroupFormat(device, [ new BindStorageBufferFormat('input', SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat('local_prefix_sums', SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat('block_sums', SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE) ]); this._reorderBindGroupFormat = new BindGroupFormat(device, [ new BindStorageBufferFormat('inputKeys', SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat('outputKeys', SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat('local_prefix_sum', SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat('prefix_block_sum', SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat('inputValues', SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat('outputValues', SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE) ]); } _createPasses(numBits) { this._destroyPasses(); this._numBits = numBits; const numPasses = numBits / BITS_PER_PASS; for(let pass = 0; pass < numPasses; pass++){ const bitOffset = pass * BITS_PER_PASS; const isFirstPass = pass === 0; const blockSumShader = this._createShader(`RadixSort4bit-BlockSum-${bitOffset}`, radixSort4bitSource, 'radix_sort', bitOffset, false, this._blockSumBindGroupFormat); const reorderShader = this._createShader(`RadixSort4bit-Reorder-${bitOffset}`, radixSortReorderSource, 'radix_sort_reorder', bitOffset, isFirstPass, this._reorderBindGroupFormat); const blockSumCompute = new Compute(this.device, blockSumShader, `RadixSort4bit-BlockSum-${bitOffset}`); const reorderCompute = new Compute(this.device, reorderShader, `RadixSort4bit-Reorder-${bitOffset}`); this._passes.push({ blockSumCompute, reorderCompute }); } } _allocateBuffers(elementCount, numBits) { const workgroupCount = Math.ceil(elementCount / THREADS_PER_WORKGROUP$1); const buffersNeedRealloc = workgroupCount > this._allocatedWorkgroupCount || !this._keys0; const passesNeedRecreate = numBits !== this._numBits; this._workgroupCount = workgroupCount; this._dispatchSize = this._findOptimalDispatchSize(workgroupCount); if (buffersNeedRealloc) { this._destroyBuffers(); this._allocatedWorkgroupCount = workgroupCount; this._workgroupCount = workgroupCount; this._dispatchSize = this._findOptimalDispatchSize(workgroupCount); const elementSize = elementCount * 4; const blockSumSize = BUCKET_COUNT * workgroupCount * 4; this._keys0 = new StorageBuffer(this.device, elementSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._keys1 = new StorageBuffer(this.device, elementSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._values0 = new StorageBuffer(this.device, elementSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._values1 = new StorageBuffer(this.device, elementSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._localPrefixSums = new StorageBuffer(this.device, elementSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._blockSums = new StorageBuffer(this.device, blockSumSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._sortedIndices = new StorageBuffer(this.device, elementSize, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._prefixSumKernel = new PrefixSumKernel(this.device); } this._prefixSumKernel.resize(this._blockSums, BUCKET_COUNT * workgroupCount); if (passesNeedRecreate) { this._createPasses(numBits); } } _findOptimalDispatchSize(workgroupCount) { const maxDimension = this.device.limits.maxComputeWorkgroupsPerDimension || 65535; if (workgroupCount <= maxDimension) { return { x: workgroupCount, y: 1 }; } const x = Math.floor(Math.sqrt(workgroupCount)); const y = Math.ceil(workgroupCount / x); return { x, y }; } _createShader(name, source, entryPoint, currentBit, isFirstPass, bindGroupFormat) { const cdefines = new Map(); cdefines.set('{WORKGROUP_SIZE_X}', WORKGROUP_SIZE_X$1); cdefines.set('{WORKGROUP_SIZE_Y}', WORKGROUP_SIZE_Y$1); cdefines.set('{THREADS_PER_WORKGROUP}', THREADS_PER_WORKGROUP$1); cdefines.set('{CURRENT_BIT}', currentBit); cdefines.set('{IS_FIRST_PASS}', isFirstPass ? 1 : 0); return new Shader(this.device, { name: name, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: source, cdefines: cdefines, computeEntryPoint: entryPoint, computeBindGroupFormat: bindGroupFormat, computeUniformBufferFormats: { uniforms: this._uniformBufferFormat } }); } sort(keysBuffer, elementCount, numBits = 16) { this._elementCount = elementCount; this._allocateBuffers(elementCount, numBits); const device = this.device; const numPasses = numBits / BITS_PER_PASS; let currentKeys = keysBuffer; let currentValues = this._values0; let nextKeys = this._keys0; let nextValues = this._values1; for(let pass = 0; pass < numPasses; pass++){ const { blockSumCompute, reorderCompute } = this._passes[pass]; const isLastPass = pass === numPasses - 1; blockSumCompute.setParameter('input', currentKeys); blockSumCompute.setParameter('local_prefix_sums', this._localPrefixSums); blockSumCompute.setParameter('block_sums', this._blockSums); blockSumCompute.setParameter('workgroupCount', this._workgroupCount); blockSumCompute.setParameter('elementCount', elementCount); blockSumCompute.setupDispatch(this._dispatchSize.x, this._dispatchSize.y, 1); device.computeDispatch([ blockSumCompute ], 'RadixSort-BlockSum'); this._prefixSumKernel.dispatch(device); const outputValues = isLastPass ? this._sortedIndices : nextValues; reorderCompute.setParameter('inputKeys', currentKeys); reorderCompute.setParameter('outputKeys', nextKeys); reorderCompute.setParameter('local_prefix_sum', this._localPrefixSums); reorderCompute.setParameter('prefix_block_sum', this._blockSums); reorderCompute.setParameter('inputValues', currentValues); reorderCompute.setParameter('outputValues', outputValues); reorderCompute.setParameter('workgroupCount', this._workgroupCount); reorderCompute.setParameter('elementCount', elementCount); reorderCompute.setupDispatch(this._dispatchSize.x, this._dispatchSize.y, 1); device.computeDispatch([ reorderCompute ], 'RadixSort-Reorder'); if (!isLastPass) { currentKeys = nextKeys; nextKeys = currentKeys === this._keys0 ? this._keys1 : this._keys0; const tempValues = currentValues; currentValues = nextValues; nextValues = tempValues; } } return this._sortedIndices; } constructor(device){ this._elementCount = 0; this._workgroupCount = 0; this._allocatedWorkgroupCount = 0; this._numBits = 0; this._keys0 = null; this._keys1 = null; this._values0 = null; this._values1 = null; this._localPrefixSums = null; this._blockSums = null; this._sortedIndices = null; this._prefixSumKernel = null; this._dispatchSize = { x: 1, y: 1 }; this._blockSumBindGroupFormat = null; this._reorderBindGroupFormat = null; this._uniformBufferFormat = null; this._passes = []; this.device = device; this._createBindGroupFormats(); } } class RenderPassShaderQuad extends RenderPass { set shader(shader) { this.quadRender?.destroy(); this.quadRender = null; this._shader = shader; if (shader) { this.quadRender = new QuadRender(shader); } } get shader() { return this._shader; } execute() { const device = this.device; device.setBlendState(this.blendState); device.setCullMode(this.cullMode); device.setDepthState(this.depthState); device.setStencilState(this.stencilFront, this.stencilBack); this.quadRender?.render(this.viewport, this.scissor); } constructor(...args){ super(...args), this._shader = null, this.quadRender = null, this.cullMode = CULLFACE_NONE, this.blendState = BlendState.NOBLEND, this.depthState = DepthState.NODEPTH, this.stencilFront = null, this.stencilBack = null; } } class LitShaderOptions { constructor(){ this.hasTangents = false; this.shaderChunks = null; this.pass = 0; this.alphaTest = false; this.blendType = BLEND_NONE; this.separateAmbient = false; this.screenSpace = false; this.skin = false; this.batch = false; this.useInstancing = false; this.useMorphPosition = false; this.useMorphNormal = false; this.useMorphTextureBasedInt = false; this.nineSlicedMode = 0; this.clusteredLightingEnabled = true; this.clusteredLightingCookiesEnabled = false; this.clusteredLightingShadowsEnabled = false; this.clusteredLightingShadowType = 0; this.clusteredLightingAreaLightsEnabled = false; this.vertexColors = false; this.useVertexColorGamma = false; this.lightMapEnabled = false; this.dirLightMapEnabled = false; this.useHeights = false; this.useNormals = false; this.useClearCoatNormals = false; this.useAo = false; this.diffuseMapEnabled = false; this.pixelSnap = false; this.ambientSH = false; this.ssao = false; this.twoSidedLighting = false; this.occludeDirect = false; this.occludeSpecular = 0; this.occludeSpecularFloat = false; this.useMsdf = false; this.msdfTextAttribute = false; this.alphaToCoverage = false; this.opacityFadesSpecular = false; this.opacityDither = DITHER_NONE; this.opacityShadowDither = DITHER_NONE; this.cubeMapProjection = 0; this.useSpecular = false; this.useSpecularityFactor = false; this.enableGGXSpecular = false; this.fresnelModel = 0; this.useRefraction = false; this.useClearCoat = false; this.useSheen = false; this.useIridescence = false; this.useMetalness = false; this.useDynamicRefraction = false; this.dispersion = false; this.fog = FOG_NONE; this.gamma = GAMMA_NONE; this.toneMap = -1; this.reflectionSource = REFLECTIONSRC_NONE; this.reflectionEncoding = null; this.reflectionCubemapEncoding = null; this.ambientSource = 'constant'; this.ambientEncoding = null; this.skyboxIntensity = 1.0; this.useCubeMapRotation = false; this.lightMapWithoutAmbient = false; this.lights = []; this.noShadow = false; this.lightMaskDynamic = 0x0; this.userAttributes = {}; this.linearDepth = false; this.shadowCatcher = false; } } class LitMaterialOptionsBuilder { static update(litOptions, material, scene, renderParams, objDefs, pass, sortedLights) { LitMaterialOptionsBuilder.updateSharedOptions(litOptions, material, scene, objDefs, pass); LitMaterialOptionsBuilder.updateMaterialOptions(litOptions, material); LitMaterialOptionsBuilder.updateEnvOptions(litOptions, material, scene, renderParams); LitMaterialOptionsBuilder.updateLightingOptions(litOptions, material, scene, objDefs, sortedLights); } static updateSharedOptions(litOptions, material, scene, objDefs, pass) { litOptions.shaderChunks = material.shaderChunks; litOptions.pass = pass; litOptions.alphaTest = material.alphaTest > 0; litOptions.blendType = material.blendType; litOptions.screenSpace = objDefs && (objDefs & SHADERDEF_SCREENSPACE) !== 0; litOptions.skin = objDefs && (objDefs & SHADERDEF_SKIN) !== 0; litOptions.useInstancing = objDefs && (objDefs & SHADERDEF_INSTANCING) !== 0; litOptions.useMorphPosition = objDefs && (objDefs & SHADERDEF_MORPH_POSITION) !== 0; litOptions.useMorphNormal = objDefs && (objDefs & SHADERDEF_MORPH_NORMAL) !== 0; litOptions.useMorphTextureBasedInt = objDefs && (objDefs & SHADERDEF_MORPH_TEXTURE_BASED_INT) !== 0; litOptions.hasTangents = objDefs && (objDefs & SHADERDEF_TANGENTS) !== 0; litOptions.nineSlicedMode = material.nineSlicedMode || SPRITE_RENDERMODE_SIMPLE; if (material.useLighting && scene.clusteredLightingEnabled) { litOptions.clusteredLightingEnabled = true; litOptions.clusteredLightingCookiesEnabled = scene.lighting.cookiesEnabled; litOptions.clusteredLightingShadowsEnabled = scene.lighting.shadowsEnabled; litOptions.clusteredLightingShadowType = scene.lighting.shadowType; litOptions.clusteredLightingAreaLightsEnabled = scene.lighting.areaLightsEnabled; } else { litOptions.clusteredLightingEnabled = false; litOptions.clusteredLightingCookiesEnabled = false; litOptions.clusteredLightingShadowsEnabled = false; litOptions.clusteredLightingAreaLightsEnabled = false; } } static updateMaterialOptions(litOptions, material) { litOptions.separateAmbient = false; litOptions.pixelSnap = material.pixelSnap; litOptions.ambientSH = material.ambientSH; litOptions.twoSidedLighting = material.twoSidedLighting; litOptions.occludeDirect = material.occludeDirect; litOptions.occludeSpecular = material.occludeSpecular; litOptions.occludeSpecularFloat = material.occludeSpecularIntensity !== 1.0; litOptions.useMsdf = false; litOptions.msdfTextAttribute = false; litOptions.alphaToCoverage = material.alphaToCoverage; litOptions.opacityFadesSpecular = material.opacityFadesSpecular; litOptions.opacityDither = material.opacityDither; litOptions.cubeMapProjection = CUBEPROJ_NONE; litOptions.useSpecular = material.hasSpecular; litOptions.useSpecularityFactor = material.hasSpecularityFactor; litOptions.enableGGXSpecular = material.ggxSpecular; litOptions.useAnisotropy = false; litOptions.fresnelModel = material.fresnelModel; litOptions.useRefraction = material.hasRefraction; litOptions.useClearCoat = material.hasClearCoat; litOptions.useSheen = material.hasSheen; litOptions.useIridescence = material.hasIrridescence; litOptions.useMetalness = material.hasMetalness; litOptions.useDynamicRefraction = material.dynamicRefraction; litOptions.dispersion = material.dispersion > 0; litOptions.vertexColors = false; litOptions.lightMapEnabled = material.hasLighting; litOptions.dirLightMapEnabled = material.dirLightMap; litOptions.useHeights = material.hasHeights; litOptions.useNormals = material.hasNormals; litOptions.useClearCoatNormals = material.hasClearCoatNormals; litOptions.useAo = material.hasAo; litOptions.diffuseMapEnabled = material.hasDiffuseMap; } static updateEnvOptions(litOptions, material, scene, renderParams) { litOptions.fog = material.useFog ? renderParams.fog : FOG_NONE; litOptions.gamma = renderParams.shaderOutputGamma; litOptions.toneMap = material.useTonemap ? renderParams.toneMapping : TONEMAP_NONE; if (material.useSkybox && scene.envAtlas && scene.skybox) { litOptions.reflectionSource = REFLECTIONSRC_ENVATLASHQ; litOptions.reflectionEncoding = scene.envAtlas.encoding; litOptions.reflectionCubemapEncoding = scene.skybox.encoding; } else if (material.useSkybox && scene.envAtlas) { litOptions.reflectionSource = REFLECTIONSRC_ENVATLAS; litOptions.reflectionEncoding = scene.envAtlas.encoding; } else if (material.useSkybox && scene.skybox) { litOptions.reflectionSource = REFLECTIONSRC_CUBEMAP; litOptions.reflectionEncoding = scene.skybox.encoding; } else { litOptions.reflectionSource = REFLECTIONSRC_NONE; litOptions.reflectionEncoding = null; } if (material.ambientSH) { litOptions.ambientSource = AMBIENTSRC_AMBIENTSH; litOptions.ambientEncoding = null; } else if (litOptions.reflectionSource !== REFLECTIONSRC_NONE && scene.envAtlas) { litOptions.ambientSource = AMBIENTSRC_ENVALATLAS; litOptions.ambientEncoding = scene.envAtlas.encoding; } else { litOptions.ambientSource = AMBIENTSRC_CONSTANT; litOptions.ambientEncoding = null; } const hasSkybox = litOptions.reflectionSource !== REFLECTIONSRC_NONE; litOptions.skyboxIntensity = hasSkybox; litOptions.useCubeMapRotation = hasSkybox && scene._skyboxRotationShaderInclude; } static updateLightingOptions(litOptions, material, scene, objDefs, sortedLights) { litOptions.lightMapWithoutAmbient = false; if (material.useLighting) { const lightsFiltered = []; const mask = objDefs ? objDefs >> 16 : MASK_AFFECT_DYNAMIC; litOptions.lightMaskDynamic = !!(mask & MASK_AFFECT_DYNAMIC); litOptions.lightMapWithoutAmbient = false; if (sortedLights) { LitMaterialOptionsBuilder.collectLights(LIGHTTYPE_DIRECTIONAL, sortedLights[LIGHTTYPE_DIRECTIONAL], lightsFiltered, mask); if (!scene.clusteredLightingEnabled) { LitMaterialOptionsBuilder.collectLights(LIGHTTYPE_OMNI, sortedLights[LIGHTTYPE_OMNI], lightsFiltered, mask); LitMaterialOptionsBuilder.collectLights(LIGHTTYPE_SPOT, sortedLights[LIGHTTYPE_SPOT], lightsFiltered, mask); } } litOptions.lights = lightsFiltered; } else { litOptions.lights = []; } if (litOptions.lights.length === 0 && !scene.clusteredLightingEnabled || (objDefs & SHADERDEF_NOSHADOW) !== 0) { litOptions.noShadow = true; } } static collectLights(lType, lights, lightsFiltered, mask) { for(let i = 0; i < lights.length; i++){ const light = lights[i]; if (light.enabled) { if (light.mask & mask) { lightsFiltered.push(light); } } } } } const builtinAttributes = { vertex_normal: SEMANTIC_NORMAL, vertex_tangent: SEMANTIC_TANGENT, vertex_texCoord0: SEMANTIC_TEXCOORD0, vertex_texCoord1: SEMANTIC_TEXCOORD1, vertex_color: SEMANTIC_COLOR, vertex_boneWeights: SEMANTIC_BLENDWEIGHT, vertex_boneIndices: SEMANTIC_BLENDINDICES }; class LitShader { fDefineSet(condition, name, value = '') { if (condition) { this.fDefines.set(name, value); } } generateVertexShader(useUv, useUnmodifiedUv, mapTransforms) { const { options, vDefines, attributes } = this; const varyings = new Map(); varyings.set('vPositionW', 'vec3'); if (options.nineSlicedMode === SPRITE_RENDERMODE_SLICED || options.nineSlicedMode === SPRITE_RENDERMODE_TILED) { vDefines.set('NINESLICED', true); } if (this.options.linearDepth) { vDefines.set('LINEAR_DEPTH', true); varyings.set('vLinearDepth', 'float'); } if (this.needsNormal) vDefines.set('NORMALS', true); if (this.options.useInstancing) { const languageChunks = ShaderChunks.get(this.device, this.shaderLanguage); if (this.chunks.get('transformInstancingVS') === languageChunks.get('transformInstancingVS')) { attributes.instance_line1 = SEMANTIC_ATTR11; attributes.instance_line2 = SEMANTIC_ATTR12; attributes.instance_line3 = SEMANTIC_ATTR14; attributes.instance_line4 = SEMANTIC_ATTR15; } } if (this.needsNormal) { attributes.vertex_normal = SEMANTIC_NORMAL; varyings.set('vNormalW', 'vec3'); if (options.hasTangents && (options.useHeights || options.useNormals || options.useClearCoatNormals || options.enableGGXSpecular)) { vDefines.set('TANGENTS', true); attributes.vertex_tangent = SEMANTIC_TANGENT; varyings.set('vTangentW', 'vec3'); varyings.set('vBinormalW', 'vec3'); } else if (options.enableGGXSpecular) { vDefines.set('GGX_SPECULAR', true); varyings.set('vObjectSpaceUpW', 'vec3'); } } const maxUvSets = 2; for(let i = 0; i < maxUvSets; i++){ if (useUv[i]) { vDefines.set(`UV${i}`, true); attributes[`vertex_texCoord${i}`] = `TEXCOORD${i}`; } if (useUnmodifiedUv[i]) { vDefines.set(`UV${i}_UNMODIFIED`, true); varyings.set(`vUv${i}`, 'vec2'); } } let numTransforms = 0; const transformDone = new Set(); mapTransforms.forEach((mapTransform)=>{ const { id, uv, name } = mapTransform; const checkId = id + uv * 100; if (!transformDone.has(checkId)) { transformDone.add(checkId); varyings.set(`vUV${uv}_${id}`, 'vec2'); const varName = `texture_${name}MapTransform`; vDefines.set(`{TRANSFORM_NAME_${numTransforms}}`, varName); vDefines.set(`{TRANSFORM_UV_${numTransforms}}`, uv); vDefines.set(`{TRANSFORM_ID_${numTransforms}}`, id); numTransforms++; } }); vDefines.set('UV_TRANSFORMS_COUNT', numTransforms); if (options.vertexColors) { attributes.vertex_color = SEMANTIC_COLOR; vDefines.set('VERTEX_COLOR', true); varyings.set('vVertexColor', 'vec4'); if (options.useVertexColorGamma) { vDefines.set('STD_VERTEX_COLOR_GAMMA', ''); } } if (options.useMsdf && options.msdfTextAttribute) { attributes.vertex_outlineParameters = SEMANTIC_ATTR8; attributes.vertex_shadowParameters = SEMANTIC_ATTR9; vDefines.set('MSDF', true); } if (options.useMorphPosition || options.useMorphNormal) { vDefines.set('MORPHING', true); if (options.useMorphTextureBasedInt) vDefines.set('MORPHING_INT', true); if (options.useMorphPosition) vDefines.set('MORPHING_POSITION', true); if (options.useMorphNormal) vDefines.set('MORPHING_NORMAL', true); attributes.morph_vertex_id = SEMANTIC_ATTR15; } if (options.skin) { attributes.vertex_boneIndices = SEMANTIC_BLENDINDICES; if (options.batch) { vDefines.set('BATCH', true); } else { attributes.vertex_boneWeights = SEMANTIC_BLENDWEIGHT; vDefines.set('SKIN', true); } } if (options.useInstancing) vDefines.set('INSTANCING', true); if (options.screenSpace) vDefines.set('SCREENSPACE', true); if (options.pixelSnap) vDefines.set('PIXELSNAP', true); varyings.forEach((type, name)=>{ this.varyingsCode += `#define VARYING_${name.toUpperCase()}\n`; this.varyingsCode += this.shaderLanguage === SHADERLANGUAGE_WGSL ? `varying ${name}: ${primitiveGlslToWgslTypeMap.get(type)};\n` : `varying ${type} ${name};\n`; }); this.includes.set('varyingsVS', this.varyingsCode); this.includes.set('varyingsPS', this.varyingsCode); this.vshader = ` #include "litMainVS" `; } _setupLightingDefines(hasAreaLights, clusteredLightingEnabled) { const fDefines = this.fDefines; const options = this.options; this.fDefines.set('LIGHT_COUNT', options.lights.length); if (hasAreaLights) fDefines.set('AREA_LIGHTS', true); if (clusteredLightingEnabled && this.lighting) { fDefines.set('LIT_CLUSTERED_LIGHTS', true); if (options.clusteredLightingCookiesEnabled) fDefines.set('CLUSTER_COOKIES', true); if (options.clusteredLightingAreaLightsEnabled) fDefines.set('CLUSTER_AREALIGHTS', true); if (options.lightMaskDynamic) fDefines.set('CLUSTER_MESH_DYNAMIC_LIGHTS', true); if (options.clusteredLightingShadowsEnabled && !options.noShadow) { const clusteredShadowInfo = shadowTypeInfo.get(options.clusteredLightingShadowType); fDefines.set('CLUSTER_SHADOWS', true); fDefines.set(`SHADOW_KIND_${clusteredShadowInfo.kind}`, true); fDefines.set(`CLUSTER_SHADOW_TYPE_${clusteredShadowInfo.kind}`, true); } } for(let i = 0; i < options.lights.length; i++){ const light = options.lights[i]; const lightType = light._type; if (clusteredLightingEnabled && lightType !== LIGHTTYPE_DIRECTIONAL) { continue; } const lightShape = hasAreaLights && light._shape ? light._shape : LIGHTSHAPE_PUNCTUAL; const shadowType = light._shadowType; const castShadow = light.castShadows && !options.noShadow; const shadowInfo = shadowTypeInfo.get(shadowType); fDefines.set(`LIGHT${i}`, true); fDefines.set(`LIGHT${i}TYPE`, `${lightTypeNames[lightType]}`); fDefines.set(`LIGHT${i}SHADOWTYPE`, `${shadowInfo.name}`); fDefines.set(`LIGHT${i}SHAPE`, `${lightShapeNames[lightShape]}`); fDefines.set(`LIGHT${i}FALLOFF`, `${lightFalloffNames[light._falloffMode]}`); if (light.affectSpecularity) fDefines.set(`LIGHT${i}AFFECT_SPECULARITY`, true); if (light._cookie) { if (lightType === LIGHTTYPE_SPOT && !light._cookie._cubemap || lightType === LIGHTTYPE_OMNI && light._cookie._cubemap) { fDefines.set(`LIGHT${i}COOKIE`, true); fDefines.set(`{LIGHT${i}COOKIE_CHANNEL}`, light._cookieChannel); if (lightType === LIGHTTYPE_SPOT) { if (light._cookieTransform) fDefines.set(`LIGHT${i}COOKIE_TRANSFORM`, true); if (light._cookieFalloff) fDefines.set(`LIGHT${i}COOKIE_FALLOFF`, true); } } } if (castShadow) { fDefines.set(`LIGHT${i}CASTSHADOW`, true); if (shadowInfo.pcf) fDefines.set(`LIGHT${i}SHADOW_PCF`, true); if (light._normalOffsetBias && !light._isVsm) fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_NORMAL_OFFSET`, true); if (lightType === LIGHTTYPE_DIRECTIONAL) { fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_ORTHO`, true); if (light.cascadeBlend > 0) fDefines.set(`LIGHT${i}_SHADOW_CASCADE_BLEND`, true); if (light.numCascades > 1) fDefines.set(`LIGHT${i}_SHADOW_CASCADES`, true); } if (shadowInfo.pcf || shadowInfo.pcss || this.device.isWebGPU) fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_SOURCE_ZBUFFER`, true); if (lightType === LIGHTTYPE_OMNI) fDefines.set(`LIGHT${i}_SHADOW_SAMPLE_POINT`, true); } if (castShadow) { fDefines.set(`SHADOW_KIND_${shadowInfo.kind}`, true); if (lightType === LIGHTTYPE_DIRECTIONAL) fDefines.set('SHADOW_DIRECTIONAL', true); } } } prepareForwardPass(lightingUv) { const { options } = this; const clusteredAreaLights = options.clusteredLightingEnabled && options.clusteredLightingAreaLightsEnabled; const hasAreaLights = clusteredAreaLights || options.lights.some((light)=>{ return light._shape && light._shape !== LIGHTSHAPE_PUNCTUAL; }); const addAmbient = !options.lightMapEnabled || options.lightMapWithoutAmbient; const hasTBN = this.needsNormal && (options.useNormals || options.useClearCoatNormals || options.enableGGXSpecular && !options.useHeights); if (options.useSpecular) { this.fDefineSet(true, 'LIT_SPECULAR'); this.fDefineSet(this.reflections, 'LIT_REFLECTIONS'); this.fDefineSet(options.useClearCoat, 'LIT_CLEARCOAT'); this.fDefineSet(options.fresnelModel > 0, 'LIT_SPECULAR_FRESNEL'); this.fDefineSet(options.useSheen, 'LIT_SHEEN'); this.fDefineSet(options.useIridescence, 'LIT_IRIDESCENCE'); } this.fDefineSet(this.lighting && options.useSpecular || this.reflections, 'LIT_SPECULAR_OR_REFLECTION'); this.fDefineSet(this.needsSceneColor, 'LIT_SCENE_COLOR'); this.fDefineSet(this.needsScreenSize, 'LIT_SCREEN_SIZE'); this.fDefineSet(this.needsTransforms, 'LIT_TRANSFORMS'); this.fDefineSet(this.needsNormal, 'LIT_NEEDS_NORMAL'); this.fDefineSet(this.lighting, 'LIT_LIGHTING'); this.fDefineSet(options.useMetalness, 'LIT_METALNESS'); this.fDefineSet(options.enableGGXSpecular, 'LIT_GGX_SPECULAR'); this.fDefineSet(options.useAnisotropy, 'LIT_ANISOTROPY'); this.fDefineSet(options.useSpecularityFactor, 'LIT_SPECULARITY_FACTOR'); this.fDefineSet(options.useCubeMapRotation, 'CUBEMAP_ROTATION'); this.fDefineSet(options.occludeSpecularFloat, 'LIT_OCCLUDE_SPECULAR_FLOAT'); this.fDefineSet(options.separateAmbient, 'LIT_SEPARATE_AMBIENT'); this.fDefineSet(options.twoSidedLighting, 'LIT_TWO_SIDED_LIGHTING'); this.fDefineSet(options.lightMapEnabled, 'LIT_LIGHTMAP'); this.fDefineSet(options.dirLightMapEnabled, 'LIT_DIR_LIGHTMAP'); this.fDefineSet(options.skyboxIntensity > 0, 'LIT_SKYBOX_INTENSITY'); this.fDefineSet(options.clusteredLightingShadowsEnabled, 'LIT_CLUSTERED_SHADOWS'); this.fDefineSet(options.clusteredLightingAreaLightsEnabled, 'LIT_CLUSTERED_AREA_LIGHTS'); this.fDefineSet(hasTBN, 'LIT_TBN'); this.fDefineSet(addAmbient, 'LIT_ADD_AMBIENT'); this.fDefineSet(options.hasTangents, 'LIT_TANGENTS'); this.fDefineSet(options.useNormals, 'LIT_USE_NORMALS'); this.fDefineSet(options.useClearCoatNormals, 'LIT_USE_CLEARCOAT_NORMALS'); this.fDefineSet(options.useRefraction, 'LIT_REFRACTION'); this.fDefineSet(options.useDynamicRefraction, 'LIT_DYNAMIC_REFRACTION'); this.fDefineSet(options.dispersion, 'LIT_DISPERSION'); this.fDefineSet(options.useHeights, 'LIT_HEIGHTS'); this.fDefineSet(options.opacityFadesSpecular, 'LIT_OPACITY_FADES_SPECULAR'); this.fDefineSet(options.alphaToCoverage, 'LIT_ALPHA_TO_COVERAGE'); this.fDefineSet(options.alphaTest, 'LIT_ALPHA_TEST'); this.fDefineSet(options.useMsdf, 'LIT_MSDF'); this.fDefineSet(options.ssao, 'LIT_SSAO'); this.fDefineSet(options.useAo, 'LIT_AO'); this.fDefineSet(options.occludeDirect, 'LIT_OCCLUDE_DIRECT'); this.fDefineSet(options.msdfTextAttribute, 'LIT_MSDF_TEXT_ATTRIBUTE'); this.fDefineSet(options.diffuseMapEnabled, 'LIT_DIFFUSE_MAP'); this.fDefineSet(options.shadowCatcher, 'LIT_SHADOW_CATCHER'); this.fDefineSet(true, 'LIT_FRESNEL_MODEL', fresnelNames[options.fresnelModel]); this.fDefineSet(true, 'LIT_NONE_SLICE_MODE', spriteRenderModeNames[options.nineSlicedMode]); this.fDefineSet(true, 'LIT_BLEND_TYPE', blendNames[options.blendType]); this.fDefineSet(true, 'LIT_CUBEMAP_PROJECTION', cubemaProjectionNames[options.cubeMapProjection]); this.fDefineSet(true, 'LIT_OCCLUDE_SPECULAR', specularOcclusionNames[options.occludeSpecular]); this.fDefineSet(true, 'LIT_REFLECTION_SOURCE', reflectionSrcNames[options.reflectionSource]); this.fDefineSet(true, 'LIT_AMBIENT_SOURCE', ambientSrcNames[options.ambientSource]); this.fDefineSet(true, '{lightingUv}', lightingUv ?? ''); this.fDefineSet(true, '{reflectionDecode}', ChunkUtils.decodeFunc(options.reflectionEncoding)); this.fDefineSet(true, '{reflectionCubemapDecode}', ChunkUtils.decodeFunc(options.reflectionCubemapEncoding)); this.fDefineSet(true, '{ambientDecode}', ChunkUtils.decodeFunc(options.ambientEncoding)); this._setupLightingDefines(hasAreaLights, options.clusteredLightingEnabled); } prepareShadowPass() { const { options } = this; const lightType = this.shaderPassInfo.lightType; const shadowType = this.shaderPassInfo.shadowType; const shadowInfo = shadowTypeInfo.get(shadowType); const usePerspectiveDepth = lightType === LIGHTTYPE_DIRECTIONAL || !shadowInfo.vsm && lightType === LIGHTTYPE_SPOT; this.fDefineSet(usePerspectiveDepth, 'PERSPECTIVE_DEPTH'); this.fDefineSet(true, 'LIGHT_TYPE', `${lightTypeNames[lightType]}`); this.fDefineSet(true, 'SHADOW_TYPE', `${shadowInfo.name}`); this.fDefineSet(options.alphaTest, 'LIT_ALPHA_TEST'); } generateFragmentShader(frontendDecl, frontendCode, lightingUv) { const options = this.options; this.includes.set('frontendDeclPS', frontendDecl ?? ''); this.includes.set('frontendCodePS', frontendCode ?? ''); if (options.pass === SHADER_PICK || options.pass === SHADER_PREPASS) ; else if (this.shadowPass) { this.prepareShadowPass(); } else { this.prepareForwardPass(lightingUv); } this.fshader = ` #include "litMainPS" `; } constructor(device, options, allowWGSL = true){ this.varyingsCode = ''; this.vDefines = new Map(); this.fDefines = new Map(); this.includes = new Map(); this.chunks = null; this.device = device; this.options = options; const userChunks = options.shaderChunks; this.shaderLanguage = device.isWebGPU && allowWGSL && (!userChunks || userChunks.useWGSL) ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL; if (device.isWebGPU && this.shaderLanguage === SHADERLANGUAGE_GLSL) { if (!device.hasTranspilers) ; } this.attributes = { vertex_position: SEMANTIC_POSITION }; if (options.userAttributes) { for (const [semantic, name] of Object.entries(options.userAttributes)){ this.attributes[name] = semantic; } } const engineChunks = ShaderChunks.get(device, this.shaderLanguage); this.chunks = new Map(engineChunks); if (userChunks) { const userChunkMap = this.shaderLanguage === SHADERLANGUAGE_GLSL ? userChunks.glsl : userChunks.wgsl; userChunkMap.forEach((chunk, chunkName)=>{ for(const a in builtinAttributes){ if (builtinAttributes.hasOwnProperty(a) && chunk.indexOf(a) >= 0) { this.attributes[a] = builtinAttributes[a]; } } this.chunks.set(chunkName, chunk); }); } this.shaderPassInfo = ShaderPass.get(this.device).getByIndex(options.pass); this.shadowPass = this.shaderPassInfo.isShadow; this.lighting = options.lights.length > 0 || options.dirLightMapEnabled || options.clusteredLightingEnabled; this.reflections = options.reflectionSource !== REFLECTIONSRC_NONE; this.needsNormal = this.lighting || this.reflections || options.useSpecular || options.ambientSH || options.useHeights || options.enableGGXSpecular || options.clusteredLightingEnabled && !this.shadowPass || options.useClearCoatNormals; this.needsNormal = this.needsNormal && !this.shadowPass; this.needsSceneColor = options.useDynamicRefraction; this.needsScreenSize = options.useDynamicRefraction; this.needsTransforms = options.useDynamicRefraction; this.vshader = null; this.fshader = null; } } const LitOptionsUtils = { generateKey (options) { return `lit${Object.keys(options).sort().map((key)=>{ if (key === 'shaderChunks') { return options.shaderChunks?.key ?? ''; } else if (key === 'lights') { return LitOptionsUtils.generateLightsKey(options); } return key + options[key]; }).join('\n')}`; }, generateLightsKey (options) { return `lights:${options.lights.map((light)=>{ return !options.clusteredLightingEnabled || light._type === LIGHTTYPE_DIRECTIONAL ? `${light.key},` : ''; }).join('')}`; } }; class StandardMaterialOptions { get pass() { return this.litOptions.pass; } constructor(){ this.defines = new Map(); this.forceUv1 = false; this.specularTint = false; this.metalnessTint = false; this.glossTint = false; this.emissiveEncoding = 'linear'; this.lightMapEncoding = 'linear'; this.vertexColorGamma = false; this.packedNormal = false; this.normalDetailPackedNormal = false; this.clearCoatPackedNormal = false; this.glossInvert = false; this.sheenGlossInvert = false; this.clearCoatGlossInvert = false; this.useAO = false; this.litOptions = new LitShaderOptions(); } } const _matTex2D = []; const buildPropertiesList = (options)=>{ return Object.keys(options).filter((key)=>key !== 'litOptions').sort(); }; class ShaderGeneratorStandard extends ShaderGenerator { generateKey(options) { let props; if (options === this.optionsContextMin) { if (!this.propsMin) this.propsMin = buildPropertiesList(options); props = this.propsMin; } else if (options === this.optionsContext) { if (!this.props) this.props = buildPropertiesList(options); props = this.props; } else { props = buildPropertiesList(options); } const definesHash = ShaderGenerator.definesHash(options.defines); const key = `standard:\n${definesHash}\n${props.map((prop)=>prop + options[prop]).join('\n')}${LitOptionsUtils.generateKey(options.litOptions)}`; return key; } _getUvSourceExpression(transformPropName, uVPropName, options) { const transformId = options[transformPropName]; const uvChannel = options[uVPropName]; const isMainPass = options.litOptions.pass === SHADER_FORWARD; let expression; if (isMainPass && options.litOptions.nineSlicedMode === SPRITE_RENDERMODE_SLICED) { expression = 'nineSlicedUv'; } else if (isMainPass && options.litOptions.nineSlicedMode === SPRITE_RENDERMODE_TILED) { expression = 'nineSlicedUv'; } else { if (transformId === 0) { expression = `vUv${uvChannel}`; } else { expression = `vUV${uvChannel}_${transformId}`; } if (options.heightMap && transformPropName !== 'heightMapTransform') { expression += ' + dUvOffset'; } } return expression; } _validateMapChunk(code, propName, chunkName, chunks) {} _addMapDefines(fDefines, propName, chunkName, options, chunks, mapping, encoding = null) { const mapPropName = `${propName}Map`; const propNameCaps = propName.toUpperCase(); const uVPropName = `${mapPropName}Uv`; const identifierPropName = `${mapPropName}Identifier`; const transformPropName = `${mapPropName}Transform`; const channelPropName = `${mapPropName}Channel`; const vertexColorChannelPropName = `${propName}VertexColorChannel`; const tintPropName = `${propName}Tint`; const vertexColorPropName = `${propName}VertexColor`; const detailModePropName = `${propName}Mode`; const invertName = `${propName}Invert`; const tintOption = options[tintPropName]; const vertexColorOption = options[vertexColorPropName]; const textureOption = options[mapPropName]; const textureIdentifier = options[identifierPropName]; const detailModeOption = options[detailModePropName]; const chunkCode = chunks.get(chunkName); if (textureOption) { fDefines.set(`STD_${propNameCaps}_TEXTURE`, ''); const uv = this._getUvSourceExpression(transformPropName, uVPropName, options); fDefines.set(`{STD_${propNameCaps}_TEXTURE_UV}`, uv); fDefines.set(`{STD_${propNameCaps}_TEXTURE_CHANNEL}`, options[channelPropName]); const textureId = `{STD_${propNameCaps}_TEXTURE_NAME}`; if (chunkCode.includes(textureId)) { let samplerName = `texture_${mapPropName}`; const alias = mapping[textureIdentifier]; if (alias) { samplerName = alias; } else { mapping[textureIdentifier] = samplerName; fDefines.set(`STD_${propNameCaps}_TEXTURE_ALLOCATE`, ''); } fDefines.set(textureId, samplerName); } if (encoding) { const textureDecode = options[channelPropName] === 'aaa' ? 'passThrough' : ChunkUtils.decodeFunc(encoding); fDefines.set(`{STD_${propNameCaps}_TEXTURE_DECODE}`, textureDecode); } } if (vertexColorOption) { fDefines.set(`STD_${propNameCaps}_VERTEX`, ''); fDefines.set(`{STD_${propNameCaps}_VERTEX_CHANNEL}`, options[vertexColorChannelPropName]); } if (detailModeOption) { fDefines.set(`{STD_${propNameCaps}_DETAILMODE}`, detailModeOption); } if (tintOption) { fDefines.set(`STD_${propNameCaps}_CONSTANT`, ''); } if (!!options[invertName]) { fDefines.set(`STD_${propNameCaps}_INVERT`, ''); } } _correctChannel(p, chan, _matTex2D) { if (_matTex2D[p] > 0) { if (_matTex2D[p] < chan.length) { return chan.substring(0, _matTex2D[p]); } else if (_matTex2D[p] > chan.length) { let str = chan; const chr = str.charAt(str.length - 1); const addLen = _matTex2D[p] - str.length; for(let i = 0; i < addLen; i++)str += chr; return str; } return chan; } } createVertexShader(litShader, options) { const useUv = []; const useUnmodifiedUv = []; const mapTransforms = []; const maxUvSets = 2; for(const p in _matTex2D){ const mapName = `${p}Map`; if (options[`${p}VertexColor`]) { const colorChannelName = `${p}VertexColorChannel`; options[colorChannelName] = this._correctChannel(p, options[colorChannelName], _matTex2D); } if (options[mapName]) { const channelName = `${mapName}Channel`; const transformName = `${mapName}Transform`; const uvName = `${mapName}Uv`; options[uvName] = Math.min(options[uvName], maxUvSets - 1); options[channelName] = this._correctChannel(p, options[channelName], _matTex2D); const uvSet = options[uvName]; useUv[uvSet] = true; useUnmodifiedUv[uvSet] = useUnmodifiedUv[uvSet] || options[mapName] && !options[transformName]; if (options[transformName]) { mapTransforms.push({ name: p, id: options[transformName], uv: options[uvName] }); } } } if (options.forceUv1) { useUv[1] = true; useUnmodifiedUv[1] = useUnmodifiedUv[1] !== undefined ? useUnmodifiedUv[1] : true; } litShader.generateVertexShader(useUv, useUnmodifiedUv, mapTransforms); } prepareFragmentDefines(options, fDefines, shaderPassInfo) { const fDefineSet = (condition, name, value = '')=>{ if (condition) { fDefines.set(name, value); } }; fDefineSet(options.lightMap, 'STD_LIGHTMAP', ''); fDefineSet(options.lightVertexColor, 'STD_LIGHT_VERTEX_COLOR', ''); fDefineSet(options.dirLightMap && options.litOptions.useSpecular, 'STD_LIGHTMAP_DIR', ''); fDefineSet(options.heightMap, 'STD_HEIGHT_MAP', ''); fDefineSet(options.useSpecularColor, 'STD_SPECULAR_COLOR', ''); fDefineSet(options.aoMap || options.aoVertexColor || options.useAO, 'STD_AO', ''); fDefineSet(true, 'STD_OPACITY_DITHER', ditherNames[shaderPassInfo.isForward ? options.litOptions.opacityDither : options.litOptions.opacityShadowDither]); } createShaderDefinition(device, options) { const shaderPassInfo = ShaderPass.get(device).getByIndex(options.litOptions.pass); const isForwardPass = shaderPassInfo.isForward; const litShader = new LitShader(device, options.litOptions); this.createVertexShader(litShader, options); const textureMapping = {}; options.litOptions.fresnelModel = options.litOptions.fresnelModel === 0 ? FRESNEL_SCHLICK : options.litOptions.fresnelModel; const fDefines = litShader.fDefines; this.prepareFragmentDefines(options, fDefines, shaderPassInfo); let lightingUv = ''; if (isForwardPass) { if (options.heightMap) { this._addMapDefines(fDefines, 'height', 'parallaxPS', options, litShader.chunks, textureMapping); } if (options.litOptions.blendType !== BLEND_NONE || options.litOptions.alphaTest || options.litOptions.alphaToCoverage || options.litOptions.opacityDither !== DITHER_NONE) { this._addMapDefines(fDefines, 'opacity', 'opacityPS', options, litShader.chunks, textureMapping); } if (litShader.needsNormal) { if (options.normalMap || options.clearCoatNormalMap) { if (!options.litOptions.hasTangents) { const baseName = options.normalMap ? 'normalMap' : 'clearCoatNormalMap'; lightingUv = this._getUvSourceExpression(`${baseName}Transform`, `${baseName}Uv`, options); } } this._addMapDefines(fDefines, 'normalDetail', 'normalMapPS', options, litShader.chunks, textureMapping, options.normalDetailPackedNormal ? 'xy' : 'xyz'); this._addMapDefines(fDefines, 'normal', 'normalMapPS', options, litShader.chunks, textureMapping, options.packedNormal ? 'xy' : 'xyz'); } if (options.diffuseDetail) { this._addMapDefines(fDefines, 'diffuseDetail', 'diffusePS', options, litShader.chunks, textureMapping, options.diffuseDetailEncoding); } this._addMapDefines(fDefines, 'diffuse', 'diffusePS', options, litShader.chunks, textureMapping, options.diffuseEncoding); if (options.litOptions.useRefraction) { this._addMapDefines(fDefines, 'refraction', 'transmissionPS', options, litShader.chunks, textureMapping); this._addMapDefines(fDefines, 'thickness', 'thicknessPS', options, litShader.chunks, textureMapping); } if (options.litOptions.useIridescence) { this._addMapDefines(fDefines, 'iridescence', 'iridescencePS', options, litShader.chunks, textureMapping); this._addMapDefines(fDefines, 'iridescenceThickness', 'iridescenceThicknessPS', options, litShader.chunks, textureMapping); } if (litShader.lighting && options.litOptions.useSpecular || litShader.reflections) { if (options.litOptions.useSheen) { this._addMapDefines(fDefines, 'sheen', 'sheenPS', options, litShader.chunks, textureMapping, options.sheenEncoding); this._addMapDefines(fDefines, 'sheenGloss', 'sheenGlossPS', options, litShader.chunks, textureMapping); } if (options.litOptions.useMetalness) { this._addMapDefines(fDefines, 'metalness', 'metalnessPS', options, litShader.chunks, textureMapping); this._addMapDefines(fDefines, 'ior', 'iorPS', options, litShader.chunks, textureMapping); } if (options.litOptions.useSpecularityFactor) { this._addMapDefines(fDefines, 'specularityFactor', 'specularityFactorPS', options, litShader.chunks, textureMapping); } if (options.useSpecularColor) { this._addMapDefines(fDefines, 'specular', 'specularPS', options, litShader.chunks, textureMapping, options.specularEncoding); } this._addMapDefines(fDefines, 'gloss', 'glossPS', options, litShader.chunks, textureMapping); } if (options.aoDetail) { this._addMapDefines(fDefines, 'aoDetail', 'aoPS', options, litShader.chunks, textureMapping); } if (options.aoMap || options.aoVertexColor || options.useAO) { this._addMapDefines(fDefines, 'ao', 'aoPS', options, litShader.chunks, textureMapping); } this._addMapDefines(fDefines, 'emissive', 'emissivePS', options, litShader.chunks, textureMapping, options.emissiveEncoding); if (options.litOptions.useClearCoat) { this._addMapDefines(fDefines, 'clearCoat', 'clearCoatPS', options, litShader.chunks, textureMapping); this._addMapDefines(fDefines, 'clearCoatGloss', 'clearCoatGlossPS', options, litShader.chunks, textureMapping); this._addMapDefines(fDefines, 'clearCoatNormal', 'clearCoatNormalPS', options, litShader.chunks, textureMapping, options.clearCoatPackedNormal ? 'xy' : 'xyz'); } if (options.litOptions.enableGGXSpecular) { this._addMapDefines(fDefines, 'anisotropy', 'anisotropyPS', options, litShader.chunks, textureMapping); } if (options.lightMap || options.lightVertexColor) { this._addMapDefines(fDefines, 'light', 'lightmapPS', options, litShader.chunks, textureMapping, options.lightMapEncoding); } } else { const opacityShadowDither = options.litOptions.opacityShadowDither; if (options.litOptions.alphaTest || opacityShadowDither) { this._addMapDefines(fDefines, 'opacity', 'opacityPS', options, litShader.chunks, textureMapping); } } litShader.generateFragmentShader(litShader.chunks.get('stdDeclarationPS'), litShader.chunks.get('stdFrontEndPS'), lightingUv); const includes = MapUtils.merge(litShader.chunks, litShader.includes); const vDefines = litShader.vDefines; options.defines.forEach((value, key)=>vDefines.set(key, value)); options.defines.forEach((value, key)=>fDefines.set(key, value)); const definition = ShaderDefinitionUtils.createDefinition(device, { name: 'StandardShader', attributes: litShader.attributes, shaderLanguage: litShader.shaderLanguage, vertexCode: litShader.vshader, fragmentCode: litShader.fshader, vertexIncludes: includes, fragmentIncludes: includes, fragmentDefines: fDefines, vertexDefines: vDefines }); if (litShader.shaderPassInfo.isForward) { definition.tag = SHADERTAG_MATERIAL; } return definition; } constructor(...args){ super(...args), this.optionsContext = new StandardMaterialOptions(), this.optionsContextMin = new StandardMaterialOptions(); } } const standard = new ShaderGeneratorStandard(); const arraysEqual = (a, b)=>{ if (a.length !== b.length) { return false; } for(let i = 0; i < a.length; ++i){ if (a[i] !== b[i]) { return false; } } return true; }; const notWhite = (color)=>{ return color.r !== 1 || color.g !== 1 || color.b !== 1; }; const notBlack = (color)=>{ return color.r !== 0 || color.g !== 0 || color.b !== 0; }; class StandardMaterialOptionsBuilder { updateMinRef(options, scene, stdMat, objDefs, pass, sortedLights) { this._updateSharedOptions(options, scene, stdMat, objDefs, pass); this._updateMinOptions(options, stdMat, pass); this._updateUVOptions(options, stdMat, objDefs, true); } updateRef(options, scene, cameraShaderParams, stdMat, objDefs, pass, sortedLights) { this._updateSharedOptions(options, scene, stdMat, objDefs, pass); this._updateEnvOptions(options, stdMat, scene, cameraShaderParams); this._updateMaterialOptions(options, stdMat, scene); options.litOptions.hasTangents = objDefs && (objDefs & SHADERDEF_TANGENTS) !== 0; this._updateLightOptions(options, scene, stdMat, objDefs, sortedLights); this._updateUVOptions(options, stdMat, objDefs, false, cameraShaderParams); } _updateSharedOptions(options, scene, stdMat, objDefs, pass) { options.forceUv1 = stdMat.forceUv1; if (stdMat.userAttributes) { options.litOptions.userAttributes = Object.fromEntries(stdMat.userAttributes.entries()); } options.litOptions.shaderChunks = stdMat.shaderChunks; options.litOptions.pass = pass; options.litOptions.alphaTest = stdMat.alphaTest > 0; options.litOptions.blendType = stdMat.blendType; options.litOptions.screenSpace = objDefs && (objDefs & SHADERDEF_SCREENSPACE) !== 0; options.litOptions.skin = objDefs && (objDefs & SHADERDEF_SKIN) !== 0; options.litOptions.batch = objDefs && (objDefs & SHADERDEF_BATCH) !== 0; options.litOptions.useInstancing = objDefs && (objDefs & SHADERDEF_INSTANCING) !== 0; options.litOptions.useMorphPosition = objDefs && (objDefs & SHADERDEF_MORPH_POSITION) !== 0; options.litOptions.useMorphNormal = objDefs && (objDefs & SHADERDEF_MORPH_NORMAL) !== 0; options.litOptions.useMorphTextureBasedInt = objDefs && (objDefs & SHADERDEF_MORPH_TEXTURE_BASED_INT) !== 0; options.litOptions.nineSlicedMode = stdMat.nineSlicedMode || 0; if (scene.clusteredLightingEnabled && stdMat.useLighting) { options.litOptions.clusteredLightingEnabled = true; options.litOptions.clusteredLightingCookiesEnabled = scene.lighting.cookiesEnabled; options.litOptions.clusteredLightingShadowsEnabled = scene.lighting.shadowsEnabled; options.litOptions.clusteredLightingShadowType = scene.lighting.shadowType; options.litOptions.clusteredLightingAreaLightsEnabled = scene.lighting.areaLightsEnabled; } else { options.litOptions.clusteredLightingEnabled = false; options.litOptions.clusteredLightingCookiesEnabled = false; options.litOptions.clusteredLightingShadowsEnabled = false; options.litOptions.clusteredLightingAreaLightsEnabled = false; } } _updateUVOptions(options, stdMat, objDefs, minimalOptions, cameraShaderParams) { let hasUv0 = false; let hasUv1 = false; let hasVcolor = false; if (objDefs) { hasUv0 = (objDefs & SHADERDEF_UV0) !== 0; hasUv1 = (objDefs & SHADERDEF_UV1) !== 0; hasVcolor = (objDefs & SHADERDEF_VCOLOR) !== 0; } options.litOptions.vertexColors = false; this._mapXForms = []; const uniqueTextureMap = {}; for(const p in _matTex2D){ this._updateTexOptions(options, stdMat, p, hasUv0, hasUv1, hasVcolor, minimalOptions, uniqueTextureMap); } this._mapXForms = null; options.litOptions.ssao = cameraShaderParams?.ssaoEnabled; options.useAO = options.litOptions.ssao; options.litOptions.lightMapEnabled = options.lightMap; options.litOptions.dirLightMapEnabled = options.dirLightMap; options.litOptions.useHeights = options.heightMap; options.litOptions.useNormals = options.normalMap; options.litOptions.useClearCoatNormals = options.clearCoatNormalMap; options.litOptions.useAo = options.aoMap || options.aoVertexColor || options.litOptions.ssao; options.litOptions.diffuseMapEnabled = options.diffuseMap; } _updateTexOptions(options, stdMat, p, hasUv0, hasUv1, hasVcolor, minimalOptions, uniqueTextureMap) { const isOpacity = p === 'opacity'; if (!minimalOptions || isOpacity) { const mname = `${p}Map`; const vname = `${p}VertexColor`; const vcname = `${p}VertexColorChannel`; const cname = `${mname}Channel`; const tname = `${mname}Transform`; const uname = `${mname}Uv`; const iname = `${mname}Identifier`; if (p !== 'light') { options[mname] = false; options[iname] = undefined; options[cname] = ''; options[tname] = 0; options[uname] = 0; } options[vname] = false; options[vcname] = ''; if (isOpacity && stdMat.blendType === BLEND_NONE && stdMat.alphaTest === 0.0 && !stdMat.alphaToCoverage && stdMat.opacityDither === DITHER_NONE) { return; } if (p !== 'height' && stdMat[vname]) { if (hasVcolor) { options[vname] = stdMat[vname]; options[vcname] = stdMat[vcname]; options.litOptions.vertexColors = true; } } if (stdMat[mname]) { let allow = true; if (stdMat[uname] === 0 && !hasUv0) allow = false; if (stdMat[uname] === 1 && !hasUv1) allow = false; if (allow) { const mapId = stdMat[mname].id; let identifier = uniqueTextureMap[mapId]; if (identifier === undefined) { uniqueTextureMap[mapId] = p; identifier = p; } options[mname] = !!stdMat[mname]; options[iname] = identifier; options[tname] = this._getMapTransformID(stdMat.getUniform(tname), stdMat[uname]); options[cname] = stdMat[cname]; options[uname] = stdMat[uname]; } } } } _updateMinOptions(options, stdMat, pass) { const isPrepass = pass === SHADER_PREPASS; options.litOptions.opacityShadowDither = isPrepass ? stdMat.opacityDither : stdMat.opacityShadowDither; options.litOptions.linearDepth = isPrepass; options.litOptions.lights = []; } _updateMaterialOptions(options, stdMat, scene) { const useSpecular = !!(stdMat.useMetalness || stdMat.specularMap || stdMat.sphereMap || stdMat.cubeMap || notBlack(stdMat.specular) || stdMat.specularityFactor > 0 && stdMat.useMetalness || stdMat.enableGGXSpecular || stdMat.clearCoat > 0); const useSpecularColor = !stdMat.useMetalness || stdMat.useMetalnessSpecularColor; const specularTint = useSpecular && (stdMat.specularTint || !stdMat.specularMap && !stdMat.specularVertexColor) && notWhite(stdMat.specular); const specularityFactorTint = useSpecular && stdMat.useMetalnessSpecularColor && (stdMat.specularityFactorTint || stdMat.specularityFactor < 1 && !stdMat.specularityFactorMap); const isPackedNormalMap = (texture)=>texture ? texture.format === PIXELFORMAT_DXT5 || texture.type === TEXTURETYPE_SWIZZLEGGGR : false; const equalish = (a, b)=>Math.abs(a - b) < 1e-4; options.specularTint = specularTint; options.specularityFactorTint = specularityFactorTint; options.metalnessTint = stdMat.useMetalness && stdMat.metalness < 1; options.glossTint = true; options.diffuseEncoding = stdMat.diffuseMap?.encoding; options.diffuseDetailEncoding = stdMat.diffuseDetailMap?.encoding; options.emissiveEncoding = stdMat.emissiveMap?.encoding; options.lightMapEncoding = stdMat.lightMap?.encoding; options.packedNormal = isPackedNormalMap(stdMat.normalMap); options.refractionTint = !equalish(stdMat.refraction, 1.0); options.refractionIndexTint = !equalish(stdMat.refractionIndex, 1.0 / 1.5); options.thicknessTint = stdMat.useDynamicRefraction && stdMat.thickness !== 1.0; options.specularEncoding = stdMat.specularMap?.encoding; options.sheenEncoding = stdMat.sheenMap?.encoding; options.aoMapUv = stdMat.aoUvSet; options.aoDetail = !!stdMat.aoDetailMap; options.diffuseDetail = !!stdMat.diffuseDetailMap; options.normalDetail = !!stdMat.normalMap; options.normalDetailPackedNormal = isPackedNormalMap(stdMat.normalDetailMap); options.diffuseDetailMode = stdMat.diffuseDetailMode; options.aoDetailMode = stdMat.aoDetailMode; options.clearCoatGloss = !!stdMat.clearCoatGloss; options.clearCoatPackedNormal = isPackedNormalMap(stdMat.clearCoatNormalMap); options.iorTint = !equalish(stdMat.refractionIndex, 1.0 / 1.5); if (scene.forcePassThroughSpecular) { options.specularEncoding = 'linear'; options.sheenEncoding = 'linear'; } options.iridescenceTint = stdMat.iridescence !== 1.0; options.glossInvert = stdMat.glossInvert; options.sheenGlossInvert = stdMat.sheenGlossInvert; options.clearCoatGlossInvert = stdMat.clearCoatGlossInvert; options.useSpecularColor = useSpecularColor; options.litOptions.separateAmbient = false; options.litOptions.pixelSnap = stdMat.pixelSnap; options.litOptions.ambientSH = !!stdMat.ambientSH; options.litOptions.twoSidedLighting = stdMat.twoSidedLighting; options.litOptions.occludeSpecular = stdMat.occludeSpecular; options.litOptions.occludeSpecularFloat = stdMat.occludeSpecularIntensity !== 1.0; options.litOptions.useMsdf = !!stdMat.msdfMap; options.litOptions.msdfTextAttribute = !!stdMat.msdfTextAttribute; options.litOptions.alphaToCoverage = stdMat.alphaToCoverage; options.litOptions.opacityFadesSpecular = stdMat.opacityFadesSpecular; options.litOptions.opacityDither = stdMat.opacityDither; options.litOptions.cubeMapProjection = stdMat.cubeMapProjection; options.litOptions.occludeDirect = stdMat.occludeDirect; options.litOptions.useSpecular = useSpecular; options.litOptions.useSpecularityFactor = (specularityFactorTint || !!stdMat.specularityFactorMap) && stdMat.useMetalnessSpecularColor; options.litOptions.enableGGXSpecular = stdMat.enableGGXSpecular; options.litOptions.useAnisotropy = stdMat.enableGGXSpecular && (stdMat.anisotropyIntensity > 0 || !!stdMat.anisotropyMap); options.litOptions.fresnelModel = stdMat.fresnelModel; options.litOptions.useRefraction = (stdMat.refraction || !!stdMat.refractionMap) && (stdMat.useDynamicRefraction || options.litOptions.reflectionSource !== REFLECTIONSRC_NONE); options.litOptions.useClearCoat = !!stdMat.clearCoat; options.litOptions.useSheen = stdMat.useSheen; options.litOptions.useIridescence = stdMat.useIridescence && stdMat.iridescence !== 0.0; options.litOptions.useMetalness = stdMat.useMetalness; options.litOptions.useDynamicRefraction = stdMat.useDynamicRefraction; options.litOptions.dispersion = stdMat.dispersion > 0; options.litOptions.shadowCatcher = stdMat.shadowCatcher; options.litOptions.useVertexColorGamma = stdMat.vertexColorGamma; } _updateEnvOptions(options, stdMat, scene, cameraShaderParams) { options.litOptions.fog = stdMat.useFog ? cameraShaderParams.fog : FOG_NONE; options.litOptions.gamma = cameraShaderParams.shaderOutputGamma; options.litOptions.toneMap = stdMat.useTonemap ? cameraShaderParams.toneMapping : TONEMAP_NONE; let usingSceneEnv = false; if (stdMat.envAtlas && stdMat.cubeMap) { options.litOptions.reflectionSource = REFLECTIONSRC_ENVATLASHQ; options.litOptions.reflectionEncoding = stdMat.envAtlas.encoding; options.litOptions.reflectionCubemapEncoding = stdMat.cubeMap.encoding; } else if (stdMat.envAtlas) { options.litOptions.reflectionSource = REFLECTIONSRC_ENVATLAS; options.litOptions.reflectionEncoding = stdMat.envAtlas.encoding; } else if (stdMat.cubeMap) { options.litOptions.reflectionSource = REFLECTIONSRC_CUBEMAP; options.litOptions.reflectionEncoding = stdMat.cubeMap.encoding; } else if (stdMat.sphereMap) { options.litOptions.reflectionSource = REFLECTIONSRC_SPHEREMAP; options.litOptions.reflectionEncoding = stdMat.sphereMap.encoding; } else if (stdMat.useSkybox && scene.envAtlas && scene.skybox) { options.litOptions.reflectionSource = REFLECTIONSRC_ENVATLASHQ; options.litOptions.reflectionEncoding = scene.envAtlas.encoding; options.litOptions.reflectionCubemapEncoding = scene.skybox.encoding; usingSceneEnv = true; } else if (stdMat.useSkybox && scene.envAtlas) { options.litOptions.reflectionSource = REFLECTIONSRC_ENVATLAS; options.litOptions.reflectionEncoding = scene.envAtlas.encoding; usingSceneEnv = true; } else if (stdMat.useSkybox && scene.skybox) { options.litOptions.reflectionSource = REFLECTIONSRC_CUBEMAP; options.litOptions.reflectionEncoding = scene.skybox.encoding; usingSceneEnv = true; } else { options.litOptions.reflectionSource = REFLECTIONSRC_NONE; options.litOptions.reflectionEncoding = null; } if (stdMat.ambientSH) { options.litOptions.ambientSource = AMBIENTSRC_AMBIENTSH; options.litOptions.ambientEncoding = null; } else { const envAtlas = stdMat.envAtlas || (stdMat.useSkybox && scene.envAtlas ? scene.envAtlas : null); if (envAtlas && !stdMat.sphereMap) { options.litOptions.ambientSource = AMBIENTSRC_ENVALATLAS; options.litOptions.ambientEncoding = envAtlas.encoding; } else { options.litOptions.ambientSource = AMBIENTSRC_CONSTANT; options.litOptions.ambientEncoding = null; } } options.litOptions.skyboxIntensity = usingSceneEnv; options.litOptions.useCubeMapRotation = usingSceneEnv && scene._skyboxRotationShaderInclude; } _updateLightOptions(options, scene, stdMat, objDefs, sortedLights) { options.lightMap = false; options.lightMapChannel = ''; options.lightMapUv = 0; options.lightMapTransform = 0; options.litOptions.lightMapWithoutAmbient = false; options.dirLightMap = false; if (objDefs) { options.litOptions.noShadow = (objDefs & SHADERDEF_NOSHADOW) !== 0; if ((objDefs & SHADERDEF_LM) !== 0) { options.lightMapEncoding = scene.lightmapPixelFormat === PIXELFORMAT_RGBA8 ? 'rgbm' : 'linear'; options.lightMap = true; options.lightMapChannel = 'rgb'; options.lightMapUv = 1; options.lightMapTransform = 0; options.litOptions.lightMapWithoutAmbient = !stdMat.lightMap; if ((objDefs & SHADERDEF_DIRLM) !== 0) { options.dirLightMap = true; } if ((objDefs & SHADERDEF_LMAMBIENT) !== 0) { options.litOptions.lightMapWithoutAmbient = false; } } } if (stdMat.useLighting) { const lightsFiltered = []; const mask = objDefs ? objDefs >> 16 : MASK_AFFECT_DYNAMIC; options.litOptions.lightMaskDynamic = !!(mask & MASK_AFFECT_DYNAMIC); if (sortedLights) { LitMaterialOptionsBuilder.collectLights(LIGHTTYPE_DIRECTIONAL, sortedLights[LIGHTTYPE_DIRECTIONAL], lightsFiltered, mask); if (!scene.clusteredLightingEnabled) { LitMaterialOptionsBuilder.collectLights(LIGHTTYPE_OMNI, sortedLights[LIGHTTYPE_OMNI], lightsFiltered, mask); LitMaterialOptionsBuilder.collectLights(LIGHTTYPE_SPOT, sortedLights[LIGHTTYPE_SPOT], lightsFiltered, mask); } } options.litOptions.lights = lightsFiltered; } else { options.litOptions.lights = []; } if (options.litOptions.lights.length === 0 && !scene.clusteredLightingEnabled) { options.litOptions.noShadow = true; } } _getMapTransformID(xform, uv) { if (!xform) return 0; let xforms = this._mapXForms[uv]; if (!xforms) { xforms = []; this._mapXForms[uv] = xforms; } for(let i = 0; i < xforms.length; i++){ if (arraysEqual(xforms[i][0].value, xform[0].value) && arraysEqual(xforms[i][1].value, xform[1].value)) { return i + 1; } } return xforms.push(xform); } constructor(){ this._mapXForms = null; } } function _textureParameter(name, channel = true, vertexColor = true) { const result = {}; result[`${name}Map`] = 'texture'; result[`${name}MapTiling`] = 'vec2'; result[`${name}MapOffset`] = 'vec2'; result[`${name}MapRotation`] = 'number'; result[`${name}MapUv`] = 'number'; if (channel) { result[`${name}MapChannel`] = 'string'; if (vertexColor) { result[`${name}VertexColor`] = 'boolean'; result[`${name}VertexColorChannel`] = 'string'; } } return result; } const standardMaterialParameterTypes = { name: 'string', chunks: 'chunks', mappingFormat: 'string', _engine: 'boolean', ambient: 'rgb', ..._textureParameter('ao'), ..._textureParameter('aoDetail', true, false), aoDetailMode: 'string', aoIntensity: 'number', diffuse: 'rgb', ..._textureParameter('diffuse'), ..._textureParameter('diffuseDetail', true, false), diffuseDetailMode: 'string', vertexColorGamma: 'boolean', specular: 'rgb', specularTint: 'boolean', ..._textureParameter('specular'), occludeSpecular: 'enum:occludeSpecular', specularityFactor: 'number', specularityFactorTint: 'boolean', ..._textureParameter('specularityFactor'), useMetalness: 'boolean', metalness: 'number', enableGGXSpecular: 'boolean', metalnessTint: 'boolean', ..._textureParameter('metalness'), useMetalnessSpecularColor: 'boolean', anisotropyIntensity: 'number', anisotropyRotation: 'number', ..._textureParameter('anisotropy'), shininess: 'number', gloss: 'number', glossInvert: 'boolean', ..._textureParameter('gloss'), clearCoat: 'number', ..._textureParameter('clearCoat'), clearCoatGloss: 'number', clearCoatGlossInvert: 'boolean', ..._textureParameter('clearCoatGloss'), clearCoatBumpiness: 'number', ..._textureParameter('clearCoatNormal', false), useSheen: 'boolean', sheen: 'rgb', ..._textureParameter('sheen'), sheenGloss: 'number', sheenGlossInvert: 'boolean', ..._textureParameter('sheenGloss'), fresnelModel: 'number', emissive: 'rgb', ..._textureParameter('emissive'), emissiveIntensity: 'number', ..._textureParameter('normal', false), bumpiness: 'number', ..._textureParameter('normalDetail', false), normalDetailMapBumpiness: 'number', ..._textureParameter('height', true, false), heightMapFactor: 'number', alphaToCoverage: 'boolean', alphaTest: 'number', alphaFade: 'number', opacity: 'number', ..._textureParameter('opacity'), opacityFadesSpecular: 'boolean', opacityDither: 'string', opacityShadowDither: 'string', reflectivity: 'number', refraction: 'number', refractionTint: 'boolean', ..._textureParameter('refraction'), refractionIndex: 'number', dispersion: 'number', thickness: 'number', thicknessTint: 'boolean', ..._textureParameter('thickness'), attenuation: 'rgb', attenuationDistance: 'number', useDynamicRefraction: 'boolean', sphereMap: 'texture', cubeMap: 'cubemap', cubeMapProjection: 'number', cubeMapProjectionBox: 'boundingbox', useIridescence: 'boolean', iridescence: 'number', iridescenceTint: 'boolean', ..._textureParameter('iridescence'), iridescenceThicknessTint: 'boolean', iridescenceThicknessMin: 'number', iridescenceThicknessMax: 'number', iridescenceRefractionIndex: 'number', ..._textureParameter('iridescenceThickness'), ..._textureParameter('light'), depthTest: 'boolean', depthFunc: 'enum:depthFunc', depthWrite: 'boolean', depthBias: 'number', slopeDepthBias: 'number', cull: 'enum:cull', blendType: 'enum:blendType', useFog: 'boolean', useLighting: 'boolean', useSkybox: 'boolean', useTonemap: 'boolean', envAtlas: 'texture', twoSidedLighting: 'boolean', shadowCatcher: 'boolean' }; const standardMaterialTextureParameters = []; for(const key in standardMaterialParameterTypes){ const type = standardMaterialParameterTypes[key]; if (type === 'texture') { standardMaterialTextureParameters.push(key); } } const standardMaterialCubemapParameters = []; for(const key in standardMaterialParameterTypes){ const type = standardMaterialParameterTypes[key]; if (type === 'cubemap') { standardMaterialCubemapParameters.push(key); } } const standardMaterialRemovedParameters = { aoMapVertexColor: 'boolean', diffuseMapTint: 'boolean', diffuseMapVertexColor: 'boolean', emissiveMapTint: 'boolean', emissiveMapVertexColor: 'boolean', glossMapVertexColor: 'boolean', metalnessMapVertexColor: 'boolean', opacityMapVertexColor: 'boolean', specularAntialias: 'boolean', specularMapTint: 'boolean', specularMapVertexColor: 'boolean', ambientTint: 'boolean', emissiveTint: 'boolean', diffuseTint: 'boolean', sheenTint: 'boolean', conserveEnergy: 'boolean', useGamma: 'boolean', useGammaTonemap: 'boolean', sheenGlossTint: 'boolean', anisotropy: 'boolean' }; const _props = {}; const _uniforms = {}; let _params = new Set(); const _tempColor$2 = new Color(); class StandardMaterial extends Material { reset() { Object.keys(_props).forEach((name)=>{ this[`_${name}`] = _props[name].value(); }); this._uniformCache = {}; } copy(source) { super.copy(source); Object.keys(_props).forEach((k)=>{ this[k] = source[k]; }); this.userAttributes = new Map(source.userAttributes); return this; } setAttribute(name, semantic) { this.userAttributes.set(semantic, name); } _setParameter(name, value) { _params.add(name); this.setParameter(name, value); } _setParameters(parameters) { parameters.forEach((v)=>{ this._setParameter(v.name, v.value); }); } _processParameters(paramsName) { const prevParams = this[paramsName]; prevParams.forEach((param)=>{ if (!_params.has(param)) { delete this.parameters[param]; } }); this[paramsName] = _params; _params = prevParams; _params.clear(); } _updateMap(p) { const mname = `${p}Map`; const map = this[mname]; if (map) { this._setParameter(`texture_${mname}`, map); const tname = `${mname}Transform`; const uniform = this.getUniform(tname); if (uniform) { this._setParameters(uniform); } } } _allocUniform(name, allocFunc) { let uniform = this._uniformCache[name]; if (!uniform) { uniform = allocFunc(); this._uniformCache[name] = uniform; } return uniform; } getUniform(name, device, scene) { return _uniforms[name](this, device, scene); } updateUniforms(device, scene) { const getUniform = (name)=>{ return this.getUniform(name, device, scene); }; this._setParameter('material_ambient', getUniform('ambient')); this._setParameter('material_diffuse', getUniform('diffuse')); this._setParameter('material_aoIntensity', this.aoIntensity); if (this.useMetalness) { if (!this.metalnessMap || this.metalness < 1) { this._setParameter('material_metalness', this.metalness); } if (!this.specularMap || this.specularTint) { this._setParameter('material_specular', getUniform('specular')); } if (!this.specularityFactorMap || this.specularityFactorTint) { this._setParameter('material_specularityFactor', this.specularityFactor); } this._setParameter('material_sheen', getUniform('sheen')); this._setParameter('material_sheenGloss', this.sheenGloss); this._setParameter('material_refractionIndex', this.refractionIndex); } else { if (!this.specularMap || this.specularTint) { this._setParameter('material_specular', getUniform('specular')); } } if (this.enableGGXSpecular) { this._setParameter('material_anisotropyIntensity', this.anisotropyIntensity); this._setParameter('material_anisotropyRotation', [ Math.cos(this.anisotropyRotation * math.DEG_TO_RAD), Math.sin(this.anisotropyRotation * math.DEG_TO_RAD) ]); } if (this.clearCoat > 0) { this._setParameter('material_clearCoat', this.clearCoat); this._setParameter('material_clearCoatGloss', this.clearCoatGloss); this._setParameter('material_clearCoatBumpiness', this.clearCoatBumpiness); } this._setParameter('material_gloss', this.gloss); this._setParameter('material_emissive', getUniform('emissive')); this._setParameter('material_emissiveIntensity', this.emissiveIntensity); if (this.refraction > 0) { this._setParameter('material_refraction', this.refraction); } if (this.dispersion > 0) { this._setParameter('material_dispersion', this.dispersion); } if (this.useDynamicRefraction) { this._setParameter('material_thickness', this.thickness); this._setParameter('material_attenuation', getUniform('attenuation')); this._setParameter('material_invAttenuationDistance', this.attenuationDistance === 0 ? 0 : 1.0 / this.attenuationDistance); } if (this.useIridescence) { this._setParameter('material_iridescence', this.iridescence); this._setParameter('material_iridescenceRefractionIndex', this.iridescenceRefractionIndex); this._setParameter('material_iridescenceThicknessMin', this.iridescenceThicknessMin); this._setParameter('material_iridescenceThicknessMax', this.iridescenceThicknessMax); } this._setParameter('material_opacity', this.opacity); if (this.opacityFadesSpecular === false) { this._setParameter('material_alphaFade', this.alphaFade); } if (this.occludeSpecular) { this._setParameter('material_occludeSpecularIntensity', this.occludeSpecularIntensity); } if (this.cubeMapProjection === CUBEPROJ_BOX) { this._setParameter(getUniform('cubeMapProjectionBox')); } for(const p in _matTex2D){ this._updateMap(p); } if (this.ambientSH) { this._setParameter('ambientSH[0]', this.ambientSH); } if (this.normalMap) { this._setParameter('material_bumpiness', this.bumpiness); } if (this.normalMap && this.normalDetailMap) { this._setParameter('material_normalDetailMapBumpiness', this.normalDetailMapBumpiness); } if (this.heightMap) { this._setParameter('material_heightMapFactor', getUniform('heightMapFactor')); } if (this.envAtlas && this.cubeMap) { this._setParameter('texture_envAtlas', this.envAtlas); this._setParameter('texture_cubeMap', this.cubeMap); } else if (this.envAtlas) { this._setParameter('texture_envAtlas', this.envAtlas); } else if (this.cubeMap) { this._setParameter('texture_cubeMap', this.cubeMap); } else if (this.sphereMap) { this._setParameter('texture_sphereMap', this.sphereMap); } this._setParameter('material_reflectivity', this.reflectivity); this._processParameters('_activeParams'); super.updateUniforms(device, scene); } updateEnvUniforms(device, scene) { const hasLocalEnvOverride = this.envAtlas || this.cubeMap || this.sphereMap; if (!hasLocalEnvOverride && this.useSkybox) { if (scene.envAtlas && scene.skybox) { this._setParameter('texture_envAtlas', scene.envAtlas); this._setParameter('texture_cubeMap', scene.skybox); } else if (scene.envAtlas) { this._setParameter('texture_envAtlas', scene.envAtlas); } else if (scene.skybox) { this._setParameter('texture_cubeMap', scene.skybox); } } this._processParameters('_activeLightingParams'); } getShaderVariant(params) { const { device, scene, pass, objDefs, sortedLights, cameraShaderParams } = params; this.updateEnvUniforms(device, scene); const shaderPassInfo = ShaderPass.get(device).getByIndex(pass); const minimalOptions = pass === SHADER_PICK || pass === SHADER_PREPASS || shaderPassInfo.isShadow; let options = minimalOptions ? standard.optionsContextMin : standard.optionsContext; options.defines = ShaderUtils.getCoreDefines(this, params); if (minimalOptions) { this.shaderOptBuilder.updateMinRef(options, scene, this, objDefs, pass, sortedLights); } else { this.shaderOptBuilder.updateRef(options, scene, cameraShaderParams, this, objDefs, pass, sortedLights); } if (!this.useFog) options.defines.set('FOG', 'NONE'); options.defines.set('TONEMAP', tonemapNames[options.litOptions.toneMap]); if (this.onUpdateShader) { options = this.onUpdateShader(options); } const processingOptions = new ShaderProcessorOptions(params.viewUniformFormat, params.viewBindGroupFormat, params.vertexFormat); const library = getProgramLibrary(device); library.register('standard', standard); const shader = library.getProgram('standard', options, processingOptions, this.userId); this._dirtyShader = false; return shader; } destroy() { for(const asset in this._assetReferences){ this._assetReferences[asset]._unbind(); } this._assetReferences = null; super.destroy(); } constructor(){ super(), this.userAttributes = new Map(); this._assetReferences = {}; this._activeParams = new Set(); this._activeLightingParams = new Set(); this.shaderOptBuilder = new StandardMaterialOptionsBuilder(); this.reset(); } } StandardMaterial.TEXTURE_PARAMETERS = standardMaterialTextureParameters; StandardMaterial.CUBEMAP_PARAMETERS = standardMaterialCubemapParameters; const defineUniform = (name, getUniformFunc)=>{ _uniforms[name] = getUniformFunc; }; const definePropInternal = (name, constructorFunc, setterFunc, getterFunc)=>{ Object.defineProperty(StandardMaterial.prototype, name, { get: getterFunc || function() { return this[`_${name}`]; }, set: setterFunc }); _props[name] = { value: constructorFunc }; }; const defineValueProp = (prop)=>{ const internalName = `_${prop.name}`; const dirtyShaderFunc = prop.dirtyShaderFunc || (()=>true); const setterFunc = function(value) { const oldValue = this[internalName]; if (oldValue !== value) { this._dirtyShader = this._dirtyShader || dirtyShaderFunc(oldValue, value); this[internalName] = value; } }; definePropInternal(prop.name, ()=>prop.defaultValue, setterFunc, prop.getterFunc); }; const defineAggProp = (prop)=>{ const internalName = `_${prop.name}`; const dirtyShaderFunc = prop.dirtyShaderFunc || (()=>true); const setterFunc = function(value) { const oldValue = this[internalName]; if (!oldValue.equals(value)) { this._dirtyShader = this._dirtyShader || dirtyShaderFunc(oldValue, value); this[internalName] = oldValue.copy(value); } }; definePropInternal(prop.name, ()=>prop.defaultValue.clone(), setterFunc, prop.getterFunc); }; const defineProp = (prop)=>{ return prop.defaultValue && prop.defaultValue.clone ? defineAggProp(prop) : defineValueProp(prop); }; function _defineTex2D(name, channel = 'rgb', vertexColor = true, uv = 0) { _matTex2D[name] = channel.length || -1; defineProp({ name: `${name}Map`, defaultValue: null, dirtyShaderFunc: (oldValue, newValue)=>{ return !!oldValue !== !!newValue || oldValue && (oldValue.type !== newValue.type || oldValue.format !== newValue.format); } }); defineProp({ name: `${name}MapTiling`, defaultValue: new Vec2(1, 1) }); defineProp({ name: `${name}MapOffset`, defaultValue: new Vec2(0, 0) }); defineProp({ name: `${name}MapRotation`, defaultValue: 0 }); defineProp({ name: `${name}MapUv`, defaultValue: uv }); if (channel) { defineProp({ name: `${name}MapChannel`, defaultValue: channel }); if (vertexColor) { defineProp({ name: `${name}VertexColor`, defaultValue: false }); defineProp({ name: `${name}VertexColorChannel`, defaultValue: channel }); } } const mapTiling = `${name}MapTiling`; const mapOffset = `${name}MapOffset`; const mapRotation = `${name}MapRotation`; const mapTransform = `${name}MapTransform`; defineUniform(mapTransform, (material, device, scene)=>{ const tiling = material[mapTiling]; const offset = material[mapOffset]; const rotation = material[mapRotation]; if (tiling.x === 1 && tiling.y === 1 && offset.x === 0 && offset.y === 0 && rotation === 0) { return null; } const uniform = material._allocUniform(mapTransform, ()=>{ return [ { name: `texture_${mapTransform}0`, value: new Float32Array(3) }, { name: `texture_${mapTransform}1`, value: new Float32Array(3) } ]; }); const cr = Math.cos(rotation * math.DEG_TO_RAD); const sr = Math.sin(rotation * math.DEG_TO_RAD); const uniform0 = uniform[0].value; uniform0[0] = cr * tiling.x; uniform0[1] = -sr * tiling.y; uniform0[2] = offset.x; const uniform1 = uniform[1].value; uniform1[0] = sr * tiling.x; uniform1[1] = cr * tiling.y; uniform1[2] = 1.0 - tiling.y - offset.y; return uniform; }); } function _defineColor(name, defaultValue) { defineProp({ name: name, defaultValue: defaultValue, getterFunc: function() { this._dirtyShader = true; return this[`_${name}`]; } }); defineUniform(name, (material, device, scene)=>{ const uniform = material._allocUniform(name, ()=>new Float32Array(3)); const color = material[name]; _tempColor$2.linear(color); uniform[0] = _tempColor$2.r; uniform[1] = _tempColor$2.g; uniform[2] = _tempColor$2.b; return uniform; }); } function _defineFloat(name, defaultValue, getUniformFunc) { defineProp({ name: name, defaultValue: defaultValue, dirtyShaderFunc: (oldValue, newValue)=>{ return (oldValue === 0 || oldValue === 1) !== (newValue === 0 || newValue === 1); } }); defineUniform(name, getUniformFunc); } function _defineObject(name, getUniformFunc) { defineProp({ name: name, defaultValue: null, dirtyShaderFunc: (oldValue, newValue)=>{ return !!oldValue === !!newValue; } }); defineUniform(name, getUniformFunc); } function _defineFlag(name, defaultValue) { defineProp({ name: name, defaultValue: defaultValue }); } function _defineMaterialProps() { _defineColor('ambient', new Color(1, 1, 1)); _defineColor('diffuse', new Color(1, 1, 1)); _defineColor('specular', new Color(0, 0, 0)); _defineColor('emissive', new Color(0, 0, 0)); _defineColor('sheen', new Color(1, 1, 1)); _defineColor('attenuation', new Color(1, 1, 1)); _defineFloat('emissiveIntensity', 1); _defineFloat('specularityFactor', 1); _defineFloat('sheenGloss', 0.0); _defineFloat('gloss', 0.25); _defineFloat('aoIntensity', 1); _defineFloat('heightMapFactor', 1, (material, device, scene)=>{ return material.heightMapFactor * 0.025; }); _defineFloat('opacity', 1); _defineFloat('alphaFade', 1); _defineFloat('alphaTest', 0); _defineFloat('bumpiness', 1); _defineFloat('normalDetailMapBumpiness', 1); _defineFloat('reflectivity', 1); _defineFloat('occludeSpecularIntensity', 1); _defineFloat('refraction', 0); _defineFloat('refractionIndex', 1.0 / 1.5, (material, device, scene)=>{ return Math.max(0.001, material.refractionIndex); }); _defineFloat('dispersion', 0); _defineFloat('thickness', 0); _defineFloat('attenuationDistance', 0); _defineFloat('metalness', 1); _defineFloat('anisotropyIntensity', 0); _defineFloat('anisotropyRotation', 0); _defineFloat('clearCoat', 0); _defineFloat('clearCoatGloss', 1); _defineFloat('clearCoatBumpiness', 1); _defineFloat('aoUvSet', 0, null); _defineFloat('iridescence', 0); _defineFloat('iridescenceRefractionIndex', 1.0 / 1.5); _defineFloat('iridescenceThicknessMin', 0); _defineFloat('iridescenceThicknessMax', 0); _defineObject('ambientSH'); _defineObject('cubeMapProjectionBox', (material, device, scene)=>{ const uniform = material._allocUniform('cubeMapProjectionBox', ()=>{ return [ { name: 'envBoxMin', value: new Float32Array(3) }, { name: 'envBoxMax', value: new Float32Array(3) } ]; }); const bboxMin = material.cubeMapProjectionBox.getMin(); const minUniform = uniform[0].value; minUniform[0] = bboxMin.x; minUniform[1] = bboxMin.y; minUniform[2] = bboxMin.z; const bboxMax = material.cubeMapProjectionBox.getMax(); const maxUniform = uniform[1].value; maxUniform[0] = bboxMax.x; maxUniform[1] = bboxMax.y; maxUniform[2] = bboxMax.z; return uniform; }); _defineFlag('specularTint', false); _defineFlag('specularityFactorTint', false); _defineFlag('useMetalness', false); _defineFlag('useMetalnessSpecularColor', false); _defineFlag('useSheen', false); _defineFlag('enableGGXSpecular', false); _defineFlag('occludeDirect', false); _defineFlag('opacityFadesSpecular', true); _defineFlag('occludeSpecular', SPECOCC_AO); _defineFlag('fresnelModel', FRESNEL_SCHLICK); _defineFlag('useDynamicRefraction', false); _defineFlag('cubeMapProjection', CUBEPROJ_NONE); _defineFlag('useFog', true); _defineFlag('useLighting', true); _defineFlag('useTonemap', true); _defineFlag('useSkybox', true); _defineFlag('forceUv1', false); _defineFlag('pixelSnap', false); _defineFlag('twoSidedLighting', false); _defineFlag('nineSlicedMode', undefined); _defineFlag('msdfTextAttribute', false); _defineFlag('useIridescence', false); _defineFlag('glossInvert', false); _defineFlag('sheenGlossInvert', false); _defineFlag('clearCoatGlossInvert', false); _defineFlag('opacityDither', DITHER_NONE); _defineFlag('opacityShadowDither', DITHER_NONE); _defineFlag('shadowCatcher', false); _defineFlag('vertexColorGamma', false); _defineTex2D('diffuse'); _defineTex2D('specular'); _defineTex2D('emissive'); _defineTex2D('thickness', 'g'); _defineTex2D('specularityFactor', 'g'); _defineTex2D('normal', ''); _defineTex2D('metalness', 'g'); _defineTex2D('gloss', 'g'); _defineTex2D('opacity', 'a'); _defineTex2D('refraction', 'g'); _defineTex2D('height', 'g', false); _defineTex2D('ao', 'g'); _defineTex2D('light', 'rgb', true, 1); _defineTex2D('msdf', ''); _defineTex2D('diffuseDetail', 'rgb', false); _defineTex2D('normalDetail', ''); _defineTex2D('aoDetail', 'g', false); _defineTex2D('clearCoat', 'g'); _defineTex2D('clearCoatGloss', 'g'); _defineTex2D('clearCoatNormal', ''); _defineTex2D('sheen', 'rgb'); _defineTex2D('sheenGloss', 'g'); _defineTex2D('iridescence', 'g'); _defineTex2D('iridescenceThickness', 'g'); _defineTex2D('anisotropy', ''); _defineFlag('diffuseDetailMode', DETAILMODE_MUL); _defineFlag('aoDetailMode', DETAILMODE_MUL); _defineObject('cubeMap'); _defineObject('sphereMap'); _defineObject('envAtlas'); const getterFunc = function() { return this._prefilteredCubemaps; }; const setterFunc = function(value) { const cubemaps = this._prefilteredCubemaps; value = value || []; let changed = false; let complete = true; for(let i = 0; i < 6; ++i){ const v = value[i] || null; if (cubemaps[i] !== v) { cubemaps[i] = v; changed = true; } complete = complete && !!cubemaps[i]; } if (changed) { if (complete) { this.envAtlas = EnvLighting.generatePrefilteredAtlas(cubemaps, { target: this.envAtlas }); } else { if (this.envAtlas) { this.envAtlas.destroy(); this.envAtlas = null; } } this._dirtyShader = true; } }; const empty = [ null, null, null, null, null, null ]; definePropInternal('prefilteredCubemaps', ()=>empty.slice(), setterFunc, getterFunc); } _defineMaterialProps(); const primitiveUv1Padding = 8.0 / 64; const primitiveUv1PaddingScale = 1.0 - primitiveUv1Padding * 2; class ConeBaseGeometry extends Geometry { constructor(baseRadius, peakRadius, height, heightSegments, capSegments, roundedCaps){ super(); const pos = new Vec3(); const bottomToTop = new Vec3(); const norm = new Vec3(); const top = new Vec3(); const bottom = new Vec3(); const tangent = new Vec3(); const positions = []; const normals = []; const uvs = []; const uvs1 = []; const indices = []; let offset; if (height > 0) { for(let i = 0; i <= heightSegments; i++){ for(let j = 0; j <= capSegments; j++){ const theta = j / capSegments * 2 * Math.PI - Math.PI; const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); bottom.set(sinTheta * baseRadius, -height / 2, cosTheta * baseRadius); top.set(sinTheta * peakRadius, height / 2, cosTheta * peakRadius); pos.lerp(bottom, top, i / heightSegments); bottomToTop.sub2(top, bottom).normalize(); tangent.set(cosTheta, 0, -sinTheta); norm.cross(tangent, bottomToTop).normalize(); positions.push(pos.x, pos.y, pos.z); normals.push(norm.x, norm.y, norm.z); let u = j / capSegments; let v = i / heightSegments; uvs.push(u, 1 - v); const _v = v; v = u; u = _v; u = u * primitiveUv1PaddingScale + primitiveUv1Padding; v = v * primitiveUv1PaddingScale + primitiveUv1Padding; u /= 3; uvs1.push(u, 1 - v); if (i < heightSegments && j < capSegments) { const first = i * (capSegments + 1) + j; const second = i * (capSegments + 1) + (j + 1); const third = (i + 1) * (capSegments + 1) + j; const fourth = (i + 1) * (capSegments + 1) + (j + 1); indices.push(first, second, third); indices.push(second, fourth, third); } } } } if (roundedCaps) { const latitudeBands = Math.floor(capSegments / 2); const longitudeBands = capSegments; const capOffset = height / 2; for(let lat = 0; lat <= latitudeBands; lat++){ const theta = lat * Math.PI * 0.5 / latitudeBands; const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); for(let lon = 0; lon <= longitudeBands; lon++){ const phi = lon * 2 * Math.PI / longitudeBands - Math.PI / 2; const sinPhi = Math.sin(phi); const cosPhi = Math.cos(phi); const x = cosPhi * sinTheta; const y = cosTheta; const z = sinPhi * sinTheta; let u = 1 - lon / longitudeBands; let v = 1 - lat / latitudeBands; positions.push(x * peakRadius, y * peakRadius + capOffset, z * peakRadius); normals.push(x, y, z); uvs.push(u, 1 - v); u = u * primitiveUv1PaddingScale + primitiveUv1Padding; v = v * primitiveUv1PaddingScale + primitiveUv1Padding; u /= 3; v /= 3; u += 1.0 / 3; uvs1.push(u, 1 - v); } } offset = (heightSegments + 1) * (capSegments + 1); for(let lat = 0; lat < latitudeBands; ++lat){ for(let lon = 0; lon < longitudeBands; ++lon){ const first = lat * (longitudeBands + 1) + lon; const second = first + longitudeBands + 1; indices.push(offset + first + 1, offset + second, offset + first); indices.push(offset + first + 1, offset + second + 1, offset + second); } } for(let lat = 0; lat <= latitudeBands; lat++){ const theta = Math.PI * 0.5 + lat * Math.PI * 0.5 / latitudeBands; const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); for(let lon = 0; lon <= longitudeBands; lon++){ const phi = lon * 2 * Math.PI / longitudeBands - Math.PI / 2; const sinPhi = Math.sin(phi); const cosPhi = Math.cos(phi); const x = cosPhi * sinTheta; const y = cosTheta; const z = sinPhi * sinTheta; let u = 1 - lon / longitudeBands; let v = 1 - lat / latitudeBands; positions.push(x * peakRadius, y * peakRadius - capOffset, z * peakRadius); normals.push(x, y, z); uvs.push(u, 1 - v); u = u * primitiveUv1PaddingScale + primitiveUv1Padding; v = v * primitiveUv1PaddingScale + primitiveUv1Padding; u /= 3; v /= 3; u += 2.0 / 3; uvs1.push(u, 1 - v); } } offset = (heightSegments + 1) * (capSegments + 1) + (longitudeBands + 1) * (latitudeBands + 1); for(let lat = 0; lat < latitudeBands; ++lat){ for(let lon = 0; lon < longitudeBands; ++lon){ const first = lat * (longitudeBands + 1) + lon; const second = first + longitudeBands + 1; indices.push(offset + first + 1, offset + second, offset + first); indices.push(offset + first + 1, offset + second + 1, offset + second); } } } else { offset = (heightSegments + 1) * (capSegments + 1); if (baseRadius > 0) { for(let i = 0; i < capSegments; i++){ const theta = i / capSegments * 2 * Math.PI; const x = Math.sin(theta); const y = -height / 2; const z = Math.cos(theta); let u = 1 - (x + 1) / 2; let v = (z + 1) / 2; positions.push(x * baseRadius, y, z * baseRadius); normals.push(0, -1, 0); uvs.push(u, 1 - v); u = u * primitiveUv1PaddingScale + primitiveUv1Padding; v = v * primitiveUv1PaddingScale + primitiveUv1Padding; u /= 3; v /= 3; u += 1 / 3; uvs1.push(u, 1 - v); if (i > 1) { indices.push(offset, offset + i, offset + i - 1); } } } offset += capSegments; if (peakRadius > 0) { for(let i = 0; i < capSegments; i++){ const theta = i / capSegments * 2 * Math.PI; const x = Math.sin(theta); const y = height / 2; const z = Math.cos(theta); let u = 1 - (x + 1) / 2; let v = (z + 1) / 2; positions.push(x * peakRadius, y, z * peakRadius); normals.push(0, 1, 0); uvs.push(u, 1 - v); u = u * primitiveUv1PaddingScale + primitiveUv1Padding; v = v * primitiveUv1PaddingScale + primitiveUv1Padding; u /= 3; v /= 3; u += 2 / 3; uvs1.push(u, 1 - v); if (i > 1) { indices.push(offset, offset + i - 1, offset + i); } } } } this.positions = positions; this.normals = normals; this.uvs = uvs; this.uvs1 = uvs1; this.indices = indices; } } class CapsuleGeometry extends ConeBaseGeometry { constructor(opts = {}){ const radius = opts.radius ?? 0.3; const height = opts.height ?? 1; const heightSegments = opts.heightSegments ?? 1; const sides = opts.sides ?? 20; super(radius, radius, height - 2 * radius, heightSegments, sides, true); if (opts.calculateTangents) { this.tangents = calculateTangents(this.positions, this.normals, this.uvs, this.indices); } } } class ConeGeometry extends ConeBaseGeometry { constructor(opts = {}){ const baseRadius = opts.baseRadius ?? 0.5; const peakRadius = opts.peakRadius ?? 0; const height = opts.height ?? 1; const heightSegments = opts.heightSegments ?? 5; const capSegments = opts.capSegments ?? 18; super(baseRadius, peakRadius, height, heightSegments, capSegments, false); if (opts.calculateTangents) { this.tangents = calculateTangents(this.positions, this.normals, this.uvs, this.indices); } } } class CylinderGeometry extends ConeBaseGeometry { constructor(opts = {}){ const radius = opts.radius ?? 0.5; const height = opts.height ?? 1; const heightSegments = opts.heightSegments ?? 5; const capSegments = opts.capSegments ?? 20; super(radius, radius, height, heightSegments, capSegments, false); if (opts.calculateTangents) { this.tangents = calculateTangents(this.positions, this.normals, this.uvs, this.indices); } } } class PlaneGeometry extends Geometry { constructor(opts = {}){ super(); const he = opts.halfExtents ?? new Vec2(0.5, 0.5); const ws = opts.widthSegments ?? 5; const ls = opts.lengthSegments ?? 5; const positions = []; const normals = []; const uvs = []; const indices = []; let vcounter = 0; for(let i = 0; i <= ws; i++){ for(let j = 0; j <= ls; j++){ const x = -he.x + 2 * he.x * i / ws; const y = 0.0; const z = -(-he.y + 2 * he.y * j / ls); const u = i / ws; const v = j / ls; positions.push(x, y, z); normals.push(0, 1, 0); uvs.push(u, 1 - v); if (i < ws && j < ls) { indices.push(vcounter + ls + 1, vcounter + 1, vcounter); indices.push(vcounter + ls + 1, vcounter + ls + 2, vcounter + 1); } vcounter++; } } this.positions = positions; this.normals = normals; this.uvs = uvs; this.uvs1 = uvs; this.indices = indices; if (opts.calculateTangents) { this.tangents = calculateTangents(positions, normals, uvs, indices); } } } class TorusGeometry extends Geometry { constructor(opts = {}){ super(); const rc = opts.tubeRadius ?? 0.2; const rt = opts.ringRadius ?? 0.3; const sectorAngle = (opts.sectorAngle ?? 360) * math.DEG_TO_RAD; const segments = opts.segments ?? 30; const sides = opts.sides ?? 20; const positions = []; const normals = []; const uvs = []; const indices = []; for(let i = 0; i <= sides; i++){ for(let j = 0; j <= segments; j++){ const x = Math.cos(sectorAngle * j / segments) * (rt + rc * Math.cos(2 * Math.PI * i / sides)); const y = Math.sin(2 * Math.PI * i / sides) * rc; const z = Math.sin(sectorAngle * j / segments) * (rt + rc * Math.cos(2 * Math.PI * i / sides)); const nx = Math.cos(sectorAngle * j / segments) * Math.cos(2 * Math.PI * i / sides); const ny = Math.sin(2 * Math.PI * i / sides); const nz = Math.sin(sectorAngle * j / segments) * Math.cos(2 * Math.PI * i / sides); const u = i / sides; const v = 1 - j / segments; positions.push(x, y, z); normals.push(nx, ny, nz); uvs.push(u, 1.0 - v); if (i < sides && j < segments) { const first = i * (segments + 1) + j; const second = (i + 1) * (segments + 1) + j; const third = i * (segments + 1) + (j + 1); const fourth = (i + 1) * (segments + 1) + (j + 1); indices.push(first, second, third); indices.push(second, fourth, third); } } } this.positions = positions; this.normals = normals; this.uvs = uvs; this.uvs1 = uvs; this.indices = indices; if (opts.calculateTangents) { this.tangents = calculateTangents(positions, normals, uvs, indices); } } } class ProgramLibrary { destroy() { this.clearCache(); } register(name, generator) { if (!this._generators.has(name)) { this._generators.set(name, generator); } } unregister(name) { if (this._generators.has(name)) { this._generators.delete(name); } } isRegistered(name) { return this._generators.has(name); } generateShaderDefinition(generator, name, key, options) { let def = this.definitionsCache.get(key); if (!def) { let lights; if (options.litOptions?.lights) { lights = options.litOptions.lights; options.litOptions.lights = lights.map((l)=>{ const lcopy = l.clone ? l.clone() : l; lcopy.key = l.key; return lcopy; }); } this.storeNewProgram(name, options); if (options.litOptions?.lights) { options.litOptions.lights = lights; } if (this._precached) ; const device = this._device; def = generator.createShaderDefinition(device, options); def.name = def.name ?? (options.pass ? `${name}-pass:${options.pass}` : name); this.definitionsCache.set(key, def); } return def; } getCachedShader(key) { return this.processedCache.get(key); } setCachedShader(key, shader) { this.processedCache.set(key, shader); } getProgram(name, options, processingOptions, userMaterialId) { const generator = this._generators.get(name); if (!generator) { return null; } const generationKeyString = generator.generateKey(options); const generationKey = hashCode(generationKeyString); const processingKeyString = processingOptions.generateKey(this._device); const processingKey = hashCode(processingKeyString); const totalKey = `${generationKey}#${processingKey}`; let processedShader = this.getCachedShader(totalKey); if (!processedShader) { const generatedShaderDef = this.generateShaderDefinition(generator, name, generationKey, options); let passName = ''; let shaderPassInfo; if (options.pass !== undefined) { shaderPassInfo = ShaderPass.get(this._device).getByIndex(options.pass); passName = `-${shaderPassInfo.name}`; } this._device.fire('shader:generate', { userMaterialId, shaderPassInfo, definition: generatedShaderDef }); const shaderDefinition = { name: `${generatedShaderDef.name}${passName}-proc`, attributes: generatedShaderDef.attributes, vshader: generatedShaderDef.vshader, vincludes: generatedShaderDef.vincludes, fincludes: generatedShaderDef.fincludes, fshader: generatedShaderDef.fshader, processingOptions: processingOptions, shaderLanguage: generatedShaderDef.shaderLanguage, meshUniformBufferFormat: generatedShaderDef.meshUniformBufferFormat, meshBindGroupFormat: generatedShaderDef.meshBindGroupFormat }; processedShader = new Shader(this._device, shaderDefinition); this.setCachedShader(totalKey, processedShader); } return processedShader; } storeNewProgram(name, options) { let opt = {}; if (name === 'standard') { const defaultMat = this._getDefaultStdMatOptions(options.pass); for(const p in options){ if (options.hasOwnProperty(p) && defaultMat[p] !== options[p] || p === 'pass') { opt[p] = options[p]; } } for(const p in options.litOptions){ opt[p] = options.litOptions[p]; } } else { opt = options; } this._programsCollection.push(JSON.stringify({ name: name, options: opt })); } dumpPrograms() { let text = 'let device = pc.app ? pc.app.graphicsDevice : pc.Application.getApplication().graphicsDevice;\n'; text += 'let shaders = ['; if (this._programsCollection[0]) { text += `\n\t${this._programsCollection[0]}`; } for(let i = 1; i < this._programsCollection.length; ++i){ text += `,\n\t${this._programsCollection[i]}`; } text += '\n];\n'; text += 'pc.getProgramLibrary(device).precompile(shaders);\n'; text += `if (pc.version != \"${version$1}\" || pc.revision != \"${revision}\")\n`; text += '\tconsole.warn(\"precompile-shaders.js: engine version mismatch, rebuild shaders lib with current engine\");'; const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`); element.setAttribute('download', 'precompile-shaders.js'); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } clearCache() { this._isClearingCache = true; this.processedCache.forEach((shader)=>{ shader.destroy(); }); this.processedCache.clear(); this._isClearingCache = false; } removeFromCache(shader) { if (this._isClearingCache) { return; } this.processedCache.forEach((cachedShader, key)=>{ if (shader === cachedShader) { this.processedCache.delete(key); } }); } _getDefaultStdMatOptions(pass) { const shaderPassInfo = ShaderPass.get(this._device).getByIndex(pass); return pass === SHADER_PICK || pass === SHADER_PREPASS || shaderPassInfo.isShadow ? this._defaultStdMatOptionMin : this._defaultStdMatOption; } precompile(cache) { if (cache) { const shaders = new Array(cache.length); for(let i = 0; i < cache.length; i++){ if (cache[i].name === 'standard') { const opt = cache[i].options; const defaultMat = this._getDefaultStdMatOptions(opt.pass); for(const p in defaultMat){ if (defaultMat.hasOwnProperty(p) && opt[p] === undefined) { opt[p] = defaultMat[p]; } } } shaders[i] = this.getProgram(cache[i].name, cache[i].options); } } this._precached = true; } constructor(device, standardMaterial){ this.processedCache = new Map(); this.definitionsCache = new Map(); this._generators = new Map(); this._device = device; this._isClearingCache = false; this._precached = false; this._programsCollection = []; this._defaultStdMatOption = new StandardMaterialOptions(); this._defaultStdMatOptionMin = new StandardMaterialOptions(); const defaultCameraShaderParams = new CameraShaderParams(); standardMaterial.shaderOptBuilder.updateRef(this._defaultStdMatOption, {}, defaultCameraShaderParams, standardMaterial, null, [], SHADER_FORWARD, null); standardMaterial.shaderOptBuilder.updateMinRef(this._defaultStdMatOptionMin, {}, standardMaterial, null, SHADER_SHADOW, null); device.on('destroy:shader', (shader)=>{ this.removeFromCache(shader); }); } } const mat4$1 = new Mat4(); const quat$2 = new Quat(); const aabb = new BoundingBox(); const aabb2 = new BoundingBox(); const debugColor = new Color(1, 1, 0, 0.4); const SH_C0$2 = 0.28209479177387814; class SplatIterator { constructor(gsplatData, p, r, s, c){ const x = gsplatData.getProp('x'); const y = gsplatData.getProp('y'); const z = gsplatData.getProp('z'); const rx = gsplatData.getProp('rot_1'); const ry = gsplatData.getProp('rot_2'); const rz = gsplatData.getProp('rot_3'); const rw = gsplatData.getProp('rot_0'); const sx = gsplatData.getProp('scale_0'); const sy = gsplatData.getProp('scale_1'); const sz = gsplatData.getProp('scale_2'); const cr = gsplatData.getProp('f_dc_0'); const cg = gsplatData.getProp('f_dc_1'); const cb = gsplatData.getProp('f_dc_2'); const ca = gsplatData.getProp('opacity'); const sigmoid = (v)=>{ if (v > 0) { return 1 / (1 + Math.exp(-v)); } const t = Math.exp(v); return t / (1 + t); }; this.read = (i)=>{ if (p) { p.x = x[i]; p.y = y[i]; p.z = z[i]; } if (r) { r.set(rx[i], ry[i], rz[i], rw[i]); } if (s) { s.set(Math.exp(sx[i]), Math.exp(sy[i]), Math.exp(sz[i])); } if (c) { c.set(0.5 + cr[i] * SH_C0$2, 0.5 + cg[i] * SH_C0$2, 0.5 + cb[i] * SH_C0$2, sigmoid(ca[i])); } }; } } const calcSplatMat = (result, p, r)=>{ quat$2.set(r.x, r.y, r.z, r.w).normalize(); result.setTRS(p, quat$2, Vec3.ONE); }; class GSplatData { static calcSplatAabb(result, p, r, s) { calcSplatMat(mat4$1, p, r); aabb.center.set(0, 0, 0); aabb.halfExtents.set(s.x * 2, s.y * 2, s.z * 2); result.setFromTransformedAabb(aabb, mat4$1); } getProp(name, elementName = 'vertex') { return this.getElement(elementName)?.properties.find((p)=>p.name === name)?.storage; } getElement(name) { return this.elements.find((e)=>e.name === name); } addProp(name, storage) { this.getElement('vertex').properties.push({ type: 'float', name, storage, byteSize: 4 }); } createIter(p, r, s, c) { return new SplatIterator(this, p, r, s, c); } calcAabb(result, pred) { let mx, my, mz, Mx, My, Mz; let first = true; const x = this.getProp('x'); const y = this.getProp('y'); const z = this.getProp('z'); const sx = this.getProp('scale_0'); const sy = this.getProp('scale_1'); const sz = this.getProp('scale_2'); for(let i = 0; i < this.numSplats; ++i){ if (pred && !pred(i)) { continue; } const px = x[i]; const py = y[i]; const pz = z[i]; const scale = Math.max(sx[i], sy[i], sz[i]); if (!isFinite(px) || !isFinite(py) || !isFinite(pz) || !isFinite(scale)) { continue; } const scaleVal = 2.0 * Math.exp(scale); if (first) { first = false; mx = px - scaleVal; my = py - scaleVal; mz = pz - scaleVal; Mx = px + scaleVal; My = py + scaleVal; Mz = pz + scaleVal; } else { mx = Math.min(mx, px - scaleVal); my = Math.min(my, py - scaleVal); mz = Math.min(mz, pz - scaleVal); Mx = Math.max(Mx, px + scaleVal); My = Math.max(My, py + scaleVal); Mz = Math.max(Mz, pz + scaleVal); } } if (!first) { result.center.set((mx + Mx) * 0.5, (my + My) * 0.5, (mz + Mz) * 0.5); result.halfExtents.set((Mx - mx) * 0.5, (My - my) * 0.5, (Mz - mz) * 0.5); } return !first; } calcAabbExact(result, pred) { const p = new Vec3(); const r = new Quat(); const s = new Vec3(); const iter = this.createIter(p, r, s); let first = true; for(let i = 0; i < this.numSplats; ++i){ if (pred && !pred(i)) { continue; } iter.read(i); if (first) { first = false; GSplatData.calcSplatAabb(result, p, r, s); } else { GSplatData.calcSplatAabb(aabb2, p, r, s); result.add(aabb2); } } return !first; } getCenters() { const x = this.getProp('x'); const y = this.getProp('y'); const z = this.getProp('z'); const result = new Float32Array(this.numSplats * 3); for(let i = 0; i < this.numSplats; ++i){ result[i * 3 + 0] = x[i]; result[i * 3 + 1] = y[i]; result[i * 3 + 2] = z[i]; } return result; } calcFocalPoint(result, pred) { const x = this.getProp('x'); const y = this.getProp('y'); const z = this.getProp('z'); const sx = this.getProp('scale_0'); const sy = this.getProp('scale_1'); const sz = this.getProp('scale_2'); result.x = 0; result.y = 0; result.z = 0; let sum = 0; for(let i = 0; i < this.numSplats; ++i){ if (pred && !pred(i)) { continue; } const px = x[i]; const py = y[i]; const pz = z[i]; if (!isFinite(px) || !isFinite(py) || !isFinite(pz)) { continue; } const weight = 1.0 / (1.0 + Math.exp(Math.max(sx[i], sy[i], sz[i]))); result.x += px * weight; result.y += py * weight; result.z += pz * weight; sum += weight; } result.mulScalar(1 / sum); } renderWireframeBounds(scene, worldMat) { const p = new Vec3(); const r = new Quat(); const s = new Vec3(); const min = new Vec3(); const max = new Vec3(); const iter = this.createIter(p, r, s); for(let i = 0; i < this.numSplats; ++i){ iter.read(i); calcSplatMat(mat4$1, p, r); mat4$1.mul2(worldMat, mat4$1); min.set(s.x * -2, s.y * -2, s.z * -2); max.set(s.x * 2.0, s.y * 2.0, s.z * 2.0); scene.immediate.drawWireAlignedBox(min, max, debugColor, true, scene.defaultDrawLayer, mat4$1); } } get isCompressed() { return false; } get shBands() { const numProps = ()=>{ for(let i = 0; i < 45; ++i){ if (!this.getProp(`f_rest_${i}`)) { return i; } } return 45; }; const sizes = { 9: 1, 24: 2, 45: 3 }; return sizes[numProps()] ?? 0; } calcMortonOrder() { const calcMinMax = (arr)=>{ let min = arr[0]; let max = arr[0]; for(let i = 1; i < arr.length; i++){ if (arr[i] < min) min = arr[i]; if (arr[i] > max) max = arr[i]; } return { min, max }; }; const encodeMorton3 = (x, y, z)=>{ const Part1By2 = (x)=>{ x &= 0x000003ff; x = (x ^ x << 16) & 0xff0000ff; x = (x ^ x << 8) & 0x0300f00f; x = (x ^ x << 4) & 0x030c30c3; x = (x ^ x << 2) & 0x09249249; return x; }; return (Part1By2(z) << 2) + (Part1By2(y) << 1) + Part1By2(x); }; const x = this.getProp('x'); const y = this.getProp('y'); const z = this.getProp('z'); const { min: minX, max: maxX } = calcMinMax(x); const { min: minY, max: maxY } = calcMinMax(y); const { min: minZ, max: maxZ } = calcMinMax(z); const sizeX = minX === maxX ? 0 : 1024 / (maxX - minX); const sizeY = minY === maxY ? 0 : 1024 / (maxY - minY); const sizeZ = minZ === maxZ ? 0 : 1024 / (maxZ - minZ); const codes = new Map(); for(let i = 0; i < this.numSplats; i++){ const ix = Math.min(1023, Math.floor((x[i] - minX) * sizeX)); const iy = Math.min(1023, Math.floor((y[i] - minY) * sizeY)); const iz = Math.min(1023, Math.floor((z[i] - minZ) * sizeZ)); const code = encodeMorton3(ix, iy, iz); const val = codes.get(code); if (val) { val.push(i); } else { codes.set(code, [ i ]); } } const keys = Array.from(codes.keys()).sort((a, b)=>a - b); const indices = new Uint32Array(this.numSplats); let idx = 0; for(let i = 0; i < keys.length; ++i){ const val = codes.get(keys[i]); for(let j = 0; j < val.length; ++j){ indices[idx++] = val[j]; } } return indices; } reorder(order) { const cache = new Map(); const getStorage = (size)=>{ if (cache.has(size)) { const buffer = cache.get(size); cache.delete(size); return buffer; } return new ArrayBuffer(size); }; const returnStorage = (buffer)=>{ cache.set(buffer.byteLength, buffer); }; const reorder = (data)=>{ const result = new data.constructor(getStorage(data.byteLength)); for(let i = 0; i < order.length; i++){ result[i] = data[order[i]]; } returnStorage(data.buffer); return result; }; this.elements.forEach((element)=>{ element.properties.forEach((property)=>{ if (property.storage) { property.storage = reorder(property.storage); } }); }); } reorderData() { this.reorder(this.calcMortonOrder()); } constructor(elements, comments = []){ this.elements = elements; this.numSplats = this.getElement('vertex').count; this.comments = comments; } } class UploadStream { destroy() { this._deviceLostEvent?.off(); this._deviceLostEvent = null; this.impl?.destroy(); this.impl = null; } upload(data, target, offset = 0, size = data.length) { this.impl?.upload(data, target, offset, size); } _onDeviceLost() { this.impl?._onDeviceLost?.(); } constructor(device, useSingleBuffer = false){ this._deviceLostEvent = null; this.device = device; this.useSingleBuffer = useSingleBuffer; this.impl = device.createUploadStreamImpl(this); this._deviceLostEvent = this.device.on('devicelost', this._onDeviceLost, this); } } var glslGsplatCopyToWorkBufferPS = ` #define GSPLAT_CENTER_NOPROJ #include "gsplatStructsVS" #include "gsplatCenterVS" #include "gsplatEvalSHVS" #include "gsplatQuatToMat3VS" #include "gsplatSourceFormatVS" uniform int uStartLine; uniform int uViewportWidth; #ifdef GSPLAT_LOD uniform usampler2D uIntervalsTexture; #endif uniform vec3 uColorMultiply; uniform int uActiveSplats; uniform vec3 model_scale; uniform vec4 model_rotation; void main(void) { ivec2 localFragCoords = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y) - uStartLine); int targetIndex = localFragCoords.y * uViewportWidth + localFragCoords.x; if (targetIndex >= uActiveSplats) { #ifdef GSPLAT_COLOR_UINT pcFragColor0 = uvec4(0u); #else pcFragColor0 = vec4(0.0); #endif #ifndef GSPLAT_COLOR_ONLY pcFragColor1 = uvec4(0u); pcFragColor2 = uvec2(0u); #endif } else { #ifdef GSPLAT_LOD int intervalsSize = int(textureSize(uIntervalsTexture, 0).x); ivec2 intervalUV = ivec2(targetIndex % intervalsSize, targetIndex / intervalsSize); uint originalIndex = texelFetch(uIntervalsTexture, intervalUV, 0).r; #else uint originalIndex = uint(targetIndex); #endif #if defined(GSPLAT_SOGS_DATA) || defined(GSPLAT_COMPRESSED_DATA) uint srcSize = uint(textureSize(packedTexture, 0).x); #else uint srcSize = uint(textureSize(splatColor, 0).x); #endif SplatSource source; source.id = uint(originalIndex); source.uv = ivec2(source.id % srcSize, source.id / srcSize); vec3 modelCenter = readCenter(source); vec3 worldCenter = (matrix_model * vec4(modelCenter, 1.0)).xyz; SplatCenter center; initCenter(modelCenter, center); vec4 srcRotation = getRotation().yzwx; vec3 srcScale = getScale(); vec4 worldRotation = quatMul(model_rotation, srcRotation); if (worldRotation.w < 0.0) { worldRotation = -worldRotation; } vec3 worldScale = model_scale * srcScale; vec4 color = readColor(source); #if SH_BANDS > 0 vec3 dir = normalize(center.view * mat3(center.modelView)); vec3 sh[SH_COEFFS]; float scale; readSHData(source, sh, scale); color.xyz += evalSH(sh, dir) * scale; #endif color.xyz *= uColorMultiply; #ifdef GSPLAT_COLOR_UINT uint packed_rg = packHalf2x16(color.rg); uint packed_ba = packHalf2x16(color.ba); pcFragColor0 = uvec4( packed_rg & 0xFFFFu, packed_rg >> 16u, packed_ba & 0xFFFFu, packed_ba >> 16u ); #else pcFragColor0 = color; #endif #ifndef GSPLAT_COLOR_ONLY pcFragColor1 = uvec4(floatBitsToUint(worldCenter.x), floatBitsToUint(worldCenter.y), floatBitsToUint(worldCenter.z), packHalf2x16(worldRotation.xy)); pcFragColor2 = uvec2(packHalf2x16(vec2(worldRotation.z, worldScale.x)), packHalf2x16(worldScale.yz)); #endif } } `; var wgslGsplatCopyToWorkBufferPS = ` #define GSPLAT_CENTER_NOPROJ #include "gsplatStructsVS" #include "gsplatCenterVS" #include "gsplatEvalSHVS" #include "gsplatQuatToMat3VS" #include "gsplatSourceFormatVS" uniform uStartLine: i32; uniform uViewportWidth: i32; #ifdef GSPLAT_LOD var uIntervalsTexture: texture_2d; #endif uniform uColorMultiply: vec3f; uniform uActiveSplats: i32; uniform model_scale: vec3f; uniform model_rotation: vec4f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let localFragCoords = vec2i(i32(input.position.x), i32(input.position.y) - uniform.uStartLine); let targetIndex = localFragCoords.y * uniform.uViewportWidth + localFragCoords.x; if (targetIndex >= uniform.uActiveSplats) { output.color = vec4f(0.0); #ifndef GSPLAT_COLOR_ONLY output.color1 = vec4u(0u); output.color2 = vec2u(0u); #endif } else { #ifdef GSPLAT_LOD let intervalsSize = i32(textureDimensions(uIntervalsTexture, 0).x); let intervalUV = vec2i(targetIndex % intervalsSize, targetIndex / intervalsSize); let originalIndex = textureLoad(uIntervalsTexture, intervalUV, 0).r; #else let originalIndex = targetIndex; #endif var srcSize: u32; #if defined(GSPLAT_SOGS_DATA) || defined(GSPLAT_COMPRESSED_DATA) srcSize = u32(textureDimensions(packedTexture, 0).x); #else srcSize = u32(textureDimensions(splatColor, 0).x); #endif var source: SplatSource; source.id = u32(originalIndex); source.uv = vec2i(i32(source.id % srcSize), i32(source.id / srcSize)); var modelCenter = readCenter(&source); let worldCenter = (uniform.matrix_model * vec4f(modelCenter, 1.0)).xyz; var center: SplatCenter; initCenter(modelCenter, ¢er); let srcRotation = getRotation().yzwx; let srcScale = getScale(); var worldRotation = quatMul(uniform.model_rotation, srcRotation); if (worldRotation.w < 0.0) { worldRotation = -worldRotation; } let worldScale = uniform.model_scale * srcScale; var color = readColor(&source); #if SH_BANDS > 0 let dir = normalize(center.view * mat3x3f(center.modelView[0].xyz, center.modelView[1].xyz, center.modelView[2].xyz)); var sh: array; var scale: f32; readSHData(&source, &sh, &scale); color = vec4f(color.xyz + evalSH(&sh, dir) * scale, color.w); #endif color = vec4f(color.xyz * uniform.uColorMultiply, color.w); output.color = color; #ifndef GSPLAT_COLOR_ONLY output.color1 = vec4u(bitcast(worldCenter.x), bitcast(worldCenter.y), bitcast(worldCenter.z), pack2x16float(worldRotation.xy)); output.color2 = vec2u(pack2x16float(vec2f(worldRotation.z, worldScale.x)), pack2x16float(worldScale.yz)); #endif } return output; } `; const _viewMat = new Mat4(); const _modelScale = new Vec3(); const _modelRotation = new Quat(); const _whiteColor = [ 1, 1, 1 ]; class GSplatWorkBufferRenderPass extends RenderPass { init(renderTarget) { super.init(renderTarget); this.colorOps.clear = false; this.depthStencilOps.clearDepth = false; } update(splats, cameraNode, colorsByLod) { this.splats.length = 0; this.colorsByLod = colorsByLod; for(let i = 0; i < splats.length; i++){ const splatInfo = splats[i]; if (splatInfo.activeSplats > 0) { this.splats.push(splatInfo); } } this.cameraNode = cameraNode; return this.splats.length > 0; } execute() { const { device, splats, cameraNode } = this; device.setBlendState(BlendState.NOBLEND); device.setCullMode(CULLFACE_NONE); device.setDepthState(DepthState.NODEPTH); device.setStencilState(); const viewInvMat = cameraNode.getWorldTransform(); const viewMat = _viewMat.copy(viewInvMat).invert(); device.scope.resolve('matrix_view').setValue(viewMat.data); for(let i = 0; i < splats.length; i++){ this.renderSplat(splats[i]); } } renderSplat(splatInfo) { const { device, resource } = splatInfo; const scope = device.scope; const { intervals, activeSplats, lineStart, viewport, intervalTexture } = splatInfo; const workBufferRenderInfo = resource.getWorkBufferRenderInfo(intervals.length > 0, this.workBuffer.colorTextureFormat, this.colorOnly); workBufferRenderInfo.material.setParameters(device); if (intervalTexture) { scope.resolve('uIntervalsTexture').setValue(intervalTexture.texture); } scope.resolve('uActiveSplats').setValue(activeSplats); scope.resolve('uStartLine').setValue(lineStart); scope.resolve('uViewportWidth').setValue(viewport.z); const color = this.colorsByLod?.[splatInfo.lodIndex] ?? this.colorsByLod?.[0] ?? _whiteColor; scope.resolve('uColorMultiply').setValue(color); const worldTransform = splatInfo.node.getWorldTransform(); worldTransform.getScale(_modelScale); _modelRotation.setFromMat4(worldTransform); if (_modelRotation.w < 0) { _modelRotation.mulScalar(-1); } this._modelScaleData[0] = _modelScale.x; this._modelScaleData[1] = _modelScale.y; this._modelScaleData[2] = _modelScale.z; this._modelRotationData[0] = _modelRotation.x; this._modelRotationData[1] = _modelRotation.y; this._modelRotationData[2] = _modelRotation.z; this._modelRotationData[3] = _modelRotation.w; scope.resolve('matrix_model').setValue(worldTransform.data); scope.resolve('model_scale').setValue(this._modelScaleData); scope.resolve('model_rotation').setValue(this._modelRotationData); workBufferRenderInfo.quadRender.render(viewport); } destroy() { this.splats.length = 0; super.destroy(); } constructor(device, workBuffer, colorOnly = false){ super(device), this.splats = [], this.colorsByLod = undefined, this.cameraNode = null, this._modelScaleData = new Float32Array(3), this._modelRotationData = new Float32Array(4); this.workBuffer = workBuffer; this.colorOnly = colorOnly; } } let id$1 = 0; class WorkBufferRenderInfo { destroy() { this.material?.destroy(); this.quadRender?.destroy(); } constructor(device, key, material, colorTextureFormat, colorOnly){ this.device = device; this.material = material; const clonedDefines = new Map(material.defines); const isColorUint = colorTextureFormat === PIXELFORMAT_RGBA16U; const colorOutputType = isColorUint ? 'uvec4' : 'vec4'; if (isColorUint) { clonedDefines.set('GSPLAT_COLOR_UINT', ''); } if (colorOnly) { clonedDefines.set('GSPLAT_COLOR_ONLY', ''); } const shader = ShaderUtils.createShader(this.device, { uniqueName: `SplatCopyToWorkBuffer:${key}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexDefines: clonedDefines, fragmentDefines: clonedDefines, vertexChunk: 'fullscreenQuadVS', fragmentGLSL: glslGsplatCopyToWorkBufferPS, fragmentWGSL: wgslGsplatCopyToWorkBufferPS, fragmentOutputTypes: colorOnly ? [ colorOutputType ] : [ colorOutputType, 'uvec4', 'uvec2' ] }); this.quadRender = new QuadRender(shader); } } class GSplatWorkBuffer { destroy() { this.renderPass?.destroy(); this.colorRenderPass?.destroy(); this.colorTexture?.destroy(); this.splatTexture0?.destroy(); this.splatTexture1?.destroy(); this.orderTexture?.destroy(); this.orderBuffer?.destroy(); this.renderTarget?.destroy(); this.colorRenderTarget?.destroy(); this.uploadStream.destroy(); } get textureSize() { return this._textureSize; } setOrderData(data) { if (this.device.isWebGPU) { this.uploadStream.upload(data, this.orderBuffer, 0, data.length); } else { this.uploadStream.upload(data, this.orderTexture, 0, data.length); } } createTexture(name, format, w, h) { return new Texture(this.device, { name: name, width: w, height: h, format: format, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); } resize(textureSize) { this.renderTarget.resize(textureSize, textureSize); this.colorRenderTarget.resize(textureSize, textureSize); this._textureSize = textureSize; if (this.device.isWebGPU) { const newByteSize = textureSize * textureSize * 4; if (this.orderBuffer.byteSize < newByteSize) { this.orderBuffer.destroy(); this.orderBuffer = new StorageBuffer(this.device, newByteSize, BUFFERUSAGE_COPY_DST); } } else { this.orderTexture.resize(textureSize, textureSize); } } render(splats, cameraNode, colorsByLod) { if (this.renderPass.update(splats, cameraNode, colorsByLod)) { this.renderPass.render(); } } renderColor(splats, cameraNode, colorsByLod) { if (this.colorRenderPass.update(splats, cameraNode, colorsByLod)) { this.colorRenderPass.render(); } } constructor(device){ this.id = id$1++; this._textureSize = 1; this.device = device; this.colorTextureFormat = device.getRenderableHdrFormat([ PIXELFORMAT_RGBA16F ]) || PIXELFORMAT_RGBA16U; this.colorTexture = this.createTexture('splatColor', this.colorTextureFormat, 1, 1); this.splatTexture0 = this.createTexture('splatTexture0', PIXELFORMAT_RGBA32U, 1, 1); this.splatTexture1 = this.createTexture('splatTexture1', PIXELFORMAT_RG32U, 1, 1); this.renderTarget = new RenderTarget({ name: `GsplatWorkBuffer-MRT-${this.id}`, colorBuffers: [ this.colorTexture, this.splatTexture0, this.splatTexture1 ], depth: false, flipY: true }); this.colorRenderTarget = new RenderTarget({ name: `GsplatWorkBuffer-Color-${this.id}`, colorBuffer: this.colorTexture, depth: false, flipY: true }); this.uploadStream = new UploadStream(device); if (device.isWebGPU) { this.orderBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_DST); } else { this.orderTexture = this.createTexture('SplatGlobalOrder', PIXELFORMAT_R32U, 1, 1); } this.renderPass = new GSplatWorkBufferRenderPass(device, this); this.renderPass.init(this.renderTarget); this.colorRenderPass = new GSplatWorkBufferRenderPass(device, this, true); this.colorRenderPass.init(this.colorRenderTarget); } } let id = 0; const tempMap = new Map(); class GSplatResourceBase { destroy() { this.mesh?.destroy(); this.instanceIndices?.destroy(); this.workBufferRenderInfos.forEach((info)=>info.destroy()); this.workBufferRenderInfos.clear(); } incRefCount() { this._refCount++; } decRefCount() { this._refCount--; } get refCount() { return this._refCount; } ensureMesh() { if (!this.mesh) { this.mesh = GSplatResourceBase.createMesh(this.device); this.mesh.aabb.copy(this.aabb); this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, this.gsplatData.numSplats); } this._meshRefCount++; } releaseMesh() { this._meshRefCount--; if (this._meshRefCount < 1) { this.mesh = null; this.instanceIndices?.destroy(); this.instanceIndices = null; } } getWorkBufferRenderInfo(useIntervals, colorTextureFormat, colorOnly = false) { this.configureMaterialDefines(tempMap); if (useIntervals) tempMap.set('GSPLAT_LOD', ''); if (colorOnly) tempMap.set('GSPLAT_COLOR_ONLY', ''); const key = Array.from(tempMap.entries()).map(([k, v])=>`${k}=${v}`).join(';'); let info = this.workBufferRenderInfos.get(key); if (!info) { const material = new ShaderMaterial(); this.configureMaterial(material); tempMap.forEach((v, k)=>material.setDefine(k, v)); info = new WorkBufferRenderInfo(this.device, key, material, colorTextureFormat, colorOnly); this.workBufferRenderInfos.set(key, info); } tempMap.clear(); return info; } static createMesh(device) { const splatInstanceSize = GSplatResourceBase.instanceSize; const meshPositions = new Float32Array(12 * splatInstanceSize); const meshIndices = new Uint32Array(6 * splatInstanceSize); for(let i = 0; i < splatInstanceSize; ++i){ meshPositions.set([ -1, -1, i, 1, -1, i, 1, 1, i, -1, 1, i ], i * 12); const b = i * 4; meshIndices.set([ 0 + b, 1 + b, 2 + b, 0 + b, 2 + b, 3 + b ], i * 6); } const mesh = new Mesh(device); mesh.setPositions(meshPositions, 3); mesh.setIndices(meshIndices); mesh.update(); return mesh; } static createInstanceIndices(device, splatCount) { const splatInstanceSize = GSplatResourceBase.instanceSize; const numSplats = Math.ceil(splatCount / splatInstanceSize) * splatInstanceSize; const numSplatInstances = numSplats / splatInstanceSize; const indexData = new Uint32Array(numSplatInstances); for(let i = 0; i < numSplatInstances; ++i){ indexData[i] = i * splatInstanceSize; } const vertexFormat = new VertexFormat(device, [ { semantic: SEMANTIC_ATTR13, components: 1, type: TYPE_UINT32, asInt: true } ]); const instanceIndices = new VertexBuffer(device, vertexFormat, numSplatInstances, { usage: BUFFER_STATIC, data: indexData.buffer }); return instanceIndices; } static get instanceSize() { return 128; } get numSplats() { return this.gsplatData.numSplats; } configureMaterial(material) {} configureMaterialDefines(defines) {} evalTextureSize(count) { return Vec2.ZERO; } createTexture(name, format, size, data) { return new Texture(this.device, { name: name, width: size.x, height: size.y, format: format, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, ...data ? { levels: [ data ] } : {} }); } instantiate() {} constructor(device, gsplatData){ this.mesh = null; this.instanceIndices = null; this.id = id++; this.workBufferRenderInfos = new Map(); this._refCount = 0; this._meshRefCount = 0; this.device = device; this.gsplatData = gsplatData; this.centers = gsplatData.getCenters(); this.aabb = new BoundingBox(); gsplatData.calcAabb(this.aabb); } } const getSHData = (gsplatData, numCoeffs)=>{ const result = []; for(let i = 0; i < numCoeffs; ++i){ result.push(gsplatData.getProp(`f_rest_${i}`)); } return result; }; class GSplatResource extends GSplatResourceBase { destroy() { this.colorTexture?.destroy(); this.transformATexture?.destroy(); this.transformBTexture?.destroy(); this.sh1to3Texture?.destroy(); this.sh4to7Texture?.destroy(); this.sh8to11Texture?.destroy(); this.sh12to15Texture?.destroy(); super.destroy(); } configureMaterialDefines(defines) { defines.set('SH_BANDS', this.shBands); } configureMaterial(material) { this.configureMaterialDefines(material.defines); material.setParameter('splatColor', this.colorTexture); material.setParameter('transformA', this.transformATexture); material.setParameter('transformB', this.transformBTexture); if (this.sh1to3Texture) material.setParameter('splatSH_1to3', this.sh1to3Texture); if (this.sh4to7Texture) material.setParameter('splatSH_4to7', this.sh4to7Texture); if (this.sh8to11Texture) material.setParameter('splatSH_8to11', this.sh8to11Texture); if (this.sh12to15Texture) material.setParameter('splatSH_12to15', this.sh12to15Texture); } evalTextureSize(count) { const width = Math.ceil(Math.sqrt(count)); const height = Math.ceil(count / width); return new Vec2(width, height); } updateColorData(gsplatData) { const texture = this.colorTexture; if (!texture) { return; } const float2Half = FloatPacking.float2Half; const data = texture.lock(); const cr = gsplatData.getProp('f_dc_0'); const cg = gsplatData.getProp('f_dc_1'); const cb = gsplatData.getProp('f_dc_2'); const ca = gsplatData.getProp('opacity'); const SH_C0 = 0.28209479177387814; for(let i = 0; i < this.numSplats; ++i){ const r = cr[i] * SH_C0 + 0.5; const g = cg[i] * SH_C0 + 0.5; const b = cb[i] * SH_C0 + 0.5; const a = 1 / (1 + Math.exp(-ca[i])); data[i * 4 + 0] = float2Half(r); data[i * 4 + 1] = float2Half(g); data[i * 4 + 2] = float2Half(b); data[i * 4 + 3] = float2Half(a); } texture.unlock(); } updateTransformData(gsplatData) { const float2Half = FloatPacking.float2Half; if (!this.transformATexture) { return; } const dataA = this.transformATexture.lock(); const dataAFloat32 = new Float32Array(dataA.buffer); const dataB = this.transformBTexture.lock(); const p = new Vec3(); const r = new Quat(); const s = new Vec3(); const iter = gsplatData.createIter(p, r, s); for(let i = 0; i < this.numSplats; i++){ iter.read(i); r.normalize(); if (r.w < 0) { r.mulScalar(-1); } dataAFloat32[i * 4 + 0] = p.x; dataAFloat32[i * 4 + 1] = p.y; dataAFloat32[i * 4 + 2] = p.z; dataA[i * 4 + 3] = float2Half(r.x) | float2Half(r.y) << 16; dataB[i * 4 + 0] = float2Half(s.x); dataB[i * 4 + 1] = float2Half(s.y); dataB[i * 4 + 2] = float2Half(s.z); dataB[i * 4 + 3] = float2Half(r.z); } this.transformATexture.unlock(); this.transformBTexture.unlock(); } updateSHData(gsplatData) { const sh1to3Data = this.sh1to3Texture.lock(); const sh4to7Data = this.sh4to7Texture?.lock(); const sh8to11Data = this.sh8to11Texture?.lock(); const sh12to15Data = this.sh12to15Texture?.lock(); const numCoeffs = { 1: 3, 2: 8, 3: 15 }[this.shBands]; const src = getSHData(gsplatData, numCoeffs * 3); const t11 = (1 << 11) - 1; const t10 = (1 << 10) - 1; const float32 = new Float32Array(1); const uint32 = new Uint32Array(float32.buffer); const c = new Array(numCoeffs * 3).fill(0); for(let i = 0; i < gsplatData.numSplats; ++i){ for(let j = 0; j < numCoeffs; ++j){ c[j * 3] = src[j][i]; c[j * 3 + 1] = src[j + numCoeffs][i]; c[j * 3 + 2] = src[j + numCoeffs * 2][i]; } let max = c[0]; for(let j = 1; j < numCoeffs * 3; ++j){ max = Math.max(max, Math.abs(c[j])); } if (max === 0) { continue; } for(let j = 0; j < numCoeffs; ++j){ c[j * 3 + 0] = Math.max(0, Math.min(t11, Math.floor((c[j * 3 + 0] / max * 0.5 + 0.5) * t11 + 0.5))); c[j * 3 + 1] = Math.max(0, Math.min(t10, Math.floor((c[j * 3 + 1] / max * 0.5 + 0.5) * t10 + 0.5))); c[j * 3 + 2] = Math.max(0, Math.min(t11, Math.floor((c[j * 3 + 2] / max * 0.5 + 0.5) * t11 + 0.5))); } float32[0] = max; sh1to3Data[i * 4 + 0] = uint32[0]; sh1to3Data[i * 4 + 1] = c[0] << 21 | c[1] << 11 | c[2]; sh1to3Data[i * 4 + 2] = c[3] << 21 | c[4] << 11 | c[5]; sh1to3Data[i * 4 + 3] = c[6] << 21 | c[7] << 11 | c[8]; if (this.shBands > 1) { sh4to7Data[i * 4 + 0] = c[9] << 21 | c[10] << 11 | c[11]; sh4to7Data[i * 4 + 1] = c[12] << 21 | c[13] << 11 | c[14]; sh4to7Data[i * 4 + 2] = c[15] << 21 | c[16] << 11 | c[17]; sh4to7Data[i * 4 + 3] = c[18] << 21 | c[19] << 11 | c[20]; if (this.shBands > 2) { sh8to11Data[i * 4 + 0] = c[21] << 21 | c[22] << 11 | c[23]; sh8to11Data[i * 4 + 1] = c[24] << 21 | c[25] << 11 | c[26]; sh8to11Data[i * 4 + 2] = c[27] << 21 | c[28] << 11 | c[29]; sh8to11Data[i * 4 + 3] = c[30] << 21 | c[31] << 11 | c[32]; sh12to15Data[i * 4 + 0] = c[33] << 21 | c[34] << 11 | c[35]; sh12to15Data[i * 4 + 1] = c[36] << 21 | c[37] << 11 | c[38]; sh12to15Data[i * 4 + 2] = c[39] << 21 | c[40] << 11 | c[41]; sh12to15Data[i * 4 + 3] = c[42] << 21 | c[43] << 11 | c[44]; } else { sh8to11Data[i] = c[21] << 21 | c[22] << 11 | c[23]; } } } this.sh1to3Texture.unlock(); this.sh4to7Texture?.unlock(); this.sh8to11Texture?.unlock(); this.sh12to15Texture?.unlock(); } constructor(device, gsplatData){ super(device, gsplatData); const numSplats = gsplatData.numSplats; const size = this.evalTextureSize(numSplats); this.colorTexture = this.createTexture('splatColor', PIXELFORMAT_RGBA16F, size); this.transformATexture = this.createTexture('transformA', PIXELFORMAT_RGBA32U, size); this.transformBTexture = this.createTexture('transformB', PIXELFORMAT_RGBA16F, size); this.updateColorData(gsplatData); this.updateTransformData(gsplatData); this.shBands = gsplatData.shBands; if (this.shBands > 0) { this.sh1to3Texture = this.createTexture('splatSH_1to3', PIXELFORMAT_RGBA32U, size); if (this.shBands > 1) { this.sh4to7Texture = this.createTexture('splatSH_4to7', PIXELFORMAT_RGBA32U, size); if (this.shBands > 2) { this.sh8to11Texture = this.createTexture('splatSH_8to11', PIXELFORMAT_RGBA32U, size); this.sh12to15Texture = this.createTexture('splatSH_12to15', PIXELFORMAT_RGBA32U, size); } else { this.sh8to11Texture = this.createTexture('splatSH_8to11', PIXELFORMAT_R32U, size); } } this.updateSHData(gsplatData); } } } const vertexGLSL = ` attribute vec2 vertex_position; void main(void) { gl_Position = vec4(vertex_position, 0.0, 1.0); } `; const fragmentGLSL = ` #include "gsplatEvalSHVS" vec4 packRgb(vec3 v) { uvec3 vb = uvec3(clamp(v, vec3(0.0), vec3(1.0)) * vec3(2047.0, 2047.0, 1023.0)); uint bits = (vb.x << 21) | (vb.y << 10) | vb.z; return vec4((uvec4(bits) >> uvec4(24, 16, 8, 0)) & uvec4(0xff)) / vec4(255.0); } uniform mediump vec3 dir; uniform mediump sampler2D centroids; uniform mediump float shN_mins; uniform mediump float shN_maxs; void main(void) { ivec2 uv = ivec2(gl_FragCoord.xy) * ivec2(SH_COEFFS, 1); mediump vec3 coefficients[SH_COEFFS]; for (int i = 0; i < SH_COEFFS; i++) { vec3 s = texelFetch(centroids, ivec2(uv.x + i, uv.y), 0).xyz; coefficients[i] = mix(vec3(shN_mins), vec3(shN_maxs), s); } gl_FragColor = packRgb(evalSH(coefficients, dir) * 0.25 + 0.5); } `; const vertexWGSL = ` attribute vertex_position: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(vertex_position, 0.0, 1.0); return output; } `; const fragmentWGSL = ` #include "gsplatEvalSHVS" fn packRgb(v: vec3f) -> vec4f { let vb = vec3u(clamp(v, vec3f(0.0), vec3f(1.0)) * vec3f(2047.0, 2047.0, 1023.0)); let bits = dot(vb, vec3u(1 << 21, 1 << 10, 1)); return vec4f((vec4u(bits) >> vec4u(24, 16, 8, 0)) & vec4u(0xff)) / vec4f(255.0); } uniform dir: vec3f; uniform shN_mins: f32; uniform shN_maxs: f32; var centroids: texture_2d; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; var uv = vec2i(input.position.xy) * vec2i(SH_COEFFS, 1); var coefficients: array; for (var i: i32 = 0; i < SH_COEFFS; i++) { let s: vec3f = textureLoad(centroids, vec2i(uv.x + i, uv.y), 0).xyz; coefficients[i] = mix(vec3f(uniform.shN_mins), vec3f(uniform.shN_maxs), s); } output.color = packRgb(evalSH(&coefficients, uniform.dir) * 0.25 + 0.5); return output; } `; const gsplatSogsColorGLSL = ` uniform mediump sampler2D sh0; uniform highp sampler2D sh_labels; uniform mediump sampler2D sh_result; uniform vec4 sh0_mins; uniform vec4 sh0_maxs; float SH_C0 = 0.28209479177387814; vec3 unpackRgb(vec4 v) { uvec4 uv = uvec4(v * 255.0); uint bits = (uv.x << 24) | (uv.y << 16) | (uv.z << 8) | uv.w; uvec3 vb = (uvec3(bits) >> uvec3(21, 10, 0)) & uvec3(0x7ffu, 0x7ffu, 0x3ffu); return vec3(vb) / vec3(2047.0, 2047.0, 1023.0); } vec4 readColor(in SplatSource source) { vec4 baseSample = mix(sh0_mins, sh0_maxs, texelFetch(sh0, source.uv, 0)); vec4 base = vec4(vec3(0.5) + baseSample.xyz * SH_C0, 1.0 / (1.0 + exp(-baseSample.w))); ivec2 labelSample = ivec2(texelFetch(sh_labels, source.uv, 0).xy * 255.0); int n = labelSample.x + labelSample.y * 256; vec4 shSample = texelFetch(sh_result, ivec2(n % 64, n / 64), 0); vec3 sh = (unpackRgb(shSample) - vec3(0.5)) * 4.0; return vec4(base.xyz + sh, base.w); } `; const gsplatSogsColorWGSL = ` var sh0: texture_2d; var sh_labels: texture_2d; var sh_result: texture_2d; uniform sh0_mins: vec4f; uniform sh0_maxs: vec4f; const SH_C0: f32 = 0.28209479177387814; fn unpackRgb(v: vec4f) -> vec3f { let bits = dot(vec4u(v * 255.0), vec4u(1u << 24, 1u << 16, 1u << 8, 1u)); let vb = (vec3u(bits) >> vec3u(21, 10, 0)) & vec3u(0x7ffu, 0x7ffu, 0x3ffu); return vec3f(vb) / vec3f(2047.0, 2047.0, 1023.0); } fn readColor(source: ptr) -> vec4f { let baseSample: vec4f = mix(uniform.sh0_mins, uniform.sh0_maxs, textureLoad(sh0, source.uv, 0)); let base = vec4f(vec3f(0.5) + baseSample.xyz * SH_C0, 1.0 / (1.0 + exp(-baseSample.w))); let labelSample: vec2i = vec2i(textureLoad(sh_labels, source.uv, 0).xy * 255.0); let n = labelSample.x + labelSample.y * 256; let shSample: vec4f = textureLoad(sh_result, vec2i(n % 64, n / 64), 0); let sh: vec3f = (unpackRgb(shSample) - vec3f(0.5)) * 4.0; return vec4f(base.xyz + sh, base.w); } `; const resolve$1 = (scope, values)=>{ for(const key in values){ scope.resolve(key).setValue(values[key]); } }; class CustomRenderPass extends RenderPass { execute() { this.executeCallback?.(); } constructor(...args){ super(...args), this.executeCallback = null; } } const invModelMat$1 = new Mat4(); const dir$1 = new Vec3(); class GSplatResolveSH { destroy() { const { gsplatInstance } = this; const { material } = gsplatInstance; material.setDefine('SH_BANDS', gsplatInstance.resource.gsplatData.shBands.toString()); const { shaderChunks } = material; shaderChunks.glsl.delete('gsplatSogsColorVS'); shaderChunks.wgsl.delete('gsplatSogsColorVS'); material.update(); this.quadRender.destroy(); this.renderPass.destroy(); this.renderTarget.destroy(); this.texture.destroy(); this.shader.destroy(); } render(camera, modelMat) { const { prevDir, updateMode } = this; if (updateMode === 'disable') { return; } invModelMat$1.invert(modelMat); invModelMat$1.transformVector(camera.forward, dir$1); dir$1.normalize(); if (updateMode === 'enable' && dir$1.equalsApprox(prevDir, 1e-3)) { return; } prevDir.copy(dir$1); const execute = ()=>{ const { device } = this; const { sh_centroids, meta } = this.gsplatInstance.resource.gsplatData; resolve$1(device.scope, { dir: dir$1.toArray(), centroids: sh_centroids, shN_mins: meta.shN.mins, shN_maxs: meta.shN.maxs }); device.setCullMode(CULLFACE_NONE); device.setDepthState(DepthState.NODEPTH); device.setStencilState(null, null); device.setBlendState(BlendState.NOBLEND); this.quadRender.render(); }; this.renderPass.executeCallback = execute; this.renderPass.render(); } constructor(device, gsplatInstance){ this.prevDir = new Vec3(); this.updateMode = 'enable'; this.device = device; this.gsplatInstance = gsplatInstance; const { resource } = gsplatInstance; const includes = new Map(ShaderChunks.get(device, device.isWebGPU ? 'wgsl' : 'glsl')); this.shader = ShaderUtils.createShader(device, { uniqueName: 'gsplatResolveSH', vertexGLSL, fragmentGLSL, vertexWGSL, fragmentWGSL, vertexIncludes: includes, fragmentIncludes: includes, fragmentDefines: new Map([ [ 'SH_BANDS', resource.gsplatData.shBands.toString() ] ]), attributes: { vertex_position: SEMANTIC_POSITION } }); this.texture = resource.createTexture('centroids', PIXELFORMAT_RGBA8, new Vec2(64, 1024)); this.renderTarget = new RenderTarget({ colorBuffer: this.texture, depth: false }); this.renderPass = new CustomRenderPass(device); this.renderPass.init(this.renderTarget, {}); this.renderPass.colorOps.clear = true; this.quadRender = new QuadRender(this.shader); const { material } = gsplatInstance; material.setDefine('SH_BANDS', '0'); const { shaderChunks } = material; shaderChunks.glsl.set('gsplatSogsColorVS', gsplatSogsColorGLSL); shaderChunks.wgsl.set('gsplatSogsColorVS', gsplatSogsColorWGSL); material.update(); device.scope.resolve('sh_result').setValue(this.texture); } } function SortWorker() { const myself = typeof self !== 'undefined' && self || require('node:worker_threads').parentPort; let order; let centers; let chunks; let mapping; let cameraPosition; let cameraDirection; let forceUpdate = false; const lastCameraPosition = { x: 0, y: 0, z: 0 }; const lastCameraDirection = { x: 0, y: 0, z: 0 }; const boundMin = { x: 0, y: 0, z: 0 }; const boundMax = { x: 0, y: 0, z: 0 }; let distances; let countBuffer; const numBins = 32; const binCount = new Array(numBins).fill(0); const binBase = new Array(numBins).fill(0); const binDivider = new Array(numBins).fill(0); const binarySearch = (m, n, compare_fn)=>{ while(m <= n){ const k = n + m >> 1; const cmp = compare_fn(k); if (cmp > 0) { m = k + 1; } else if (cmp < 0) { n = k - 1; } else { return k; } } return ~m; }; const update = ()=>{ if (!order || !centers || centers.length === 0 || !cameraPosition || !cameraDirection) return; const sortStartTime = performance.now(); const px = cameraPosition.x; const py = cameraPosition.y; const pz = cameraPosition.z; const dx = cameraDirection.x; const dy = cameraDirection.y; const dz = cameraDirection.z; const epsilon = 0.001; if (!forceUpdate && Math.abs(px - lastCameraPosition.x) < epsilon && Math.abs(py - lastCameraPosition.y) < epsilon && Math.abs(pz - lastCameraPosition.z) < epsilon && Math.abs(dx - lastCameraDirection.x) < epsilon && Math.abs(dy - lastCameraDirection.y) < epsilon && Math.abs(dz - lastCameraDirection.z) < epsilon) { return; } forceUpdate = false; lastCameraPosition.x = px; lastCameraPosition.y = py; lastCameraPosition.z = pz; lastCameraDirection.x = dx; lastCameraDirection.y = dy; lastCameraDirection.z = dz; let minDist; let maxDist; for(let i = 0; i < 8; ++i){ const x = i & 1 ? boundMin.x : boundMax.x; const y = i & 2 ? boundMin.y : boundMax.y; const z = i & 4 ? boundMin.z : boundMax.z; const d = x * dx + y * dy + z * dz; if (i === 0) { minDist = maxDist = d; } else { minDist = Math.min(minDist, d); maxDist = Math.max(maxDist, d); } } const numVertices = centers.length / 3; const compareBits = Math.max(10, Math.min(20, Math.round(Math.log2(numVertices / 4)))); const bucketCount = 2 ** compareBits + 1; if (distances?.length !== numVertices) { distances = new Uint32Array(numVertices); } if (!countBuffer || countBuffer.length !== bucketCount) { countBuffer = new Uint32Array(bucketCount); } else { countBuffer.fill(0); } const range = maxDist - minDist; if (range < 1e-6) { for(let i = 0; i < numVertices; ++i){ distances[i] = 0; countBuffer[0]++; } } else { const numChunks = chunks.length / 4; binCount.fill(0); for(let i = 0; i < numChunks; ++i){ const x = chunks[i * 4 + 0]; const y = chunks[i * 4 + 1]; const z = chunks[i * 4 + 2]; const r = chunks[i * 4 + 3]; const d = x * dx + y * dy + z * dz - minDist; const binMin = Math.max(0, Math.floor((d - r) * numBins / range)); const binMax = Math.min(numBins, Math.ceil((d + r) * numBins / range)); for(let j = binMin; j < binMax; ++j){ binCount[j]++; } } const binTotal = binCount.reduce((a, b)=>a + b, 0); for(let i = 0; i < numBins; ++i){ binDivider[i] = binCount[i] / binTotal * bucketCount >>> 0; } for(let i = 0; i < numBins; ++i){ binBase[i] = i === 0 ? 0 : binBase[i - 1] + binDivider[i - 1]; } const binRange = range / numBins; let ii = 0; for(let i = 0; i < numVertices; ++i){ const x = centers[ii++]; const y = centers[ii++]; const z = centers[ii++]; const d = (x * dx + y * dy + z * dz - minDist) / binRange; const bin = d >>> 0; const sortKey = binBase[bin] + binDivider[bin] * (d - bin) >>> 0; distances[i] = sortKey; countBuffer[sortKey]++; } } for(let i = 1; i < bucketCount; i++){ countBuffer[i] += countBuffer[i - 1]; } for(let i = 0; i < numVertices; i++){ const distance = distances[i]; const destIndex = --countBuffer[distance]; order[destIndex] = i; } const cameraDist = px * dx + py * dy + pz * dz; const dist = (i)=>{ let o = order[i] * 3; return centers[o++] * dx + centers[o++] * dy + centers[o] * dz - cameraDist; }; const findZero = ()=>{ const result = binarySearch(0, numVertices - 1, (i)=>-dist(i)); return Math.min(numVertices, Math.abs(result)); }; const count = dist(numVertices - 1) >= 0 ? findZero() : numVertices; if (mapping) { for(let i = 0; i < numVertices; ++i){ order[i] = mapping[order[i]]; } } myself.postMessage({ order: order.buffer, count, sortTime: performance.now() - sortStartTime }, [ order.buffer ]); order = null; }; myself.addEventListener('message', (message)=>{ const msgData = message.data ?? message; if (msgData.order) { order = new Uint32Array(msgData.order); } if (msgData.centers) { centers = new Float32Array(msgData.centers); forceUpdate = true; if (msgData.chunks) { const chunksSrc = new Float32Array(msgData.chunks); chunks = new Float32Array(msgData.chunks, 0, chunksSrc.length * 4 / 6); boundMin.x = chunksSrc[0]; boundMin.y = chunksSrc[1]; boundMin.z = chunksSrc[2]; boundMax.x = chunksSrc[3]; boundMax.y = chunksSrc[4]; boundMax.z = chunksSrc[5]; for(let i = 0; i < chunksSrc.length / 6; ++i){ const mx = chunksSrc[i * 6 + 0]; const my = chunksSrc[i * 6 + 1]; const mz = chunksSrc[i * 6 + 2]; const Mx = chunksSrc[i * 6 + 3]; const My = chunksSrc[i * 6 + 4]; const Mz = chunksSrc[i * 6 + 5]; chunks[i * 4 + 0] = (mx + Mx) * 0.5; chunks[i * 4 + 1] = (my + My) * 0.5; chunks[i * 4 + 2] = (mz + Mz) * 0.5; chunks[i * 4 + 3] = Math.sqrt((Mx - mx) ** 2 + (My - my) ** 2 + (Mz - mz) ** 2) * 0.5; if (mx < boundMin.x) boundMin.x = mx; if (my < boundMin.y) boundMin.y = my; if (mz < boundMin.z) boundMin.z = mz; if (Mx > boundMax.x) boundMax.x = Mx; if (My > boundMax.y) boundMax.y = My; if (Mz > boundMax.z) boundMax.z = Mz; } } else { const numVertices = centers.length / 3; const numChunks = Math.ceil(numVertices / 256); chunks = new Float32Array(numChunks * 4); boundMin.x = boundMin.y = boundMin.z = Infinity; boundMax.x = boundMax.y = boundMax.z = -Infinity; let mx, my, mz, Mx, My, Mz; for(let c = 0; c < numChunks; ++c){ mx = my = mz = Infinity; Mx = My = Mz = -Infinity; const start = c * 256; const end = Math.min(numVertices, (c + 1) * 256); for(let i = start; i < end; ++i){ const x = centers[i * 3 + 0]; const y = centers[i * 3 + 1]; const z = centers[i * 3 + 2]; const validX = Number.isFinite(x); const validY = Number.isFinite(y); const validZ = Number.isFinite(z); if (!validX) centers[i * 3 + 0] = 0; if (!validY) centers[i * 3 + 1] = 0; if (!validZ) centers[i * 3 + 2] = 0; if (!validX || !validY || !validZ) { continue; } if (x < mx) mx = x; else if (x > Mx) Mx = x; if (y < my) my = y; else if (y > My) My = y; if (z < mz) mz = z; else if (z > Mz) Mz = z; if (x < boundMin.x) boundMin.x = x; else if (x > boundMax.x) boundMax.x = x; if (y < boundMin.y) boundMin.y = y; else if (y > boundMax.y) boundMax.y = y; if (z < boundMin.z) boundMin.z = z; else if (z > boundMax.z) boundMax.z = z; } chunks[c * 4 + 0] = (mx + Mx) * 0.5; chunks[c * 4 + 1] = (my + My) * 0.5; chunks[c * 4 + 2] = (mz + Mz) * 0.5; chunks[c * 4 + 3] = Math.sqrt((Mx - mx) ** 2 + (My - my) ** 2 + (Mz - mz) ** 2) * 0.5; } } } if (msgData.hasOwnProperty('mapping')) { mapping = msgData.mapping ? new Uint32Array(msgData.mapping) : null; forceUpdate = true; } if (msgData.cameraPosition) cameraPosition = msgData.cameraPosition; if (msgData.cameraDirection) cameraDirection = msgData.cameraDirection; update(); }); } class GSplatSorter extends EventHandler { destroy() { this.worker.terminate(); this.worker = null; } init(orderTexture, centers, chunks) { this.orderTexture = orderTexture; this.centers = centers.slice(); const orderBuffer = this.orderTexture.lock({ mode: TEXTURELOCK_READ }).slice(); this.orderTexture.unlock(); for(let i = 0; i < orderBuffer.length; ++i){ orderBuffer[i] = i; } const obj = { order: orderBuffer.buffer, centers: centers.buffer, chunks: chunks?.buffer }; const transfer = [ orderBuffer.buffer, centers.buffer ].concat(chunks ? [ chunks.buffer ] : []); this.worker.postMessage(obj, transfer); } setMapping(mapping) { if (mapping) { const centers = new Float32Array(mapping.length * 3); for(let i = 0; i < mapping.length; ++i){ const src = mapping[i] * 3; const dst = i * 3; centers[dst + 0] = this.centers[src + 0]; centers[dst + 1] = this.centers[src + 1]; centers[dst + 2] = this.centers[src + 2]; } this.worker.postMessage({ centers: centers.buffer, mapping: mapping.buffer }, [ centers.buffer, mapping.buffer ]); } else { const centers = this.centers.slice(); this.worker.postMessage({ centers: centers.buffer, mapping: null }, [ centers.buffer ]); } } setCamera(pos, dir) { this.worker.postMessage({ cameraPosition: { x: pos.x, y: pos.y, z: pos.z }, cameraDirection: { x: dir.x, y: dir.y, z: dir.z } }); } constructor(scene){ super(); this.scene = scene ?? null; const messageHandler = (message)=>{ const msgData = message.data ?? message; if (this.scene && msgData.sortTime !== undefined) { this.scene.fire('gsplat:sorted', msgData.sortTime); } const newOrder = msgData.order; const oldOrder = this.orderTexture._levels[0].buffer; this.worker.postMessage({ order: oldOrder }, [ oldOrder ]); this.orderTexture._levels[0] = new Uint32Array(newOrder); this.orderTexture.upload(); this.fire('updated', msgData.count); }; const workerSource = `(${SortWorker.toString()})()`; if (platform.environment === 'node') { this.worker = new Worker(workerSource, { eval: true }); this.worker.on('message', messageHandler); } else { this.worker = new Worker(URL.createObjectURL(new Blob([ workerSource ], { type: "application/javascript" }))); this.worker.addEventListener('message', messageHandler); } } } var glslGsplatSogsReorderPS = ` #include "gsplatPackingPS" uniform highp sampler2D means_l; uniform highp sampler2D means_u; uniform highp sampler2D quats; uniform highp sampler2D scales; uniform highp sampler2D sh0; uniform highp sampler2D sh_labels; uniform highp uint numSplats; #ifdef REORDER_V1 float sigmoid(float x) { return 1.0 / (1.0 + exp(-x)); } vec3 vmin(vec3 v) { return vec3(min(min(v.x, v.y), v.z)); } vec3 vmax(vec3 v) { return vec3(max(max(v.x, v.y), v.z)); } vec3 resolve(vec3 m, vec3 M, vec3 v) { return (mix(m, M, v) - vmin(m)) / (vmax(M) - vmin(m)); } uniform vec3 scalesMins; uniform vec3 scalesMaxs; uniform vec4 sh0Mins; uniform vec4 sh0Maxs; #else uniform vec4 scales_codebook[64]; uniform vec4 sh0_codebook[64]; #endif void main(void) { int w = int(textureSize(means_l, 0).x); ivec2 uv = ivec2(gl_FragCoord.xy); if (uint(uv.x + uv.y * w) >= numSplats) { discard; } vec3 meansLSample = texelFetch(means_l, uv, 0).xyz; vec3 meansUSample = texelFetch(means_u, uv, 0).xyz; vec4 quatsSample = texelFetch(quats, uv, 0); vec3 scalesSample = texelFetch(scales, uv, 0).xyz; vec4 sh0Sample = texelFetch(sh0, uv, 0); vec2 shLabelsSample = texelFetch(sh_labels, uv, 0).xy; #ifdef REORDER_V1 uint scale = pack101010(resolve(scalesMins, scalesMaxs, scalesSample)); uint sh0 = pack111110(resolve(sh0Mins.xyz, sh0Maxs.xyz, sh0Sample.xyz)); float alpha = sigmoid(mix(sh0Mins.w, sh0Maxs.w, sh0Sample.w)); #else uint scale = pack101010(resolveCodebook(scalesSample, scales_codebook)); uint sh0 = pack111110(resolveCodebook(sh0Sample.xyz, sh0_codebook)); float alpha = sh0Sample.w; #endif uint qmode = uint(quatsSample.w * 255.0) - 252u; pcFragColor0 = uvec4( pack8888(vec4(meansLSample, shLabelsSample.x)), pack8888(vec4(meansUSample, shLabelsSample.y)), pack8888(vec4(quatsSample.xyz, alpha)), (scale << 2u) | qmode ); pcFragColor1 = unpack8888(sh0); } `; var wgslGsplatSogsReorderPS = ` #include "gsplatPackingPS" var means_l: texture_2d; var means_u: texture_2d; var quats: texture_2d; var scales: texture_2d; var sh0: texture_2d; var sh_labels: texture_2d; uniform numSplats: u32; #ifdef REORDER_V1 fn sigmoid(x: f32) -> f32 { return 1.0 / (1.0 + exp(-x)); } fn vmin(v: vec3f) -> vec3f { return vec3f(min(min(v.x, v.y), v.z)); } fn vmax(v: vec3f) -> vec3f { return vec3f(max(max(v.x, v.y), v.z)); } fn resolve(m: vec3f, M: vec3f, v: vec3f) -> vec3f { return (mix(m, M, v) - vmin(m)) / (vmax(M) - vmin(m)); } uniform scalesMins: vec3f; uniform scalesMaxs: vec3f; uniform sh0Mins: vec4f; uniform sh0Maxs: vec4f; #else uniform scales_codebook: array; uniform sh0_codebook: array; #endif @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let w: u32 = textureDimensions(means_l, 0).x; let uv: vec2 = vec2(input.position.xy); if (uv.x + uv.y * w >= uniform.numSplats) { discard; return output; } let meansLSample: vec3 = textureLoad(means_l, uv, 0).xyz; let meansUSample: vec3 = textureLoad(means_u, uv, 0).xyz; let quatsSample: vec4 = textureLoad(quats, uv, 0); let scalesSample: vec3 = textureLoad(scales, uv, 0).xyz; let sh0Sample: vec4f = textureLoad(sh0, uv, 0); let shLabelsSample: vec2 = textureLoad(sh_labels, uv, 0).xy; #ifdef REORDER_V1 let scale = pack101010(resolve(uniform.scalesMins, uniform.scalesMaxs, scalesSample)); let sh0 = pack111110(resolve(uniform.sh0Mins.xyz, uniform.sh0Maxs.xyz, sh0Sample.xyz)); let alpha = sigmoid(mix(uniform.sh0Mins.w, uniform.sh0Maxs.w, sh0Sample.w)); #else let scale = pack101010(resolveCodebook(scalesSample, &uniform.scales_codebook)); let sh0 = pack111110(resolveCodebook(sh0Sample.xyz, &uniform.sh0_codebook)); let alpha = sh0Sample.w; #endif let qmode = u32(quatsSample.w * 255.0) - 252u; output.color = vec4u( pack8888(vec4f(meansLSample, shLabelsSample.x)), pack8888(vec4f(meansUSample, shLabelsSample.y)), pack8888(vec4f(quatsSample.xyz, alpha)), (scale << 2u) | qmode ); output.color1 = unpack8888(sh0); return output; } `; var glslGsplatSogsReorderSh = ` #include "gsplatPackingPS" uniform highp sampler2D sh_centroids; uniform vec4 shN_codebook[64]; void main(void) { ivec2 uv = ivec2(gl_FragCoord.xy); vec3 shNSample = texelFetch(sh_centroids, uv, 0).xyz; #ifdef REORDER_V1 pcFragColor0 = unpack8888(pack111110(shNSample)); #else pcFragColor0 = unpack8888(pack111110(resolveCodebook(shNSample, shN_codebook))); #endif } `; var gsplatPackingPS$1 = ` uint pack8888(vec4 v) { uvec4 t = uvec4(v * 255.0) << uvec4(24u, 16u, 8u, 0u); return t.x | t.y | t.z | t.w; } uint pack101010(vec3 v) { uvec3 t = uvec3(v * 1023.0) << uvec3(20u, 10u, 0u); return t.x | t.y | t.z; } uint pack111110(vec3 v) { uvec3 t = uvec3(v * vec3(2047.0, 2047.0, 1023.0)) << uvec3(21u, 10u, 0u); return t.x | t.y | t.z; } vec4 unpack8888(uint v) { return vec4((uvec4(v) >> uvec4(24u, 16u, 8u, 0u)) & 0xffu) / 255.0; } vec3 unpack101010(uint v) { return vec3((uvec3(v) >> uvec3(20u, 10u, 0u)) & 0x3ffu) / 1023.0; } vec3 unpack111110(uint v) { return vec3((uvec3(v) >> uvec3(21u, 10u, 0u)) & uvec3(0x7ffu, 0x7ffu, 0x3ffu)) / vec3(2047.0, 2047.0, 1023.0); } vec3 resolveCodebook(vec3 s, vec4 codebook[64]) { uvec3 idx = uvec3(s * 255.0); vec3 v = vec3( codebook[idx.x >> 2u][idx.x & 3u], codebook[idx.y >> 2u][idx.y & 3u], codebook[idx.z >> 2u][idx.z & 3u] ); return (v - codebook[0].x) / (codebook[63].w - codebook[0].x); } `; var wgslGsplatSogsReorderSH = ` #include "gsplatPackingPS" var sh_centroids: texture_2d; uniform shN_codebook: array; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; var uv = vec2i(input.position.xy); var shNSample = textureLoad(sh_centroids, uv, 0).xyz; #ifdef REORDER_V1 output.color = unpack8888(pack111110(shNSample)); #else output.color = unpack8888(pack111110(resolveCodebook(shNSample, &uniform.shN_codebook))); #endif return output; } `; var gsplatPackingPS = ` fn pack8888(v: vec4f) -> u32 { let t = vec4u(v * 255.0) << vec4u(24u, 16u, 8u, 0u); return t.x | t.y | t.z | t.w; } fn pack101010(v: vec3f) -> u32 { let t = vec3u(v * vec3f(1023.0, 1023.0, 1023.0)) << vec3u(20u, 10u, 0u); return t.x | t.y | t.z; } fn pack111110(v: vec3f) -> u32 { let t = vec3u(v * vec3f(2047.0, 2047.0, 1023.0)) << vec3u(21u, 10u, 0u); return t.x | t.y | t.z; } fn unpack8888(v: u32) -> vec4f { return vec4f((vec4u(v) >> vec4u(24u, 16u, 8u, 0u)) & vec4u(0xffu)) / 255.0; } fn unpack101010(v: u32) -> vec3f { return vec3f((vec3u(v) >> vec3u(20u, 10u, 0u)) & vec3u(0x3ffu)) / 1023.0; } fn unpack111110(v: u32) -> vec3f { return vec3f((vec3u(v) >> vec3u(21u, 10u, 0u)) & vec3u(0x7ffu, 0x7ffu, 0x3ffu)) / vec3f(2047.0, 2047.0, 1023.0); } fn resolveCodebook(s: vec3f, codebook: ptr>) -> vec3f { let idx = vec3u(s * 255.0); let v = vec3f( codebook[idx.x >> 2u][idx.x & 3u], codebook[idx.y >> 2u][idx.y & 3u], codebook[idx.z >> 2u][idx.z & 3u] ); return (v - codebook[0].x) / (codebook[63].w - codebook[0].x); } `; var glslSogsCentersPS = ` #include "gsplatPackingPS" uniform highp sampler2D means_l; uniform highp sampler2D means_u; uniform highp uint numSplats; uniform highp vec3 means_mins; uniform highp vec3 means_maxs; void main(void) { int w = int(textureSize(means_l, 0).x); ivec2 uv = ivec2(gl_FragCoord.xy); if (uint(uv.x + uv.y * w) >= numSplats) { discard; } vec3 l = texelFetch(means_l, uv, 0).xyz; vec3 u = texelFetch(means_u, uv, 0).xyz; vec3 n = (l + u * 256.0) / 257.0; vec3 v = mix(means_mins, means_maxs, n); vec3 center = sign(v) * (exp(abs(v)) - 1.0); pcFragColor0 = uvec4(floatBitsToUint(center), 0u); } `; var wgslSogsCentersPS = ` var means_l: texture_2d; var means_u: texture_2d; uniform numSplats: u32; uniform means_mins: vec3f; uniform means_maxs: vec3f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let w: u32 = textureDimensions(means_l, 0).x; let uv: vec2 = vec2(input.position.xy); if (u32(uv.x + uv.y * i32(w)) >= uniform.numSplats) { discard; return output; } let l: vec3f = textureLoad(means_l, uv, 0).xyz; let u: vec3f = textureLoad(means_u, uv, 0).xyz; let n: vec3f = (l + u * 256.0) / 257.0; let v: vec3f = mix(uniform.means_mins, uniform.means_maxs, n); let center: vec3f = sign(v) * (exp(abs(v)) - 1.0); let packed: vec4 = bitcast>(vec4f(center, 0.0)); output.color = packed; return output; } `; const SH_C0$1 = 0.28209479177387814; const readImageDataAsync = (texture)=>{ if (texture.device.isNull) { return new Promise((resolve)=>{ resolve(new Uint8Array(texture.width * texture.height * 4)); }); } return texture.read(0, 0, texture.width, texture.height, { mipLevel: 0, face: 0, immediate: true }); }; const resolve = (scope, values)=>{ for(const key in values){ scope.resolve(key).setValue(values[key]); } }; class GSplatSogsIterator { constructor(data, p, r, s, c, sh){ const lerp = (a, b, t)=>a * (1 - t) + b * t; const { meta, shBands } = data; const { means, scales, sh0, shN } = meta; const means_l_data = p && data.means_l._levels[0]; const means_u_data = p && data.means_u._levels[0]; const quats_data = r && data.quats._levels[0]; const scales_data = s && data.scales._levels[0]; const sh0_data = c && data.sh0._levels[0]; const sh_labels_data = sh && data.sh_labels._levels[0]; const sh_centroids_data = sh && data.sh_centroids._levels[0]; const norm = Math.SQRT2; const coeffs = { 1: 3, 2: 8, 3: 15 }[shBands] ?? 0; this.read = (i)=>{ if (p) { const nx = lerp(means.mins[0], means.maxs[0], ((means_u_data[i * 4 + 0] << 8) + means_l_data[i * 4 + 0]) / 65535); const ny = lerp(means.mins[1], means.maxs[1], ((means_u_data[i * 4 + 1] << 8) + means_l_data[i * 4 + 1]) / 65535); const nz = lerp(means.mins[2], means.maxs[2], ((means_u_data[i * 4 + 2] << 8) + means_l_data[i * 4 + 2]) / 65535); p.x = Math.sign(nx) * (Math.exp(Math.abs(nx)) - 1); p.y = Math.sign(ny) * (Math.exp(Math.abs(ny)) - 1); p.z = Math.sign(nz) * (Math.exp(Math.abs(nz)) - 1); } if (r) { const a = (quats_data[i * 4 + 0] / 255 - 0.5) * norm; const b = (quats_data[i * 4 + 1] / 255 - 0.5) * norm; const c = (quats_data[i * 4 + 2] / 255 - 0.5) * norm; const d = Math.sqrt(Math.max(0, 1 - (a * a + b * b + c * c))); const mode = quats_data[i * 4 + 3] - 252; switch(mode){ case 0: r.set(a, b, c, d); break; case 1: r.set(d, b, c, a); break; case 2: r.set(b, d, c, a); break; case 3: r.set(b, c, d, a); break; } } if (s) { if (meta.version === 2) { const sx = scales.codebook[scales_data[i * 4 + 0]]; const sy = scales.codebook[scales_data[i * 4 + 1]]; const sz = scales.codebook[scales_data[i * 4 + 2]]; s.set(sx, sy, sz); } else { const sx = lerp(scales.mins[0], scales.maxs[0], scales_data[i * 4 + 0] / 255); const sy = lerp(scales.mins[1], scales.maxs[1], scales_data[i * 4 + 1] / 255); const sz = lerp(scales.mins[2], scales.maxs[2], scales_data[i * 4 + 2] / 255); s.set(sx, sy, sz); } } if (c) { if (meta.version === 2) { const r = sh0.codebook[sh0_data[i * 4 + 0]]; const g = sh0.codebook[sh0_data[i * 4 + 1]]; const b = sh0.codebook[sh0_data[i * 4 + 2]]; const a = sh0_data[i * 4 + 3] / 255; c.set(0.5 + r * SH_C0$1, 0.5 + g * SH_C0$1, 0.5 + b * SH_C0$1, a); } else { const r = lerp(sh0.mins[0], sh0.maxs[0], sh0_data[i * 4 + 0] / 255); const g = lerp(sh0.mins[1], sh0.maxs[1], sh0_data[i * 4 + 1] / 255); const b = lerp(sh0.mins[2], sh0.maxs[2], sh0_data[i * 4 + 2] / 255); const a = lerp(sh0.mins[3], sh0.maxs[3], sh0_data[i * 4 + 3] / 255); c.set(0.5 + r * SH_C0$1, 0.5 + g * SH_C0$1, 0.5 + b * SH_C0$1, 1.0 / (1.0 + Math.exp(-a))); } } if (sh) { const n = sh_labels_data[i * 4 + 0] + (sh_labels_data[i * 4 + 1] << 8); const u = n % 64 * coeffs; const v = Math.floor(n / 64); if (meta.version === 2) { for(let j = 0; j < 3; ++j){ for(let k = 0; k < coeffs; ++k){ sh[j * 15 + k] = shN.codebook[sh_centroids_data[(u + k) * 4 + j + v * data.sh_centroids.width * 4]]; } } } else { for(let j = 0; j < 3; ++j){ for(let k = 0; k < coeffs; ++k){ sh[j * 15 + k] = lerp(shN.mins, shN.maxs, sh_centroids_data[(u + k) * 4 + j + v * data.sh_centroids.width * 4] / 255); } } } } }; } } class GSplatSogsData { _destroyGpuResources() { this.means_l?.destroy(); this.means_u?.destroy(); this.quats?.destroy(); this.scales?.destroy(); this.sh0?.destroy(); this.sh_centroids?.destroy(); this.sh_labels?.destroy(); this.packedTexture?.destroy(); this.packedSh0?.destroy(); this.packedShN?.destroy(); } static calcBands(centroidsWidth) { const shBandsWidths = { 192: 1, 512: 2, 960: 3 }; return shBandsWidths[centroidsWidth] ?? 0; } destroy() { this.deviceRestoredEvent?.off(); this.deviceRestoredEvent = null; this.destroyed = true; this._destroyGpuResources(); } createIter(p, r, s, c, sh) { return new GSplatSogsIterator(this, p, r, s, c, sh); } calcAabb(result) { const { mins, maxs } = this.meta.means; const map = (v)=>Math.sign(v) * (Math.exp(Math.abs(v)) - 1); result.center.set((map(mins[0]) + map(maxs[0])) * 0.5, (map(mins[1]) + map(maxs[1])) * 0.5, (map(mins[2]) + map(maxs[2])) * 0.5); result.halfExtents.set((map(maxs[0]) - map(mins[0])) * 0.5, (map(maxs[1]) - map(mins[1])) * 0.5, (map(maxs[2]) - map(mins[2])) * 0.5); } getCenters() { const centers = this._centers; this._centers = null; return centers; } calcFocalPoint(result, pred) { const { mins, maxs } = this.meta.means; const map = (v)=>Math.sign(v) * (Math.exp(Math.abs(v)) - 1); result.set((map(mins[0]) + map(maxs[0])) * 0.5, (map(mins[1]) + map(maxs[1])) * 0.5, (map(mins[2]) + map(maxs[2])) * 0.5); } get isSogs() { return true; } async decompress() { const members = [ 'x', 'y', 'z', 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3' ]; const { shBands } = this; const { means_l, means_u, quats, scales, sh0, sh_labels, sh_centroids } = this; means_l._levels[0] = await readImageDataAsync(means_l); means_u._levels[0] = await readImageDataAsync(means_u); quats._levels[0] = await readImageDataAsync(quats); scales._levels[0] = await readImageDataAsync(scales); sh0._levels[0] = await readImageDataAsync(sh0); if (shBands > 0) { sh_labels._levels[0] = await readImageDataAsync(sh_labels); sh_centroids._levels[0] = await readImageDataAsync(sh_centroids); const shMembers = []; for(let i = 0; i < 45; ++i){ shMembers.push(`f_rest_${i}`); } members.splice(members.indexOf('f_dc_0') + 1, 0, ...shMembers); } const data = {}; members.forEach((name)=>{ data[name] = new Float32Array(this.numSplats); }); const p = new Vec3(); const r = new Quat(); const s = new Vec3(); const c = new Vec4(); const sh = shBands > 0 ? new Float32Array(45) : null; const iter = this.createIter(p, r, s, c, sh); for(let i = 0; i < this.numSplats; ++i){ iter.read(i); data.x[i] = p.x; data.y[i] = p.y; data.z[i] = p.z; data.rot_1[i] = r.x; data.rot_2[i] = r.y; data.rot_3[i] = r.z; data.rot_0[i] = r.w; data.scale_0[i] = s.x; data.scale_1[i] = s.y; data.scale_2[i] = s.z; data.f_dc_0[i] = (c.x - 0.5) / SH_C0$1; data.f_dc_1[i] = (c.y - 0.5) / SH_C0$1; data.f_dc_2[i] = (c.z - 0.5) / SH_C0$1; data.opacity[i] = c.w <= 0 ? -40 : c.w >= 1 ? 40 : -Math.log(1 / c.w - 1); if (sh) { for(let c = 0; c < 45; ++c){ data[`f_rest_${c}`][i] = sh[c]; } } } return new GSplatData([ { name: 'vertex', count: this.numSplats, properties: members.map((name)=>{ return { name: name, type: 'float', byteSize: 4, storage: data[name] }; }) } ]); } async generateCenters() { const { device, width, height } = this.means_l; const { scope } = device; const centersTexture = new Texture(device, { name: 'sogsCentersTexture', width, height, format: PIXELFORMAT_RGBA32U, mipmaps: false }); const shader = ShaderUtils.createShader(device, { uniqueName: 'GsplatSogsCentersShader', attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'fullscreenQuadVS', fragmentGLSL: glslSogsCentersPS, fragmentWGSL: wgslSogsCentersPS, fragmentOutputTypes: [ 'uvec4' ], fragmentIncludes: new Map([ [ 'gsplatPackingPS', device.isWebGPU ? gsplatPackingPS : gsplatPackingPS$1 ] ]) }); const renderTarget = new RenderTarget({ colorBuffer: centersTexture, depth: false, mipLevel: 0 }); device.setCullMode(CULLFACE_NONE); device.setBlendState(BlendState.NOBLEND); device.setDepthState(DepthState.NODEPTH); resolve(scope, { means_l: this.means_l, means_u: this.means_u, numSplats: this.numSplats, means_mins: this.meta.means.mins, means_maxs: this.meta.means.maxs }); drawQuadWithShader(device, renderTarget, shader); renderTarget.destroy(); const u32 = await readImageDataAsync(centersTexture); if (this.destroyed || device._destroyed) { centersTexture.destroy(); return; } const asFloat = new Float32Array(u32.buffer); const result = new Float32Array(this.numSplats * 3); for(let i = 0; i < this.numSplats; i++){ const base = i * 4; result[i * 3 + 0] = asFloat[base + 0]; result[i * 3 + 1] = asFloat[base + 1]; result[i * 3 + 2] = asFloat[base + 2]; } this._centers = result; centersTexture.destroy(); } packGpuMemory() { const { meta, means_l, means_u, quats, scales, sh0, sh_labels, numSplats } = this; const { device } = means_l; const { scope } = device; const shaderKey = meta.version === 2 ? 'v2' : 'v1'; const shader = ShaderUtils.createShader(device, { uniqueName: `GsplatSogsReorderShader-${shaderKey}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'fullscreenQuadVS', fragmentGLSL: glslGsplatSogsReorderPS, fragmentWGSL: wgslGsplatSogsReorderPS, fragmentOutputTypes: [ 'uvec4', 'vec4' ], fragmentIncludes: new Map([ [ 'gsplatPackingPS', device.isWebGPU ? gsplatPackingPS : gsplatPackingPS$1 ] ]), fragmentDefines: meta.version === 2 ? undefined : new Map([ [ 'REORDER_V1', '1' ] ]) }); const renderTarget = new RenderTarget({ colorBuffers: [ this.packedTexture, this.packedSh0 ], depth: false, mipLevel: 0 }); device.setCullMode(CULLFACE_NONE); device.setBlendState(BlendState.NOBLEND); device.setDepthState(DepthState.NODEPTH); resolve(scope, { means_l, means_u, quats, scales, sh0, sh_labels: sh_labels ?? means_l, numSplats, 'scales_codebook[0]': this.meta.scales.codebook, 'sh0_codebook[0]': this.meta.sh0.codebook, scalesMins: meta.scales.mins, scalesMaxs: meta.scales.maxs, sh0Mins: meta.sh0.mins, sh0Maxs: meta.sh0.maxs }); drawQuadWithShader(device, renderTarget, shader); renderTarget.destroy(); } packShMemory() { const { meta, sh_centroids } = this; const { device } = sh_centroids; const { scope } = device; const shaderKey = meta.version === 2 ? 'v2' : 'v1'; const shader = ShaderUtils.createShader(device, { uniqueName: `GsplatSogsReorderShShader-${shaderKey}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexChunk: 'fullscreenQuadVS', fragmentGLSL: glslGsplatSogsReorderSh, fragmentWGSL: wgslGsplatSogsReorderSH, fragmentIncludes: new Map([ [ 'gsplatPackingPS', device.isWebGPU ? gsplatPackingPS : gsplatPackingPS$1 ] ]), fragmentDefines: meta.version === 2 ? undefined : new Map([ [ 'REORDER_V1', '1' ] ]) }); const renderTarget = new RenderTarget({ colorBuffer: this.packedShN, depth: false, mipLevel: 0 }); device.setCullMode(CULLFACE_NONE); device.setBlendState(BlendState.NOBLEND); device.setDepthState(DepthState.NODEPTH); resolve(scope, { sh_centroids, 'shN_codebook[0]': this.meta.shN.codebook }); drawQuadWithShader(device, renderTarget, shader); renderTarget.destroy(); } async prepareGpuData() { let device = this.means_l.device; const { height, width } = this.means_l; if (this.destroyed || !device || device._destroyed) return; const urlSuffix = this.url ? `_${this.url}` : ''; this.packedTexture = new Texture(device, { name: `sogsPackedTexture${urlSuffix}`, width, height, format: PIXELFORMAT_RGBA32U, mipmaps: false }); this.packedSh0 = new Texture(device, { name: `sogsPackedSh0${urlSuffix}`, width, height, format: PIXELFORMAT_RGBA8, mipmaps: false }); this.packedShN = this.sh_centroids && new Texture(device, { name: `sogsPackedShN${urlSuffix}`, width: this.sh_centroids.width, height: this.sh_centroids.height, format: PIXELFORMAT_RGBA8, mipmaps: false }); if (!this.minimalMemory) { this.deviceRestoredEvent = device.on('devicerestored', ()=>{ this.packGpuMemory(); if (this.packedShN) { this.packShMemory(); } }); } [ 'scales', 'sh0', 'shN' ].forEach((name)=>{ const codebook = this.meta[name]?.codebook; if (codebook?.[0] === null) { codebook[0] = codebook[1] + (codebook[1] - codebook[255]) / 255; } }); device = this.means_l?.device; if (this.destroyed || !device || device._destroyed) return; await this.generateCenters(); device = this.means_l?.device; if (this.destroyed || !device || device._destroyed) return; this.packGpuMemory(); if (this.packedShN) { device = this.means_l?.device; if (this.destroyed || !device || device._destroyed) return; this.packShMemory(); } if (this.minimalMemory) { this.means_l?.destroy(); this.means_u?.destroy(); this.quats?.destroy(); this.scales?.destroy(); this.sh0?.destroy(); this.sh_centroids?.destroy(); this.sh_labels?.destroy(); this.means_l = null; this.means_u = null; this.quats = null; this.scales = null; this.sh0 = null; this.sh_centroids = null; this.sh_labels = null; } } reorderData() { return this.prepareGpuData(); } constructor(){ this.url = ''; this.minimalMemory = false; this.deviceRestoredEvent = null; this._centers = null; this.destroyed = false; this.shBands = 0; } } const mat = new Mat4(); const cameraPosition$1 = new Vec3(); const cameraDirection$1 = new Vec3(); class GSplatInstance { destroy() { this.resource?.releaseMesh(); this.orderTexture?.destroy(); this.resolveSH?.destroy(); this.material?.destroy(); this.meshInstance?.destroy(); this.sorter?.destroy(); } setMaterialOrderTexture(material) { material.setParameter('splatOrder', this.orderTexture); material.setParameter('splatTextureSize', this.orderTexture.width); } set material(value) { if (this._material !== value) { this._material = value; this.setMaterialOrderTexture(this._material); if (this.meshInstance) { this.meshInstance.material = value; } } } get material() { return this._material; } configureMaterial(material, options = {}) { this.resource.configureMaterial(material); material.setParameter('numSplats', 0); this.setMaterialOrderTexture(material); material.setParameter('alphaClip', 0.3); material.setDefine(`DITHER_${options.dither ? 'BLUENOISE' : 'NONE'}`, ''); material.cull = CULLFACE_NONE; material.blendType = options.dither ? BLEND_NONE : BLEND_PREMULTIPLIED; material.depthWrite = !!options.dither; } sort(cameraNode) { if (this.sorter) { const cameraMat = cameraNode.getWorldTransform(); cameraMat.getTranslation(cameraPosition$1); cameraMat.getZ(cameraDirection$1); const modelMat = this.meshInstance.node.getWorldTransform(); const invModelMat = mat.invert(modelMat); invModelMat.transformPoint(cameraPosition$1, cameraPosition$1); invModelMat.transformVector(cameraDirection$1, cameraDirection$1); if (!cameraPosition$1.equalsApprox(this.lastCameraPosition) || !cameraDirection$1.equalsApprox(this.lastCameraDirection)) { this.lastCameraPosition.copy(cameraPosition$1); this.lastCameraDirection.copy(cameraDirection$1); this.sorter.setCamera(cameraPosition$1, cameraDirection$1); } } } update() { if (this.cameras.length > 0) { const camera = this.cameras[0]; this.sort(camera._node); this.resolveSH?.render(camera._node, this.meshInstance.node.getWorldTransform()); this.cameras.length = 0; } } setHighQualitySH(value) { const { resource } = this; const { gsplatData } = resource; if (gsplatData instanceof GSplatSogsData && gsplatData.shBands > 0 && value === !!this.resolveSH) { if (this.resolveSH) { this.resolveSH.destroy(); this.resolveSH = null; } else { this.resolveSH = new GSplatResolveSH(resource.device, this); } } } constructor(resource, options = {}){ this.options = {}; this.sorter = null; this.lastCameraPosition = new Vec3(); this.lastCameraDirection = new Vec3(); this.resolveSH = null; this.cameras = []; this.resource = resource; this.orderTexture = resource.createTexture('splatOrder', PIXELFORMAT_R32U, resource.evalTextureSize(resource.numSplats)); if (options.material) { this._material = options.material; this.setMaterialOrderTexture(this._material); } else { this._material = new ShaderMaterial({ uniqueName: 'SplatMaterial', vertexGLSL: '#include "gsplatVS"', fragmentGLSL: '#include "gsplatPS"', vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION, vertex_id_attrib: SEMANTIC_ATTR13 } }); this.configureMaterial(this._material); this._material.update(); } resource.ensureMesh(); this.meshInstance = new MeshInstance(resource.mesh, this._material); this.meshInstance.setInstancing(resource.instanceIndices, true); this.meshInstance.gsplatInstance = this; this.meshInstance.instancingCount = 0; const centers = resource.centers.slice(); const chunks = resource.chunks?.slice(); this.sorter = new GSplatSorter(options.scene); this.sorter.init(this.orderTexture, centers, chunks); this.sorter.on('updated', (count)=>{ this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize); this.material.setParameter('numSplats', count); }); this.setHighQualitySH(options.highQualitySH ?? false); } } class GSplatSogsResource extends GSplatResourceBase { destroy() { this.gsplatData.destroy(); super.destroy(); } configureMaterialDefines(defines) { defines.set('GSPLAT_SOGS_DATA', true); defines.set('SH_BANDS', this.gsplatData.shBands); } configureMaterial(material) { const { gsplatData } = this; const { meta } = gsplatData; this.configureMaterialDefines(material.defines); [ 'packedTexture', 'packedSh0', 'packedShN' ].forEach((name)=>{ if (gsplatData[name]) { material.setParameter(name, gsplatData[name]); } }); [ 'means' ].forEach((name)=>{ const v = meta[name]; if (v) { material.setParameter(`${name}_mins`, v.mins); material.setParameter(`${name}_maxs`, v.maxs); } }); if (meta.version === 2) { [ 'scales', 'sh0', 'shN' ].forEach((name)=>{ const v = meta[name]; if (v) { material.setParameter(`${name}_mins`, v.codebook[0]); material.setParameter(`${name}_maxs`, v.codebook[255]); } }); } else { [ 'scales', 'sh0' ].forEach((name)=>{ const v = meta[name]; if (v) { material.setParameter(`${name}_mins`, Math.min(...v.mins.slice(0, 3))); material.setParameter(`${name}_maxs`, Math.max(...v.maxs.slice(0, 3))); } }); [ 'shN' ].forEach((name)=>{ const v = meta[name]; if (v) { material.setParameter(`${name}_mins`, v.mins); material.setParameter(`${name}_maxs`, v.maxs); } }); } } evalTextureSize(count) { return new Vec2(this.gsplatData.means_l.width, this.gsplatData.means_l.height); } } const FILLMODE_FILL_WINDOW = 'FILL_WINDOW'; const FILLMODE_KEEP_ASPECT = 'KEEP_ASPECT'; const RESOLUTION_AUTO = 'AUTO'; const RESOLUTION_FIXED = 'FIXED'; let currentApplication; function getApplication() { return currentApplication; } function setApplication(app) { currentApplication = app; } class FrameGraph { addRenderPass(renderPass) { renderPass.frameUpdate(); const beforePasses = renderPass.beforePasses; for(let i = 0; i < beforePasses.length; i++){ const pass = beforePasses[i]; if (pass.enabled) { this.addRenderPass(pass); } } if (renderPass.enabled) { this.renderPasses.push(renderPass); } const afterPasses = renderPass.afterPasses; for(let i = 0; i < afterPasses.length; i++){ const pass = afterPasses[i]; if (pass.enabled) { this.addRenderPass(pass); } } } reset() { this.renderPasses.length = 0; } compile() { const renderTargetMap = this.renderTargetMap; const renderPasses = this.renderPasses; for(let i = 0; i < renderPasses.length; i++){ const renderPass = renderPasses[i]; const renderTarget = renderPass.renderTarget; if (renderTarget !== undefined) { const prevPass = renderTargetMap.get(renderTarget); if (prevPass) { const count = renderPass.colorArrayOps.length; for(let j = 0; j < count; j++){ const colorOps = renderPass.colorArrayOps[j]; if (!colorOps.clear) { prevPass.colorArrayOps[j].store = true; } } if (!renderPass.depthStencilOps.clearDepth) { prevPass.depthStencilOps.storeDepth = true; } if (!renderPass.depthStencilOps.clearStencil) { prevPass.depthStencilOps.storeStencil = true; } } renderTargetMap.set(renderTarget, renderPass); } } for(let i = 0; i < renderPasses.length - 1; i++){ const firstPass = renderPasses[i]; const firstRT = firstPass.renderTarget; const secondPass = renderPasses[i + 1]; const secondRT = secondPass.renderTarget; if (firstRT !== secondRT || firstRT === undefined) { continue; } if (secondPass.depthStencilOps.clearDepth || secondPass.depthStencilOps.clearStencil || secondPass.colorArrayOps.some((colorOps)=>colorOps.clear)) { continue; } if (firstPass.afterPasses.length > 0) { continue; } if (secondPass.beforePasses.length > 0) { continue; } firstPass._skipEnd = true; secondPass._skipStart = true; } let lastCubeTexture = null; let lastCubeRenderPass = null; for(let i = 0; i < renderPasses.length; i++){ const renderPass = renderPasses[i]; const renderTarget = renderPass.renderTarget; const thisTexture = renderTarget?.colorBuffer; if (thisTexture?.cubemap) { if (lastCubeTexture === thisTexture) { const count = lastCubeRenderPass.colorArrayOps.length; for(let j = 0; j < count; j++){ lastCubeRenderPass.colorArrayOps[j].mipmaps = false; } } lastCubeTexture = renderTarget.colorBuffer; lastCubeRenderPass = renderPass; } else if (renderPass.requiresCubemaps) { lastCubeTexture = null; lastCubeRenderPass = null; } } renderTargetMap.clear(); } render(device) { this.compile(); const renderPasses = this.renderPasses; for(let i = 0; i < renderPasses.length; i++){ renderPasses[i].render(); } } constructor(){ this.renderPasses = []; this.renderTargetMap = new Map(); } } class AreaLightCacheEntry { destroy() { this.texture0?.destroy(); this.texture1?.destroy(); } constructor(texture0, texture1){ this.texture0 = texture0; this.texture1 = texture1; } } const deviceCache = new DeviceCache(); class AreaLightLuts { static createTexture(device, format, size, postfix = '') { const tex = new Texture(device, { name: `AreaLightLUT${postfix}`, width: size, height: size, format: format, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, type: TEXTURETYPE_DEFAULT, magFilter: FILTER_LINEAR, minFilter: FILTER_NEAREST, anisotropy: 1, mipmaps: false }); return tex; } static applyTextures(device, texture1, texture2) { deviceCache.remove(device); deviceCache.get(device, ()=>{ return new AreaLightCacheEntry(texture1, texture1 === texture2 ? null : texture2); }); device.scope.resolve('areaLightsLutTex1').setValue(texture1); device.scope.resolve('areaLightsLutTex2').setValue(texture2); } static createPlaceholder(device) { const texture = AreaLightLuts.createTexture(device, PIXELFORMAT_RGBA16F, 2, 'placeholder'); const pixels = texture.lock(); pixels.fill(0); texture.unlock(); AreaLightLuts.applyTextures(device, texture, texture); } static set(device, ltcMat1, ltcMat2) { function buildTexture(device, data, format) { const texture = AreaLightLuts.createTexture(device, format, 64); texture.lock().set(data); texture.unlock(); return texture; } function convertToHalfFloat(data) { const count = data.length; const ret = new Uint16Array(count); const float2Half = FloatPacking.float2Half; for(let i = 0; i < count; i++){ ret[i] = float2Half(data[i]); } return ret; } const srcData1 = ltcMat1; const srcData2 = ltcMat2; const data1 = convertToHalfFloat(srcData1); const data2 = convertToHalfFloat(srcData2); const tex1 = buildTexture(device, data1, PIXELFORMAT_RGBA16F); const tex2 = buildTexture(device, data2, PIXELFORMAT_RGBA16F); AreaLightLuts.applyTextures(device, tex1, tex2); } } const DEFAULT_LOCALE = 'en-US'; const DEFAULT_LOCALE_FALLBACKS = { 'en': 'en-US', 'es': 'en-ES', 'zh': 'zh-CN', 'zh-HK': 'zh-TW', 'zh-TW': 'zh-HK', 'zh-MO': 'zh-HK', 'fr': 'fr-FR', 'de': 'de-DE', 'it': 'it-IT', 'ru': 'ru-RU', 'ja': 'ja-JP' }; const PLURALS = {}; function definePluralFn(locales, fn) { for(let i = 0, len = locales.length; i < len; i++){ PLURALS[locales[i]] = fn; } } function getLang(locale) { const idx = locale.indexOf('-'); if (idx !== -1) { return locale.substring(0, idx); } return locale; } function replaceLang(locale, desiredLang) { const idx = locale.indexOf('-'); if (idx !== -1) { return desiredLang + locale.substring(idx); } return desiredLang; } function findAvailableLocale(desiredLocale, availableLocales) { if (availableLocales[desiredLocale]) { return desiredLocale; } let fallback = DEFAULT_LOCALE_FALLBACKS[desiredLocale]; if (fallback && availableLocales[fallback]) { return fallback; } const lang = getLang(desiredLocale); fallback = DEFAULT_LOCALE_FALLBACKS[lang]; if (availableLocales[fallback]) { return fallback; } if (availableLocales[lang]) { return lang; } return DEFAULT_LOCALE; } definePluralFn([ 'ja', 'ko', 'th', 'vi', 'zh', 'id' ], (n)=>{ return 0; }); definePluralFn([ 'fa', 'hi' ], (n)=>{ if (n >= 0 && n <= 1) { return 0; } return 1; }); definePluralFn([ 'fr', 'pt' ], (n)=>{ if (n >= 0 && n < 2) { return 0; } return 1; }); definePluralFn([ 'da' ], (n)=>{ if (n === 1 || !Number.isInteger(n) && n >= 0 && n <= 1) { return 0; } return 1; }); definePluralFn([ 'de', 'en', 'it', 'el', 'es', 'tr', 'fi', 'sv', 'nb', 'no', 'ur' ], (n)=>{ if (n === 1) { return 0; } return 1; }); definePluralFn([ 'ru', 'uk' ], (n)=>{ if (Number.isInteger(n)) { const mod10 = n % 10; const mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) { return 0; } else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) { return 1; } else if (mod10 === 0 || mod10 >= 5 && mod10 <= 9 || mod100 >= 11 && mod100 <= 14) { return 2; } } return 3; }); definePluralFn([ 'pl' ], (n)=>{ if (Number.isInteger(n)) { if (n === 1) { return 0; } const mod10 = n % 10; const mod100 = n % 100; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) { return 1; } else if (mod10 >= 0 && mod10 <= 1 || mod10 >= 5 && mod10 <= 9 || mod100 >= 12 && mod100 <= 14) { return 2; } } return 3; }); definePluralFn([ 'ar' ], (n)=>{ if (n === 0) { return 0; } else if (n === 1) { return 1; } else if (n === 2) { return 2; } if (Number.isInteger(n)) { const mod100 = n % 100; if (mod100 >= 3 && mod100 <= 10) { return 3; } else if (mod100 >= 11 && mod100 <= 99) { return 4; } } return 5; }); const DEFAULT_PLURAL_FN = PLURALS[getLang(DEFAULT_LOCALE)]; function getPluralFn(lang) { return PLURALS[lang] || DEFAULT_PLURAL_FN; } const ABSOLUTE_URL = new RegExp('^' + '\\s*' + '(?:' + '(?:' + '[a-z]+[a-z0-9\\-+.]*' + ':' + ')?' + '//' + '|' + 'data:' + '|blob:' + ')', 'i'); class AssetFile { equals(other) { return this.url === other.url && this.filename === other.filename && this.hash === other.hash && this.size === other.size && this.opt === other.opt && this.contents === other.contents; } constructor(url = '', filename = '', hash = null, size = null, opt = null, contents = null){ this.url = url; this.filename = filename; this.hash = hash; this.size = size; this.opt = opt; this.contents = contents; } } let assetIdCounter = -1; const VARIANT_SUPPORT = { pvr: 'extCompressedTexturePVRTC', dxt: 'extCompressedTextureS3TC', etc2: 'extCompressedTextureETC', etc1: 'extCompressedTextureETC1', basis: 'canvas' }; const VARIANT_DEFAULT_PRIORITY = [ 'pvr', 'dxt', 'etc2', 'etc1', 'basis' ]; class Asset extends EventHandler { set name(value) { if (this._name === value) { return; } const old = this._name; this._name = value; this.fire('name', this, this._name, old); } get name() { return this._name; } set file(value) { if (value && value.variants && [ 'texture', 'textureatlas', 'bundle' ].indexOf(this.type) !== -1) { const app = this.registry?._loader?._app || getApplication(); const device = app?.graphicsDevice; if (device) { for(let i = 0, len = VARIANT_DEFAULT_PRIORITY.length; i < len; i++){ const variant = VARIANT_DEFAULT_PRIORITY[i]; if (value.variants[variant] && device[VARIANT_SUPPORT[variant]]) { value = value.variants[variant]; break; } if (app.enableBundles) { const bundles = app.bundles.listBundlesForAsset(this); if (bundles && bundles.find((b)=>{ return b?.file?.variants[variant]; })) { break; } } } } } const oldFile = this._file; const newFile = value ? new AssetFile(value.url, value.filename, value.hash, value.size, value.opt, value.contents) : null; if (!!newFile !== !!oldFile || newFile && !newFile.equals(oldFile)) { this._file = newFile; this.fire('change', this, 'file', newFile, oldFile); this.reload(); } } get file() { return this._file; } set data(value) { const old = this._data; this._data = value; if (value !== old) { this.fire('change', this, 'data', value, old); if (this.loaded) { this.registry._loader.patch(this, this.registry); } } } get data() { return this._data; } set resource(value) { const _old = this._resources[0]; this._resources[0] = value; this.fire('change', this, 'resource', value, _old); } get resource() { return this._resources[0]; } set resources(value) { const _old = this._resources; this._resources = value; this.fire('change', this, 'resources', value, _old); } get resources() { return this._resources; } set preload(value) { value = !!value; if (this._preload === value) { return; } this._preload = value; if (this._preload && !this.loaded && !this.loading && this.registry) { this.registry.load(this); } } get preload() { return this._preload; } set loadFaces(value) { value = !!value; if (!this.hasOwnProperty('_loadFaces') || value !== this._loadFaces) { this._loadFaces = value; if (this.loaded) { this.registry._loader.patch(this, this.registry); } } } get loadFaces() { return this._loadFaces; } getFileUrl() { const file = this.file; if (!file || !file.url) { return null; } let url = file.url; if (this.registry && this.registry.prefix && !ABSOLUTE_URL.test(url)) { url = this.registry.prefix + url; } if (this.type !== "script" && file.hash) { const separator = url.indexOf('?') !== -1 ? '&' : '?'; url += `${separator}t=${file.hash}`; } return url; } getAbsoluteUrl(relativePath) { if (relativePath.startsWith('blob:') || relativePath.startsWith('data:')) { return relativePath; } const base = path.getDirectory(this.file.url); return path.join(base, relativePath); } getLocalizedAssetId(locale) { locale = findAvailableLocale(locale, this._i18n); return this._i18n[locale] || null; } addLocalizedAssetId(locale, assetId) { this._i18n[locale] = assetId; this.fire('add:localized', locale, assetId); } removeLocalizedAssetId(locale) { const assetId = this._i18n[locale]; if (assetId) { delete this._i18n[locale]; this.fire('remove:localized', locale, assetId); } } ready(callback, scope) { scope = scope || this; if (this.loaded) { callback.call(scope, this); } else { this.once('load', (asset)=>{ callback.call(scope, asset); }); } } reload() { if (this.loaded) { this.loaded = false; this.registry.load(this); } } unload() { if (!this.loaded && this._resources.length === 0) { return; } this.fire('unload', this); this.registry.fire(`unload:${this.id}`, this); const old = this._resources; if (this.urlObject) { URL.revokeObjectURL(this.urlObject); this.urlObject = null; } this.resources = []; this.loaded = false; if (this.file) { this.registry._loader.clearCache(this.getFileUrl(), this.type); } for(let i = 0; i < old.length; ++i){ old[i]?.destroy?.(); } } static fetchArrayBuffer(loadUrl, callback, asset, maxRetries = 0) { if (asset?.file?.contents) { setTimeout(()=>{ callback(null, asset.file.contents); }); } else { http.get(loadUrl, { cache: true, responseType: 'arraybuffer', retry: maxRetries > 0, maxRetries: maxRetries, progress: asset }, callback); } } constructor(name, type, file, data = {}, options = {}){ super(), this._file = null, this._i18n = {}, this._preload = false, this._resources = [], this.id = assetIdCounter--, this.loaded = false, this.loading = false, this.options = {}, this.registry = null, this.tags = new Tags(this), this.urlObject = null; this._name = name || ''; this.type = type; this._data = data || {}; this.options = options || {}; if (file) this.file = file; } } Asset.EVENT_LOAD = 'load'; Asset.EVENT_UNLOAD = 'unload'; Asset.EVENT_REMOVE = 'remove'; Asset.EVENT_ERROR = 'error'; Asset.EVENT_CHANGE = 'change'; Asset.EVENT_PROGRESS = 'progress'; Asset.EVENT_ADDLOCALIZED = 'add:localized'; Asset.EVENT_REMOVELOCALIZED = 'remove:localized'; class TagsCache { addItem(item) { const tags = item.tags._list; for (const tag of tags){ this.add(tag, item); } } removeItem(item) { const tags = item.tags._list; for (const tag of tags){ this.remove(tag, item); } } add(tag, item) { if (this._index[tag] && this._index[tag].list.indexOf(item) !== -1) { return; } if (!this._index[tag]) { this._index[tag] = { list: [] }; if (this._key) { this._index[tag].keys = {}; } } this._index[tag].list.push(item); if (this._key) { this._index[tag].keys[item[this._key]] = item; } } remove(tag, item) { if (!this._index[tag]) { return; } if (this._key) { if (!this._index[tag].keys[item[this._key]]) { return; } } const ind = this._index[tag].list.indexOf(item); if (ind === -1) { return; } this._index[tag].list.splice(ind, 1); if (this._key) { delete this._index[tag].keys[item[this._key]]; } if (this._index[tag].list.length === 0) { delete this._index[tag]; } } find(args) { const index = {}; const items = []; let item, tag, tags, tagsRest, missingIndex; const sort = (a, b)=>{ return this._index[a].list.length - this._index[b].list.length; }; for(let i = 0; i < args.length; i++){ tag = args[i]; if (tag instanceof Array) { if (tag.length === 0) { continue; } if (tag.length === 1) { tag = tag[0]; } else { missingIndex = false; for(let t = 0; t < tag.length; t++){ if (!this._index[tag[t]]) { missingIndex = true; break; } } if (missingIndex) { continue; } tags = tag.slice(0).sort(sort); tagsRest = tags.slice(1); if (tagsRest.length === 1) { tagsRest = tagsRest[0]; } for(let n = 0; n < this._index[tags[0]].list.length; n++){ item = this._index[tags[0]].list[n]; if ((this._key ? !index[item[this._key]] : items.indexOf(item) === -1) && item.tags.has(tagsRest)) { if (this._key) { index[item[this._key]] = true; } items.push(item); } } continue; } } if (tag && typeof tag === 'string' && this._index[tag]) { for(let n = 0; n < this._index[tag].list.length; n++){ item = this._index[tag].list[n]; if (this._key) { if (!index[item[this._key]]) { index[item[this._key]] = true; items.push(item); } } else if (items.indexOf(item) === -1) { items.push(item); } } } } return items; } constructor(key = null){ this._index = {}; this._key = key; } } class AssetRegistry extends EventHandler { get loader() { return this._loader; } list(filters = {}) { const assets = Array.from(this._assets); if (filters.preload !== undefined) { return assets.filter((asset)=>asset.preload === filters.preload); } return assets; } add(asset) { if (this._assets.has(asset)) return; this._assets.add(asset); this._idToAsset.set(asset.id, asset); if (asset.file?.url) { this._urlToAsset.set(asset.file.url, asset); } if (!this._nameToAsset.has(asset.name)) { this._nameToAsset.set(asset.name, new Set()); } this._nameToAsset.get(asset.name).add(asset); asset.on('name', this._onNameChange, this); asset.registry = this; this._tags.addItem(asset); asset.tags.on('add', this._onTagAdd, this); asset.tags.on('remove', this._onTagRemove, this); this.fire('add', asset); this.fire(`add:${asset.id}`, asset); if (asset.file?.url) { this.fire(`add:url:${asset.file.url}`, asset); } if (asset.preload) { this.load(asset); } } remove(asset) { if (!this._assets.has(asset)) return false; this._assets.delete(asset); this._idToAsset.delete(asset.id); if (asset.file?.url) { this._urlToAsset.delete(asset.file.url); } asset.off('name', this._onNameChange, this); if (this._nameToAsset.has(asset.name)) { const items = this._nameToAsset.get(asset.name); items.delete(asset); if (items.size === 0) { this._nameToAsset.delete(asset.name); } } this._tags.removeItem(asset); asset.tags.off('add', this._onTagAdd, this); asset.tags.off('remove', this._onTagRemove, this); asset.fire('remove', asset); this.fire('remove', asset); this.fire(`remove:${asset.id}`, asset); if (asset.file?.url) { this.fire(`remove:url:${asset.file.url}`, asset); } return true; } get(id) { return this._idToAsset.get(Number(id)); } getByUrl(url) { return this._urlToAsset.get(url); } load(asset, options) { if ((asset.loading || asset.loaded) && !options?.force) { return; } const file = asset.file; const _fireLoad = ()=>{ this.fire('load', asset); this.fire(`load:${asset.id}`, asset); if (file && file.url) { this.fire(`load:url:${file.url}`, asset); } asset.fire('load', asset); }; const _opened = (resource)=>{ if (resource instanceof Array) { asset.resources = resource; } else { asset.resource = resource; } this._loader.patch(asset, this); if (asset.type === 'bundle') { const assetIds = asset.data.assets; for(let i = 0; i < assetIds.length; i++){ const assetInBundle = this._idToAsset.get(assetIds[i]); if (assetInBundle && !assetInBundle.loaded) { this.load(assetInBundle, { force: true }); } } if (asset.resource.loaded) { _fireLoad(); } else { this.fire('load:start', asset); this.fire(`load:start:${asset.id}`, asset); if (file && file.url) { this.fire(`load:start:url:${file.url}`, asset); } asset.fire('load:start', asset); asset.resource.on('load', _fireLoad); } } else { _fireLoad(); } }; const _loaded = (err, resource, extra)=>{ asset.loaded = true; asset.loading = false; if (err) { this.fire('error', err, asset); this.fire(`error:${asset.id}`, err, asset); asset.fire('error', err, asset); } else { if (asset.type === "script") { const handler = this._loader.getHandler("script"); if (handler._cache[asset.id] && handler._cache[asset.id].parentNode === document.head) { document.head.removeChild(handler._cache[asset.id]); } if (extra) { handler._cache[asset.id] = extra; } } _opened(resource); } }; if (file || asset.type === 'cubemap') { this.fire('load:start', asset); this.fire(`load:${asset.id}:start`, asset); asset.loading = true; const fileUrl = asset.getFileUrl(); if (asset.type === 'bundle') { const assetIds = asset.data.assets; for(let i = 0; i < assetIds.length; i++){ const assetInBundle = this._idToAsset.get(assetIds[i]); if (!assetInBundle) { continue; } if (assetInBundle.loaded || assetInBundle.resource || assetInBundle.loading) { continue; } assetInBundle.loading = true; } } this._loader.load(fileUrl, asset.type, _loaded, asset, options); } else { const resource = this._loader.open(asset.type, asset.data); asset.loaded = true; _opened(resource); } } loadFromUrl(url, type, callback) { this.loadFromUrlAndFilename(url, null, type, callback); } loadFromUrlAndFilename(url, filename, type, callback) { const name = path.getBasename(filename || url); const file = { filename: filename || name, url: url }; let asset = this.getByUrl(url); if (!asset) { asset = new Asset(name, type, file); this.add(asset); } else if (asset.loaded) { callback(asset.loadFromUrlError || null, asset); return; } const startLoad = (asset)=>{ asset.once('load', (loadedAsset)=>{ if (type === 'material') { this._loadTextures(loadedAsset, (err, textures)=>{ callback(err, loadedAsset); }); } else { callback(null, loadedAsset); } }); asset.once('error', (err)=>{ if (err) { this.loadFromUrlError = err; } callback(err, asset); }); this.load(asset); }; if (asset.resource) { callback(null, asset); } else if (type === 'model') { this._loadModel(asset, startLoad); } else { startLoad(asset); } } _loadModel(modelAsset, continuation) { const url = modelAsset.getFileUrl(); const ext = path.getExtension(url); if (ext === '.json' || ext === '.glb') { const dir = path.getDirectory(url); const basename = path.getBasename(url); const mappingUrl = path.join(dir, basename.replace(ext, '.mapping.json')); this._loader.load(mappingUrl, 'json', (err, data)=>{ if (err) { modelAsset.data = { mapping: [] }; continuation(modelAsset); } else { this._loadMaterials(modelAsset, data, (e, materials)=>{ modelAsset.data = data; continuation(modelAsset); }); } }); } else { continuation(modelAsset); } } _loadMaterials(modelAsset, mapping, callback) { const materials = []; let count = 0; const onMaterialLoaded = (err, materialAsset)=>{ this._loadTextures(materialAsset, (err, textures)=>{ materials.push(materialAsset); if (materials.length === count) { callback(null, materials); } }); }; for(let i = 0; i < mapping.mapping.length; i++){ const path = mapping.mapping[i].path; if (path) { count++; const url = modelAsset.getAbsoluteUrl(path); this.loadFromUrl(url, 'material', onMaterialLoaded); } } if (count === 0) { callback(null, materials); } } _loadTextures(materialAsset, callback) { const textures = []; let count = 0; const data = materialAsset.data; if (data.mappingFormat !== 'path') { callback(null, textures); return; } const onTextureLoaded = (err, texture)=>{ if (err) console.error(err); textures.push(texture); if (textures.length === count) { callback(null, textures); } }; const texParams = standardMaterialTextureParameters; for(let i = 0; i < texParams.length; i++){ const path = data[texParams[i]]; if (path && typeof path === 'string') { count++; const url = materialAsset.getAbsoluteUrl(path); this.loadFromUrl(url, 'texture', onTextureLoaded); } } if (count === 0) { callback(null, textures); } } _onTagAdd(tag, asset) { this._tags.add(tag, asset); } _onTagRemove(tag, asset) { this._tags.remove(tag, asset); } _onNameChange(asset, name, nameOld) { if (this._nameToAsset.has(nameOld)) { const items = this._nameToAsset.get(nameOld); items.delete(asset); if (items.size === 0) { this._nameToAsset.delete(nameOld); } } if (!this._nameToAsset.has(asset.name)) { this._nameToAsset.set(asset.name, new Set()); } this._nameToAsset.get(asset.name).add(asset); } findByTag(...query) { return this._tags.find(query); } filter(callback) { return Array.from(this._assets).filter((asset)=>callback(asset)); } find(name, type) { const items = this._nameToAsset.get(name); if (!items) return null; for (const asset of items){ if (!type || asset.type === type) { return asset; } } return null; } findAll(name, type) { const items = this._nameToAsset.get(name); if (!items) return []; const results = Array.from(items); if (!type) return results; return results.filter((asset)=>asset.type === type); } log() {} constructor(loader){ super(), this._assets = new Set(), this._idToAsset = new Map(), this._urlToAsset = new Map(), this._nameToAsset = new Map(), this._tags = new TagsCache('id'), this.prefix = null, this.bundles = null; this._loader = loader; } } AssetRegistry.EVENT_LOAD = 'load'; AssetRegistry.EVENT_ADD = 'add'; AssetRegistry.EVENT_REMOVE = 'remove'; AssetRegistry.EVENT_ERROR = 'error'; class BundleRegistry { _onAssetAdd(asset) { if (asset.type === 'bundle') { this._idToBundle.set(asset.id, asset); this._assets.on(`load:start:${asset.id}`, this._onBundleLoadStart, this); this._assets.on(`load:${asset.id}`, this._onBundleLoad, this); this._assets.on(`error:${asset.id}`, this._onBundleError, this); const assetIds = asset.data.assets; for(let i = 0; i < assetIds.length; i++){ this._indexAssetInBundle(assetIds[i], asset); } } else { if (this._assetToBundles.has(asset.id)) { this._indexAssetFileUrls(asset); } } } _unbindAssetEvents(id) { this._assets.off(`load:start:${id}`, this._onBundleLoadStart, this); this._assets.off(`load:${id}`, this._onBundleLoad, this); this._assets.off(`error:${id}`, this._onBundleError, this); } _indexAssetInBundle(id, bundle) { let bundles = this._assetToBundles.get(id); if (!bundles) { bundles = new Set(); this._assetToBundles.set(id, bundles); } bundles.add(bundle); const asset = this._assets.get(id); if (asset) this._indexAssetFileUrls(asset); } _indexAssetFileUrls(asset) { const urls = this._getAssetFileUrls(asset); if (!urls) return; for(let i = 0; i < urls.length; i++){ const bundles = this._assetToBundles.get(asset.id); if (!bundles) continue; this._urlsToBundles.set(urls[i], bundles); } } _getAssetFileUrls(asset) { let url = asset.getFileUrl(); if (!url) return null; url = url.split('?')[0]; const urls = [ url ]; if (asset.type === 'font') { const numFiles = asset.data.info.maps.length; for(let i = 1; i < numFiles; i++){ urls.push(url.replace('.png', `${i}.png`)); } } return urls; } _onAssetRemove(asset) { if (asset.type === 'bundle') { this._idToBundle.delete(asset.id); this._unbindAssetEvents(asset.id); const assetIds = asset.data.assets; for(let i = 0; i < assetIds.length; i++){ const bundles = this._assetToBundles.get(assetIds[i]); if (!bundles) continue; bundles.delete(asset); if (bundles.size === 0) { this._assetToBundles.delete(assetIds[i]); for (const [url, otherBundles] of this._urlsToBundles){ if (otherBundles !== bundles) { continue; } this._urlsToBundles.delete(url); } } } this._onBundleError(`Bundle ${asset.id} was removed`); } else { const bundles = this._assetToBundles.get(asset.id); if (!bundles) return; this._assetToBundles.delete(asset.id); const urls = this._getAssetFileUrls(asset); if (!urls) return; for(let i = 0; i < urls.length; i++){ this._urlsToBundles.delete(urls[i]); } } } _onBundleLoadStart(asset) { asset.resource.on('add', (url, data)=>{ const callbacks = this._fileRequests.get(url); if (!callbacks) return; for(let i = 0; i < callbacks.length; i++){ callbacks[i](null, data); } this._fileRequests.delete(url); }); } _onBundleLoad(asset) { if (!asset.resource) { this._onBundleError(`Bundle ${asset.id} failed to load`); return; } if (!this._fileRequests) { return; } for (const [url, requests] of this._fileRequests){ const bundles = this._urlsToBundles.get(url); if (!bundles || !bundles.has(asset)) continue; const decodedUrl = decodeURIComponent(url); let err, data; if (asset.resource.has(decodedUrl)) { data = asset.resource.get(decodedUrl); } else if (asset.resource.loaded) { err = `Bundle ${asset.id} does not contain URL ${url}`; } else { continue; } for(let i = 0; i < requests.length; i++){ requests[i](err, err || data); } this._fileRequests.delete(url); } } _onBundleError(err) { for (const [url, requests] of this._fileRequests){ const bundle = this._findLoadedOrLoadingBundleForUrl(url); if (!bundle) { for(let i = 0; i < requests.length; i++){ requests[i](err); } this._fileRequests.delete(url); } } } _findLoadedOrLoadingBundleForUrl(url) { const bundles = this._urlsToBundles.get(url); if (!bundles) return null; let candidate = null; for (const bundle of bundles){ if (bundle.loaded && bundle.resource) { return bundle; } else if (bundle.loading) { candidate = bundle; } } return candidate; } listBundlesForAsset(asset) { const bundles = this._assetToBundles.get(asset.id); if (bundles) return Array.from(bundles); return null; } list() { return Array.from(this._idToBundle.values()); } hasUrl(url) { return this._urlsToBundles.has(url); } urlIsLoadedOrLoading(url) { return !!this._findLoadedOrLoadingBundleForUrl(url); } loadUrl(url, callback) { const bundle = this._findLoadedOrLoadingBundleForUrl(url); if (!bundle) { callback(`URL ${url} not found in any bundles`); return; } if (bundle.loaded) { const decodedUrl = decodeURIComponent(url); if (bundle.resource.has(decodedUrl)) { callback(null, bundle.resource.get(decodedUrl)); return; } else if (bundle.resource.loaded) { callback(`Bundle ${bundle.id} does not contain URL ${url}`); return; } } let callbacks = this._fileRequests.get(url); if (!callbacks) { callbacks = []; this._fileRequests.set(url, callbacks); } callbacks.push(callback); } destroy() { this._assets.off('add', this._onAssetAdd, this); this._assets.off('remove', this._onAssetRemove, this); for (const id of this._idToBundle.keys()){ this._unbindAssetEvents(id); } this._assets = null; this._idToBundle.clear(); this._idToBundle = null; this._assetToBundles.clear(); this._assetToBundles = null; this._urlsToBundles.clear(); this._urlsToBundles = null; this._fileRequests.clear(); this._fileRequests = null; } constructor(assets){ this._idToBundle = new Map(); this._assetToBundles = new Map(); this._urlsToBundles = new Map(); this._fileRequests = new Map(); this._assets = assets; this._assets.bundles = this; this._assets.on('add', this._onAssetAdd, this); this._assets.on('remove', this._onAssetRemove, this); } } class ComponentSystemRegistry extends EventHandler { add(system) { const id = system.id; if (this[id]) { throw new Error(`ComponentSystem name '${id}' already registered or not allowed`); } this[id] = system; this.list.push(system); } remove(system) { const id = system.id; if (!this[id]) { throw new Error(`No ComponentSystem named '${id}' registered`); } delete this[id]; const index = this.list.indexOf(this[id]); if (index !== -1) { this.list.splice(index, 1); } } destroy() { this.off(); for(let i = 0; i < this.list.length; i++){ this.list[i].destroy(); } } constructor(){ super(); this.list = []; } } class Bundle extends EventHandler { addFile(url, data) { if (this._index.has(url)) { return; } this._index.set(url, data); this.fire('add', url, data); } has(url) { return this._index.has(url); } get(url) { return this._index.get(url) || null; } destroy() { this._index.clear(); } set loaded(value) { if (!value || this._loaded) { return; } this._loaded = true; this.fire('load'); } get loaded() { return this._loaded; } constructor(...args){ super(...args), this._index = new Map(), this._loaded = false; } } Bundle.EVENT_ADD = 'add'; Bundle.EVENT_LOAD = 'load'; class Untar extends EventHandler { pump(done, value) { if (done) { this.fire('done'); return null; } this.bytesReceived += value.byteLength; const data = new Uint8Array(this.data.length + value.length); data.set(this.data); data.set(value, this.data.length); this.data = data; while(this.readFile()); return this.reader.read().then((res)=>{ this.pump(res.done, res.value); }).catch((err)=>{ this.fire('error', err); }); } readFile() { if (!this.headerRead && this.bytesReceived > this.bytesRead + this.headerSize) { this.headerRead = true; const view = new DataView(this.data.buffer, this.bytesRead, this.headerSize); this.decoder ?? (this.decoder = new TextDecoder('windows-1252')); const headers = this.decoder.decode(view); this.fileName = headers.substring(0, 100).replace(/\0/g, ''); this.fileSize = parseInt(headers.substring(124, 136), 8); this.fileType = headers.substring(156, 157); this.ustarFormat = headers.substring(257, 263); if (this.ustarFormat.indexOf('ustar') !== -1) { const prefix = headers.substring(345, 500).replace(/\0/g, ''); if (prefix.length > 0) { this.fileName = prefix.trim() + this.fileName.trim(); } } this.bytesRead += 512; } if (this.headerRead) { if (this.bytesReceived < this.bytesRead + this.fileSize) { return false; } if (this.fileType === '' || this.fileType === '0') { const dataView = new DataView(this.data.buffer, this.bytesRead, this.fileSize); const file = { name: this.prefix + this.fileName, size: this.fileSize, data: dataView }; this.fire('file', file); } this.bytesRead += this.fileSize; this.headerRead = false; const bytesRemained = this.bytesRead % this.paddingSize; if (bytesRemained !== 0) { this.bytesRead += this.paddingSize - bytesRemained; } return true; } return false; } constructor(fetchPromise, assetsPrefix = ''){ super(), this.headerSize = 512, this.paddingSize = 512, this.bytesRead = 0, this.bytesReceived = 0, this.headerRead = false, this.reader = null, this.data = new Uint8Array(0), this.decoder = null, this.prefix = '', this.fileName = '', this.fileSize = 0, this.fileType = '', this.ustarFormat = ''; this.prefix = assetsPrefix || ''; this.reader = fetchPromise.body.getReader(); this.reader.read().then((res)=>{ this.pump(res.done, res.value); }).catch((err)=>{ this.fire('error', err); }); } } class ResourceHandler { set maxRetries(value) { this._maxRetries = value; } get maxRetries() { return this._maxRetries; } load(url, callback, asset) {} open(url, data, asset) { return data; } patch(asset, assets) {} constructor(app, handlerType){ this.handlerType = ''; this._maxRetries = 0; this._app = app; this.handlerType = handlerType; } } class BundleHandler extends ResourceHandler { _fetchRetries(url, options, retries = 0) { return new Promise((resolve, reject)=>{ const tryFetch = ()=>{ fetch(url, options).then(resolve).catch((err)=>{ retries++; if (retries < this.maxRetries) { tryFetch(); } else { reject(err); } }); }; tryFetch(); }); } load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } this._fetchRetries(url.load, { mode: 'cors' }, this.maxRetries).then((res)=>{ const bundle = new Bundle(); callback(null, bundle); const untar = new Untar(res, this._assets.prefix); untar.on('file', (file)=>{ bundle.addFile(file.name, file.data); }); untar.on('done', ()=>{ bundle.loaded = true; }); untar.on('error', (err)=>{ callback(err); }); }).catch((err)=>{ callback(err); }); } open(url, bundle) { return bundle; } constructor(app){ super(app, 'bundle'); this._assets = app.assets; } } class ResourceLoader { addHandler(type, handler) { this._handlers[type] = handler; handler._loader = this; } removeHandler(type) { delete this._handlers[type]; } getHandler(type) { return this._handlers[type]; } static makeKey(url, type) { return `${url}-${type}`; } load(url, type, callback, asset, options) { const handler = this._handlers[type]; if (!handler) { const err = `No resource handler for asset type: '${type}' when loading [${url}]`; callback(err); return; } if (!url) { this._loadNull(handler, callback, asset); return; } const key = ResourceLoader.makeKey(url, type); if (this._cache[key] !== undefined) { callback(null, this._cache[key]); } else if (this._requests[key]) { this._requests[key].push(callback); } else { this._requests[key] = [ callback ]; const self = this; const handleLoad = function(err, urlObj) { if (err) { self._onFailure(key, err); return; } if (urlObj.load instanceof DataView) { if (handler.openBinary) { if (!self._requests[key]) { return; } try { const data = handler.openBinary(urlObj.load); self._onSuccess(key, data); } catch (err) { self._onFailure(key, err); } return; } urlObj.load = URL.createObjectURL(new Blob([ urlObj.load ])); if (asset) { if (asset.urlObject) { URL.revokeObjectURL(asset.urlObject); } asset.urlObject = urlObj.load; } } handler.load(urlObj, (err, data, extra)=>{ if (!self._requests[key]) { return; } if (err) { self._onFailure(key, err); return; } try { self._onSuccess(key, handler.open(urlObj.original, data, asset), extra); } catch (e) { self._onFailure(key, e); } }, asset); }; const normalizedUrl = url.split('?')[0]; if (this._app.enableBundles && this._app.bundles.hasUrl(normalizedUrl) && !(options && options.bundlesIgnore)) { if (!this._app.bundles.urlIsLoadedOrLoading(normalizedUrl)) { const bundles = this._app.bundles.listBundlesForAsset(asset); let bundle; if (options && options.bundlesFilter) { bundle = options.bundlesFilter(bundles); } if (!bundle) { bundles?.sort((a, b)=>{ return a.file.size - b.file.size; }); bundle = bundles?.[0]; } if (bundle) this._app.assets?.load(bundle); } this._app.bundles.loadUrl(normalizedUrl, (err, fileUrlFromBundle)=>{ handleLoad(err, { load: fileUrlFromBundle, original: normalizedUrl }); }); } else { handleLoad(null, { load: url, original: asset && asset.file.filename || url }); } } } _loadNull(handler, callback, asset) { const onLoad = function(err, data, extra) { if (err) { callback(err); } else { try { callback(null, handler.open(null, data, asset), extra); } catch (e) { callback(e); } } }; handler.load(null, onLoad, asset); } _onSuccess(key, result, extra) { if (result !== null) { this._cache[key] = result; } else { delete this._cache[key]; } for(let i = 0; i < this._requests[key].length; i++){ this._requests[key][i](null, result, extra); } delete this._requests[key]; } _onFailure(key, err) { console.error(err); if (this._requests[key]) { for(let i = 0; i < this._requests[key].length; i++){ this._requests[key][i](err); } delete this._requests[key]; } } open(type, data) { const handler = this._handlers[type]; if (!handler) { console.warn(`No resource handler found for: ${type}`); return data; } return handler.open(null, data); } patch(asset, assets) { const handler = this._handlers[asset.type]; if (!handler) { console.warn(`No resource handler found for: ${asset.type}`); return; } if (handler.patch) { handler.patch(asset, assets); } } clearCache(url, type) { const key = ResourceLoader.makeKey(url, type); delete this._cache[key]; } getFromCache(url, type) { const key = ResourceLoader.makeKey(url, type); if (this._cache[key]) { return this._cache[key]; } return undefined; } enableRetry(maxRetries = 5) { maxRetries = Math.max(0, maxRetries) || 0; for(const key in this._handlers){ this._handlers[key].maxRetries = maxRetries; } } disableRetry() { for(const key in this._handlers){ this._handlers[key].maxRetries = 0; } } destroy() { this._handlers = {}; this._requests = {}; this._cache = {}; } constructor(app){ this._handlers = {}; this._requests = {}; this._cache = {}; this._app = app; } } class I18nParser { _validate(data) { if (!data.header) { throw new Error('pc.I18n#addData: Missing "header" field'); } if (!data.header.version) { throw new Error('pc.I18n#addData: Missing "header.version" field'); } if (data.header.version !== 1) { throw new Error('pc.I18n#addData: Invalid "header.version" field'); } if (!data.data) { throw new Error('pc.I18n#addData: Missing "data" field'); } else if (!Array.isArray(data.data)) { throw new Error('pc.I18n#addData: "data" field must be an array'); } for(let i = 0, len = data.data.length; i < len; i++){ const entry = data.data[i]; if (!entry.info) { throw new Error(`pc.I18n#addData: missing "data[${i}].info" field`); } if (!entry.info.locale) { throw new Error(`pc.I18n#addData: missing "data[${i}].info.locale" field`); } if (typeof entry.info.locale !== 'string') { throw new Error(`pc.I18n#addData: "data[${i}].info.locale" must be a string`); } if (!entry.messages) { throw new Error(`pc.I18n#addData: missing "data[${i}].messages" field`); } } } parse(data) { return data.data; } } class I18n extends EventHandler { set assets(value) { const index = {}; for(let i = 0, len = value.length; i < len; i++){ const id = value[i] instanceof Asset ? value[i].id : value[i]; index[id] = true; } let i = this._assets.length; while(i--){ const id = this._assets[i]; if (!index[id]) { this._app.assets.off(`add:${id}`, this._onAssetAdd, this); const asset = this._app.assets.get(id); if (asset) { this._onAssetRemove(asset); } this._assets.splice(i, 1); } } for(const id in index){ const idNum = parseInt(id, 10); if (this._assets.indexOf(idNum) !== -1) continue; this._assets.push(idNum); const asset = this._app.assets.get(idNum); if (!asset) { this._app.assets.once(`add:${idNum}`, this._onAssetAdd, this); } else { this._onAssetAdd(asset); } } } get assets() { return this._assets; } set locale(value) { if (this._locale === value) { return; } let lang = getLang(value); if (lang === 'in') { lang = 'id'; value = replaceLang(value, lang); if (this._locale === value) { return; } } const old = this._locale; this._locale = value; this._lang = lang; this._pluralFn = getPluralFn(this._lang); this.fire(I18n.EVENT_CHANGE, value, old); } get locale() { return this._locale; } static findAvailableLocale(desiredLocale, availableLocales) { return findAvailableLocale(desiredLocale, availableLocales); } findAvailableLocale(desiredLocale) { if (this._translations[desiredLocale]) { return desiredLocale; } const lang = getLang(desiredLocale); return this._findFallbackLocale(desiredLocale, lang); } getText(key, locale) { let result = key; let lang; if (!locale) { locale = this._locale; lang = this._lang; } let translations = this._translations[locale]; if (!translations) { if (!lang) { lang = getLang(locale); } locale = this._findFallbackLocale(locale, lang); translations = this._translations[locale]; } if (translations && translations.hasOwnProperty(key)) { result = translations[key]; if (Array.isArray(result)) { result = result[0]; } if (result === null || result === undefined) { result = key; } } return result; } getPluralText(key, n, locale) { let result = key; let lang; let pluralFn; if (!locale) { locale = this._locale; lang = this._lang; pluralFn = this._pluralFn; } else { lang = getLang(locale); pluralFn = getPluralFn(lang); } let translations = this._translations[locale]; if (!translations) { locale = this._findFallbackLocale(locale, lang); lang = getLang(locale); pluralFn = getPluralFn(lang); translations = this._translations[locale]; } if (translations && translations[key] && pluralFn) { const index = pluralFn(n); result = translations[key][index]; if (result === null || result === undefined) { result = key; } } return result; } addData(data) { let parsed; try { parsed = this._parser.parse(data); } catch (err) { console.error(err); return; } for(let i = 0, len = parsed.length; i < len; i++){ const entry = parsed[i]; const locale = entry.info.locale; const messages = entry.messages; if (!this._translations[locale]) { this._translations[locale] = {}; const lang = getLang(locale); if (!this._availableLangs[lang]) { this._availableLangs[lang] = locale; } } Object.assign(this._translations[locale], messages); this.fire('data:add', locale, messages); } } removeData(data) { let parsed; try { parsed = this._parser.parse(data); } catch (err) { console.error(err); return; } for(let i = 0, len = parsed.length; i < len; i++){ const entry = parsed[i]; const locale = entry.info.locale; const translations = this._translations[locale]; if (!translations) continue; const messages = entry.messages; for(const key in messages){ delete translations[key]; } if (Object.keys(translations).length === 0) { delete this._translations[locale]; delete this._availableLangs[getLang(locale)]; } this.fire('data:remove', locale, messages); } } destroy() { this._translations = null; this._availableLangs = null; this._assets = null; this._parser = null; this.off(); } _findFallbackLocale(locale, lang) { let result = DEFAULT_LOCALE_FALLBACKS[locale]; if (result && this._translations[result]) { return result; } result = DEFAULT_LOCALE_FALLBACKS[lang]; if (result && this._translations[result]) { return result; } result = this._availableLangs[lang]; if (result && this._translations[result]) { return result; } return DEFAULT_LOCALE; } _onAssetAdd(asset) { asset.on('load', this._onAssetLoad, this); asset.on('change', this._onAssetChange, this); asset.on('remove', this._onAssetRemove, this); asset.on('unload', this._onAssetUnload, this); if (asset.resource) { this._onAssetLoad(asset); } } _onAssetLoad(asset) { this.addData(asset.resource); } _onAssetChange(asset) { if (asset.resource) { this.addData(asset.resource); } } _onAssetRemove(asset) { asset.off('load', this._onAssetLoad, this); asset.off('change', this._onAssetChange, this); asset.off('remove', this._onAssetRemove, this); asset.off('unload', this._onAssetUnload, this); if (asset.resource) { this.removeData(asset.resource); } this._app.assets.once(`add:${asset.id}`, this._onAssetAdd, this); } _onAssetUnload(asset) { if (asset.resource) { this.removeData(asset.resource); } } constructor(app){ super(); this.locale = DEFAULT_LOCALE; this._translations = {}; this._availableLangs = {}; this._app = app; this._assets = []; this._parser = new I18nParser(); } } I18n.EVENT_CHANGE = 'change'; class ScriptRegistry extends EventHandler { destroy() { this.app = null; this.off(); } addSchema(id, schema) { if (!schema) return; this._scriptSchemas.set(id, schema); } getSchema(id) { return this._scriptSchemas.get(id); } add(script) { const scriptName = script.__name; if (this._scripts.hasOwnProperty(scriptName)) { setTimeout(()=>{ if (script.prototype.swap) { const old = this._scripts[scriptName]; const ind = this._list.indexOf(old); this._list[ind] = script; this._scripts[scriptName] = script; this.fire('swap', scriptName, script); this.fire(`swap:${scriptName}`, script); } else { console.warn(`script registry already has '${scriptName}' script, define 'swap' method for new script type to enable code hot swapping`); } }); return false; } this._scripts[scriptName] = script; this._list.push(script); this.fire('add', scriptName, script); this.fire(`add:${scriptName}`, script); setTimeout(()=>{ if (!this._scripts.hasOwnProperty(scriptName)) { return; } if (!this.app || !this.app.systems || !this.app.systems.script) { return; } const components = this.app.systems.script._components; let attributes; const scriptInstances = []; const scriptInstancesInitialized = []; for(components.loopIndex = 0; components.loopIndex < components.length; components.loopIndex++){ const component = components.items[components.loopIndex]; if (component._scriptsIndex[scriptName] && component._scriptsIndex[scriptName].awaiting) { if (component._scriptsData && component._scriptsData[scriptName]) { attributes = component._scriptsData[scriptName].attributes; } const scriptInstance = component.create(scriptName, { preloading: true, ind: component._scriptsIndex[scriptName].ind, attributes: attributes }); if (scriptInstance) { scriptInstances.push(scriptInstance); } for (const script of component.scripts){ component.initializeAttributes(script); } } } for(let i = 0; i < scriptInstances.length; i++){ if (scriptInstances[i].enabled) { scriptInstances[i]._initialized = true; scriptInstancesInitialized.push(scriptInstances[i]); if (scriptInstances[i].initialize) { scriptInstances[i].initialize(); } } } for(let i = 0; i < scriptInstancesInitialized.length; i++){ if (!scriptInstancesInitialized[i].enabled || scriptInstancesInitialized[i]._postInitialized) { continue; } scriptInstancesInitialized[i]._postInitialized = true; if (scriptInstancesInitialized[i].postInitialize) { scriptInstancesInitialized[i].postInitialize(); } } }); return true; } remove(nameOrType) { let scriptType = nameOrType; let scriptName = nameOrType; if (typeof scriptName !== 'string') { scriptName = scriptType.__name; } else { scriptType = this.get(scriptName); } if (this.get(scriptName) !== scriptType) { return false; } delete this._scripts[scriptName]; const ind = this._list.indexOf(scriptType); this._list.splice(ind, 1); this.fire('remove', scriptName, scriptType); this.fire(`remove:${scriptName}`, scriptType); return true; } get(name) { return this._scripts[name] || null; } has(nameOrType) { if (typeof nameOrType === 'string') { return this._scripts.hasOwnProperty(nameOrType); } if (!nameOrType) return false; const scriptName = nameOrType.__name; return this._scripts[scriptName] === nameOrType; } list() { return this._list; } constructor(app){ super(), this._scripts = {}, this._list = [], this._scriptSchemas = new Map(); this.app = app; } } const cmpStaticOrder = (a, b)=>a.constructor.order - b.constructor.order; const sortStaticOrder = (arr)=>arr.sort(cmpStaticOrder); const _enableList = []; const tmpPool = []; const getTempArray = ()=>{ return tmpPool.pop() ?? []; }; const releaseTempArray = (a)=>{ a.length = 0; tmpPool.push(a); }; class Entity extends GraphNode { addComponent(type, data) { const system = this._app.systems[type]; if (!system) { return null; } if (this.c[type]) { return null; } return system.addComponent(this, data); } removeComponent(type) { const system = this._app.systems[type]; if (!system) { return; } if (!this.c[type]) { return; } system.removeComponent(this); } findComponent(type) { const entity = this.findOne((entity)=>entity.c?.[type]); return entity && entity.c[type]; } findComponents(type) { return this.find((entity)=>entity.c?.[type]).map((entity)=>entity.c[type]); } findScript(nameOrType) { const entity = this.findOne((node)=>node.c?.script?.has(nameOrType)); return entity?.c.script.get(nameOrType); } findScripts(nameOrType) { const entities = this.find((node)=>node.c?.script?.has(nameOrType)); return entities.map((entity)=>entity.c.script.get(nameOrType)); } getGuid() { if (!this._guid) { this.setGuid(guid.create()); } return this._guid; } setGuid(guid) { const index = this._app._entityIndex; if (this._guid) { delete index[this._guid]; } this._guid = guid; index[this._guid] = this; } _notifyHierarchyStateChanged(node, enabled) { let enableFirst = false; if (node === this && _enableList.length === 0) { enableFirst = true; } node._beingEnabled = true; node._onHierarchyStateChanged(enabled); if (node._onHierarchyStatePostChanged) { _enableList.push(node); } const c = node._children; for(let i = 0, len = c.length; i < len; i++){ if (c[i]._enabled) { this._notifyHierarchyStateChanged(c[i], enabled); } } node._beingEnabled = false; if (enableFirst) { for(let i = 0; i < _enableList.length; i++){ _enableList[i]._onHierarchyStatePostChanged(); } _enableList.length = 0; } } _onHierarchyStateChanged(enabled) { super._onHierarchyStateChanged(enabled); const components = this._getSortedComponents(); for(let i = 0; i < components.length; i++){ const component = components[i]; if (component.enabled) { if (enabled) { component.onEnable(); } else { component.onDisable(); } } } releaseTempArray(components); } _onHierarchyStatePostChanged() { const components = this._getSortedComponents(); for(let i = 0; i < components.length; i++){ components[i].onPostStateChange(); } releaseTempArray(components); } findByGuid(guid) { if (this._guid === guid) return this; const e = this._app._entityIndex[guid]; if (e && (e === this || e.isDescendantOf(this))) { return e; } return null; } destroy() { this._destroying = true; for(const name in this.c){ this.c[name].enabled = false; } for(const name in this.c){ this.c[name].system.removeComponent(this); } super.destroy(); if (this._guid) { delete this._app._entityIndex[this._guid]; } this._destroying = false; } clone() { const duplicatedIdsMap = {}; const clone = this._cloneRecursively(duplicatedIdsMap); duplicatedIdsMap[this.getGuid()] = clone; resolveDuplicatedEntityReferenceProperties(this, this, clone, duplicatedIdsMap); return clone; } _getSortedComponents() { const components = this.c; const sortedArray = getTempArray(); let needSort = 0; for(const type in components){ if (components.hasOwnProperty(type)) { const component = components[type]; needSort |= component.constructor.order !== 0; sortedArray.push(component); } } if (needSort && sortedArray.length > 1) { sortStaticOrder(sortedArray); } return sortedArray; } _cloneRecursively(duplicatedIdsMap) { const clone = new this.constructor(undefined, this._app); super._cloneInternal(clone); for(const type in this.c){ const component = this.c[type]; component.system.cloneComponent(this, clone); } for(let i = 0; i < this._children.length; i++){ const oldChild = this._children[i]; if (oldChild instanceof Entity) { const newChild = oldChild._cloneRecursively(duplicatedIdsMap); clone.addChild(newChild); duplicatedIdsMap[oldChild.getGuid()] = newChild; } } return clone; } constructor(name, app = getApplication()){ super(name), this.c = {}, this._destroying = false, this._guid = null, this._template = false; this._app = app; } } Entity.EVENT_DESTROY = 'destroy'; function resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, oldEntity, newEntity, duplicatedIdsMap) { if (oldEntity instanceof Entity) { const components = oldEntity.c; for(const componentName in components){ const component = components[componentName]; const entityProperties = component.system.getPropertiesOfType('entity'); for(let i = 0, len = entityProperties.length; i < len; i++){ const propertyDescriptor = entityProperties[i]; const propertyName = propertyDescriptor.name; const oldEntityReferenceId = component[propertyName]; const entityIsWithinOldSubtree = !!oldSubtreeRoot.findByGuid(oldEntityReferenceId); if (entityIsWithinOldSubtree) { const newEntityReferenceId = duplicatedIdsMap[oldEntityReferenceId].getGuid(); if (newEntityReferenceId) { newEntity.c[componentName][propertyName] = newEntityReferenceId; } } } } if (components.script) { newEntity.script.resolveDuplicatedEntityReferenceProperties(components.script, duplicatedIdsMap); } if (components.render) { newEntity.render.resolveDuplicatedEntityReferenceProperties(components.render, duplicatedIdsMap); } if (components.button) { newEntity.button.resolveDuplicatedEntityReferenceProperties(components.button, duplicatedIdsMap); } if (components.scrollview) { newEntity.scrollview.resolveDuplicatedEntityReferenceProperties(components.scrollview, duplicatedIdsMap); } if (components.scrollbar) { newEntity.scrollbar.resolveDuplicatedEntityReferenceProperties(components.scrollbar, duplicatedIdsMap); } if (components.anim) { newEntity.anim.resolveDuplicatedEntityReferenceProperties(components.anim, duplicatedIdsMap); } const _old = oldEntity.children.filter((e)=>e instanceof Entity); const _new = newEntity.children.filter((e)=>e instanceof Entity); for(let i = 0, len = _old.length; i < len; i++){ resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, _old[i], _new[i], duplicatedIdsMap); } } } class SceneRegistryItem { get loaded() { return !!this.data; } get loading() { return this._loading; } constructor(name, url){ this.data = null; this._loading = false; this._onLoadedCallbacks = []; this.name = name; this.url = url; } } class SceneRegistry { destroy() { this._app = null; } list() { return this._list; } add(name, url) { if (this._index.hasOwnProperty(name)) { return false; } const item = new SceneRegistryItem(name, url); const i = this._list.push(item); this._index[item.name] = i - 1; this._urlIndex[item.url] = i - 1; return true; } find(name) { if (this._index.hasOwnProperty(name)) { return this._list[this._index[name]]; } return null; } findByUrl(url) { if (this._urlIndex.hasOwnProperty(url)) { return this._list[this._urlIndex[url]]; } return null; } remove(name) { if (this._index.hasOwnProperty(name)) { const idx = this._index[name]; let item = this._list[idx]; delete this._urlIndex[item.url]; delete this._index[name]; this._list.splice(idx, 1); for(let i = 0; i < this._list.length; i++){ item = this._list[i]; this._index[item.name] = i; this._urlIndex[item.url] = i; } } } _loadSceneData(sceneItem, storeInCache, callback) { const app = this._app; let url = sceneItem; if (typeof sceneItem === 'string') { sceneItem = this.findByUrl(url) || this.find(url) || new SceneRegistryItem('Untitled', url); } url = sceneItem.url; if (!url) { callback('Cannot find scene to load'); return; } if (sceneItem.loaded) { callback(null, sceneItem); return; } if (app.assets && app.assets.prefix && !ABSOLUTE_URL.test(url)) { url = path.join(app.assets.prefix, url); } sceneItem._onLoadedCallbacks.push(callback); if (!sceneItem._loading) { const handler = app.loader.getHandler('hierarchy'); handler.load(url, (err, data)=>{ sceneItem.data = data; sceneItem._loading = false; for(let i = 0; i < sceneItem._onLoadedCallbacks.length; i++){ sceneItem._onLoadedCallbacks[i](err, sceneItem); } if (!storeInCache) { sceneItem.data = null; } sceneItem._onLoadedCallbacks.length = 0; }); } sceneItem._loading = true; } loadSceneData(sceneItem, callback) { this._loadSceneData(sceneItem, true, callback); } unloadSceneData(sceneItem) { if (typeof sceneItem === 'string') { sceneItem = this.findByUrl(sceneItem); } if (sceneItem) { sceneItem.data = null; } } _loadSceneHierarchy(sceneItem, onBeforeAddHierarchy, callback) { this._loadSceneData(sceneItem, false, (err, sceneItem)=>{ if (err) { if (callback) { callback(err); } return; } if (onBeforeAddHierarchy) { onBeforeAddHierarchy(sceneItem); } const app = this._app; const _loaded = ()=>{ const handler = app.loader.getHandler('hierarchy'); app.systems.script.preloading = true; const entity = handler.open(sceneItem.url, sceneItem.data); app.systems.script.preloading = false; app.loader.clearCache(sceneItem.url, 'hierarchy'); app.root.addChild(entity); app.systems.fire('initialize', entity); app.systems.fire('postInitialize', entity); app.systems.fire('postPostInitialize', entity); if (callback) callback(null, entity); }; app._preloadScripts(sceneItem.data, _loaded); }); } loadSceneHierarchy(sceneItem, callback) { this._loadSceneHierarchy(sceneItem, null, callback); } loadSceneSettings(sceneItem, callback) { this._loadSceneData(sceneItem, false, (err, sceneItem)=>{ if (!err) { this._app.applySceneSettings(sceneItem.data.settings); if (callback) { callback(null); } } else { if (callback) { callback(err); } } }); } changeScene(sceneItem, callback) { const app = this._app; const onBeforeAddHierarchy = (sceneItem)=>{ const { children } = app.root; while(children.length){ children[0].destroy(); } app.applySceneSettings(sceneItem.data.settings); }; this._loadSceneHierarchy(sceneItem, onBeforeAddHierarchy, callback); } loadScene(url, callback) { const app = this._app; const handler = app.loader.getHandler('scene'); if (app.assets && app.assets.prefix && !ABSOLUTE_URL.test(url)) { url = path.join(app.assets.prefix, url); } handler.load(url, (err, data)=>{ if (!err) { const _loaded = ()=>{ app.systems.script.preloading = true; const scene = handler.open(url, data); const sceneItem = this.findByUrl(url); if (sceneItem && !sceneItem.loaded) { sceneItem.data = data; } app.systems.script.preloading = false; app.loader.clearCache(url, 'scene'); app.loader.patch({ resource: scene, type: 'scene' }, app.assets); app.root.addChild(scene.root); if (app.systems.rigidbody && typeof Ammo !== 'undefined') { app.systems.rigidbody.gravity.set(scene._gravity.x, scene._gravity.y, scene._gravity.z); } if (callback) { callback(null, scene); } }; app._preloadScripts(data, _loaded); } else { if (callback) { callback(err); } } }); } constructor(app){ this._list = []; this._index = {}; this._urlIndex = {}; this._app = app; } } class ApplicationStats { get scene() { return getApplication().scene._stats; } get lightmapper() { return getApplication().lightmapper?.stats; } get batcher() { const batcher = getApplication()._batcher; return batcher ? batcher._stats : null; } frameEnd() { this.frame.gsplatSort = 0; } constructor(device){ this.frame = { fps: 0, ms: 0, dt: 0, updateStart: 0, updateTime: 0, renderStart: 0, renderTime: 0, physicsStart: 0, physicsTime: 0, scriptUpdateStart: 0, scriptUpdate: 0, scriptPostUpdateStart: 0, scriptPostUpdate: 0, animUpdateStart: 0, animUpdate: 0, cullTime: 0, sortTime: 0, skinTime: 0, morphTime: 0, instancingTime: 0, triangles: 0, gsplats: 0, gsplatSort: 0, otherPrimitives: 0, shaders: 0, materials: 0, cameras: 0, shadowMapUpdates: 0, shadowMapTime: 0, depthMapTime: 0, forwardTime: 0, lightClustersTime: 0, lightClusters: 0, _timeToCountFrames: 0, _fpsAccum: 0 }; this.drawCalls = { forward: 0, depth: 0, shadow: 0, immediate: 0, misc: 0, total: 0, skinned: 0, instanced: 0, removedByInstancing: 0 }; this.misc = { renderTargetCreationTime: 0 }; this.particles = { updatesPerFrame: 0, _updatesPerFrame: 0, frameTime: 0, _frameTime: 0 }; this.shaders = device._shaderStats; this.vram = device._vram; this.gpu = device.gpuProfiler?.passTimings ?? new Map(); Object.defineProperty(this.vram, 'totalUsed', { get: function() { return this.tex + this.vb + this.ib; } }); Object.defineProperty(this.vram, 'geom', { get: function() { return this.vb + this.ib; } }); } } var alphaTestPS$1 = ` uniform float alpha_ref; void alphaTest(float a) { if (a < alpha_ref) discard; } `; var ambientPS$1 = ` #ifdef LIT_AMBIENT_SOURCE == AMBIENTSH uniform vec3 ambientSH[9]; #endif #if LIT_AMBIENT_SOURCE == ENVALATLAS #include "envAtlasPS" #ifndef ENV_ATLAS #define ENV_ATLAS uniform sampler2D texture_envAtlas; #endif #endif void addAmbient(vec3 worldNormal) { #ifdef LIT_AMBIENT_SOURCE == AMBIENTSH vec3 n = cubeMapRotate(worldNormal); vec3 color = ambientSH[0] + ambientSH[1] * n.x + ambientSH[2] * n.y + ambientSH[3] * n.z + ambientSH[4] * n.x * n.z + ambientSH[5] * n.z * n.y + ambientSH[6] * n.y * n.x + ambientSH[7] * (3.0 * n.z * n.z - 1.0) + ambientSH[8] * (n.x * n.x - n.y * n.y); dDiffuseLight += processEnvironment(max(color, vec3(0.0))); #endif #if LIT_AMBIENT_SOURCE == ENVALATLAS vec3 dir = normalize(cubeMapRotate(worldNormal) * vec3(-1.0, 1.0, 1.0)); vec2 uv = mapUv(toSphericalUv(dir), vec4(128.0, 256.0 + 128.0, 64.0, 32.0) / atlasSize); vec4 raw = texture2D(texture_envAtlas, uv); vec3 linear = {ambientDecode}(raw); dDiffuseLight += processEnvironment(linear); #endif #if LIT_AMBIENT_SOURCE == CONSTANT dDiffuseLight += light_globalAmbient; #endif } `; var anisotropyPS$1 = ` #ifdef LIT_GGX_SPECULAR uniform float material_anisotropyIntensity; uniform vec2 material_anisotropyRotation; #endif void getAnisotropy() { dAnisotropy = 0.0; dAnisotropyRotation = vec2(1.0, 0.0); #ifdef LIT_GGX_SPECULAR dAnisotropy = material_anisotropyIntensity; dAnisotropyRotation = material_anisotropyRotation; #endif #ifdef STD_ANISOTROPY_TEXTURE vec3 anisotropyTex = texture2DBias({STD_ANISOTROPY_TEXTURE_NAME}, {STD_ANISOTROPY_TEXTURE_UV}, textureBias).rgb; dAnisotropy *= anisotropyTex.b; vec2 anisotropyRotationFromTex = anisotropyTex.rg * 2.0 - vec2(1.0); mat2 rotationMatrix = mat2(dAnisotropyRotation.x, dAnisotropyRotation.y, -dAnisotropyRotation.y, dAnisotropyRotation.x); dAnisotropyRotation = rotationMatrix * anisotropyRotationFromTex; #endif dAnisotropy = clamp(dAnisotropy, 0.0, 1.0); } `; var aoPS$1 = ` #if defined(STD_AO_TEXTURE) || defined(STD_AO_VERTEX) uniform float material_aoIntensity; #endif #ifdef STD_AODETAIL_TEXTURE #include "detailModesPS" #endif void getAO() { dAo = 1.0; #ifdef STD_AO_TEXTURE float aoBase = texture2DBias({STD_AO_TEXTURE_NAME}, {STD_AO_TEXTURE_UV}, textureBias).{STD_AO_TEXTURE_CHANNEL}; #ifdef STD_AODETAIL_TEXTURE float aoDetail = texture2DBias({STD_AODETAIL_TEXTURE_NAME}, {STD_AODETAIL_TEXTURE_UV}, textureBias).{STD_AODETAIL_TEXTURE_CHANNEL}; aoBase = detailMode_{STD_AODETAIL_DETAILMODE}(vec3(aoBase), vec3(aoDetail)).r; #endif dAo *= aoBase; #endif #ifdef STD_AO_VERTEX dAo *= saturate(vVertexColor.{STD_AO_VERTEX_CHANNEL}); #endif #if defined(STD_AO_TEXTURE) || defined(STD_AO_VERTEX) dAo = mix(1.0, dAo, material_aoIntensity); #endif } `; var aoDiffuseOccPS$1 = ` void occludeDiffuse(float ao) { dDiffuseLight *= ao; } `; var aoSpecOccPS$1 = ` #if LIT_OCCLUDE_SPECULAR != NONE #ifdef LIT_OCCLUDE_SPECULAR_FLOAT uniform float material_occludeSpecularIntensity; #endif #endif void occludeSpecular(float gloss, float ao, vec3 worldNormal, vec3 viewDir) { #if LIT_OCCLUDE_SPECULAR == AO #ifdef LIT_OCCLUDE_SPECULAR_FLOAT float specOcc = mix(1.0, ao, material_occludeSpecularIntensity); #else float specOcc = ao; #endif #endif #if LIT_OCCLUDE_SPECULAR == GLOSSDEPENDENT float specPow = exp2(gloss * 11.0); float specOcc = saturate(pow(dot(worldNormal, viewDir) + ao, 0.01 * specPow) - 1.0 + ao); #ifdef LIT_OCCLUDE_SPECULAR_FLOAT specOcc = mix(1.0, specOcc, material_occludeSpecularIntensity); #endif #endif #if LIT_OCCLUDE_SPECULAR != NONE dSpecularLight *= specOcc; dReflection *= specOcc; #ifdef LIT_SHEEN sSpecularLight *= specOcc; sReflection *= specOcc; #endif #endif } `; var bakeDirLmEndPS$1 = ` vec4 dirLm = texture2D(texture_dirLightMap, vUv1); if (bakeDir > 0.5) { if (dAtten > 0.00001) { dirLm.xyz = dirLm.xyz * 2.0 - vec3(1.0); dAtten = saturate(dAtten); gl_FragColor.rgb = normalize(dLightDirNormW.xyz*dAtten + dirLm.xyz*dirLm.w) * 0.5 + vec3(0.5); gl_FragColor.a = dirLm.w + dAtten; gl_FragColor.a = max(gl_FragColor.a, 1.0 / 255.0); } else { gl_FragColor = dirLm; } } else { gl_FragColor.rgb = dirLm.xyz; gl_FragColor.a = max(dirLm.w, dAtten > 0.00001 ? (1.0/255.0) : 0.0); } `; var bakeLmEndPS$1 = ` #ifdef LIT_LIGHTMAP_BAKING_ADD_AMBIENT dDiffuseLight = ((dDiffuseLight - 0.5) * max(ambientBakeOcclusionContrast + 1.0, 0.0)) + 0.5; dDiffuseLight += vec3(ambientBakeOcclusionBrightness); dDiffuseLight = saturate(dDiffuseLight); dDiffuseLight *= dAmbientLight; #endif #ifdef LIGHTMAP_RGBM gl_FragColor.rgb = dDiffuseLight; gl_FragColor.rgb = pow(gl_FragColor.rgb, vec3(0.5)); gl_FragColor.rgb /= 8.0; gl_FragColor.a = clamp( max( max( gl_FragColor.r, gl_FragColor.g ), max( gl_FragColor.b, 1.0 / 255.0 ) ), 0.0,1.0 ); gl_FragColor.a = ceil(gl_FragColor.a * 255.0) / 255.0; gl_FragColor.rgb /= gl_FragColor.a; #else gl_FragColor = vec4(dDiffuseLight, 1.0); #endif `; var basePS$1 = ` uniform vec3 view_position; uniform vec3 light_globalAmbient; float square(float x) { return x*x; } float saturate(float x) { return clamp(x, 0.0, 1.0); } vec3 saturate(vec3 x) { return clamp(x, vec3(0.0), vec3(1.0)); } `; var baseNineSlicedPS$1 = ` #define NINESLICED varying vec2 vMask; varying vec2 vTiledUv; uniform mediump vec4 innerOffset; uniform mediump vec2 outerScale; uniform mediump vec4 atlasRect; vec2 nineSlicedUv; `; var baseNineSlicedTiledPS$1 = ` #define NINESLICED #define NINESLICETILED varying vec2 vMask; varying vec2 vTiledUv; uniform mediump vec4 innerOffset; uniform mediump vec2 outerScale; uniform mediump vec4 atlasRect; vec2 nineSlicedUv; `; var bayerPS$1 = ` float bayer2(vec2 p) { return mod(2.0 * p.y + p.x + 1.0, 4.0); } float bayer4(vec2 p) { vec2 p1 = mod(p, 2.0); vec2 p2 = floor(0.5 * mod(p, 4.0)); return 4.0 * bayer2(p1) + bayer2(p2); } float bayer8(vec2 p) { vec2 p1 = mod(p, 2.0); vec2 p2 = floor(0.5 * mod(p, 4.0)); vec2 p4 = floor(0.25 * mod(p, 8.0)); return 4.0 * (4.0 * bayer2(p1) + bayer2(p2)) + bayer2(p4); } `; var blurVSMPS$1 = ` varying vec2 vUv0; uniform sampler2D source; uniform vec2 pixelOffset; #ifdef GAUSS uniform float weight[{SAMPLES}]; #endif void main(void) { vec3 moments = vec3(0.0); vec2 uv = vUv0 - pixelOffset * (float({SAMPLES}) * 0.5); for (int i = 0; i < {SAMPLES}; i++) { vec4 c = texture2D(source, uv + pixelOffset * float(i)); #ifdef GAUSS moments += c.xyz * weight[i]; #else moments += c.xyz; #endif } #ifndef GAUSS moments *= 1.0 / float({SAMPLES}); #endif gl_FragColor = vec4(moments.x, moments.y, moments.z, 1.0); } `; var clearCoatPS$1 = ` uniform float material_clearCoat; void getClearCoat() { ccSpecularity = material_clearCoat; #ifdef STD_CLEARCOAT_TEXTURE ccSpecularity *= texture2DBias({STD_CLEARCOAT_TEXTURE_NAME}, {STD_CLEARCOAT_TEXTURE_UV}, textureBias).{STD_CLEARCOAT_TEXTURE_CHANNEL}; #endif #ifdef STD_CLEARCOAT_VERTEX ccSpecularity *= saturate(vVertexColor.{STD_CLEARCOAT_VERTEX_CHANNEL}); #endif } `; var clearCoatGlossPS$1 = ` uniform float material_clearCoatGloss; void getClearCoatGlossiness() { ccGlossiness = material_clearCoatGloss; #ifdef STD_CLEARCOATGLOSS_TEXTURE ccGlossiness *= texture2DBias({STD_CLEARCOATGLOSS_TEXTURE_NAME}, {STD_CLEARCOATGLOSS_TEXTURE_UV}, textureBias).{STD_CLEARCOATGLOSS_TEXTURE_CHANNEL}; #endif #ifdef STD_CLEARCOATGLOSS_VERTEX ccGlossiness *= saturate(vVertexColor.{STD_CLEARCOATGLOSS_VERTEX_CHANNEL}); #endif #ifdef STD_CLEARCOATGLOSS_INVERT ccGlossiness = 1.0 - ccGlossiness; #endif ccGlossiness += 0.0000001; } `; var clearCoatNormalPS$1 = ` #ifdef STD_CLEARCOATNORMAL_TEXTURE uniform float material_clearCoatBumpiness; #endif void getClearCoatNormal() { #ifdef STD_CLEARCOATNORMAL_TEXTURE vec3 normalMap = {STD_CLEARCOATNORMAL_TEXTURE_DECODE}(texture2DBias({STD_CLEARCOATNORMAL_TEXTURE_NAME}, {STD_CLEARCOATNORMAL_TEXTURE_UV}, textureBias)); normalMap = mix(vec3(0.0, 0.0, 1.0), normalMap, material_clearCoatBumpiness); ccNormalW = normalize(dTBN * normalMap); #else ccNormalW = dVertexNormalW; #endif } `; var clusteredLightUtilsPS$1 = ` vec2 getCubemapFaceCoordinates(const vec3 dir, out float faceIndex, out vec2 tileOffset) { vec3 vAbs = abs(dir); float ma; vec2 uv; if (vAbs.z >= vAbs.x && vAbs.z >= vAbs.y) { faceIndex = dir.z < 0.0 ? 5.0 : 4.0; ma = 0.5 / vAbs.z; uv = vec2(dir.z < 0.0 ? -dir.x : dir.x, -dir.y); tileOffset.x = 2.0; tileOffset.y = dir.z < 0.0 ? 1.0 : 0.0; } else if(vAbs.y >= vAbs.x) { faceIndex = dir.y < 0.0 ? 3.0 : 2.0; ma = 0.5 / vAbs.y; uv = vec2(dir.x, dir.y < 0.0 ? -dir.z : dir.z); tileOffset.x = 1.0; tileOffset.y = dir.y < 0.0 ? 1.0 : 0.0; } else { faceIndex = dir.x < 0.0 ? 1.0 : 0.0; ma = 0.5 / vAbs.x; uv = vec2(dir.x < 0.0 ? dir.z : -dir.z, -dir.y); tileOffset.x = 0.0; tileOffset.y = dir.x < 0.0 ? 1.0 : 0.0; } return uv * ma + 0.5; } vec2 getCubemapAtlasCoordinates(const vec3 omniAtlasViewport, float shadowEdgePixels, float shadowTextureResolution, const vec3 dir) { float faceIndex; vec2 tileOffset; vec2 uv = getCubemapFaceCoordinates(dir, faceIndex, tileOffset); float atlasFaceSize = omniAtlasViewport.z; float tileSize = shadowTextureResolution * atlasFaceSize; float offset = shadowEdgePixels / tileSize; uv = uv * vec2(1.0 - offset * 2.0) + vec2(offset * 1.0); uv *= atlasFaceSize; uv += tileOffset * atlasFaceSize; uv += omniAtlasViewport.xy; return uv; } `; var clusteredLightCookiesPS$1 = ` vec3 _getCookieClustered(TEXTURE_ACCEPT(tex), vec2 uv, float intensity, vec4 cookieChannel) { vec4 pixel = mix(vec4(1.0), texture2DLod(tex, uv, 0.0), intensity); bool isRgb = dot(cookieChannel.rgb, vec3(1.0)) == 3.0; return isRgb ? pixel.rgb : vec3(dot(pixel, cookieChannel)); } vec3 getCookie2DClustered(TEXTURE_ACCEPT(tex), mat4 transform, vec3 worldPosition, float intensity, vec4 cookieChannel) { vec4 projPos = transform * vec4(worldPosition, 1.0); return _getCookieClustered(TEXTURE_PASS(tex), projPos.xy / projPos.w, intensity, cookieChannel); } vec3 getCookieCubeClustered(TEXTURE_ACCEPT(tex), vec3 dir, float intensity, vec4 cookieChannel, float shadowTextureResolution, float shadowEdgePixels, vec3 omniAtlasViewport) { vec2 uv = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, dir); return _getCookieClustered(TEXTURE_PASS(tex), uv, intensity, cookieChannel); } `; var clusteredLightShadowsPS$1 = ` vec3 _getShadowCoordPerspZbuffer(mat4 shadowMatrix, vec4 shadowParams, vec3 wPos) { vec4 projPos = shadowMatrix * vec4(wPos, 1.0); projPos.xyz /= projPos.w; return projPos.xyz; } vec3 getShadowCoordPerspZbufferNormalOffset(mat4 shadowMatrix, vec4 shadowParams, vec3 normal) { vec3 wPos = vPositionW + normal * shadowParams.y; return _getShadowCoordPerspZbuffer(shadowMatrix, shadowParams, wPos); } vec3 normalOffsetPointShadow(vec4 shadowParams, vec3 lightPos, vec3 lightDir, vec3 lightDirNorm, vec3 normal) { float distScale = length(lightDir); vec3 wPos = vPositionW + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; vec3 dir = wPos - lightPos; return dir; } #if defined(CLUSTER_SHADOW_TYPE_PCF1) float getShadowOmniClusteredPCF1(SHADOWMAP_ACCEPT(shadowMap), vec4 shadowParams, vec3 omniAtlasViewport, float shadowEdgePixels, vec3 lightDir) { float shadowTextureResolution = shadowParams.x; vec2 uv = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, lightDir); float shadowZ = length(lightDir) * shadowParams.w + shadowParams.z; return textureShadow(shadowMap, vec3(uv, shadowZ)); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF3) float getShadowOmniClusteredPCF3(SHADOWMAP_ACCEPT(shadowMap), vec4 shadowParams, vec3 omniAtlasViewport, float shadowEdgePixels, vec3 lightDir) { float shadowTextureResolution = shadowParams.x; vec2 uv = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, lightDir); float shadowZ = length(lightDir) * shadowParams.w + shadowParams.z; vec3 shadowCoord = vec3(uv, shadowZ); return getShadowPCF3x3(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF5) float getShadowOmniClusteredPCF5(SHADOWMAP_ACCEPT(shadowMap), vec4 shadowParams, vec3 omniAtlasViewport, float shadowEdgePixels, vec3 lightDir) { float shadowTextureResolution = shadowParams.x; vec2 uv = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, lightDir); float shadowZ = length(lightDir) * shadowParams.w + shadowParams.z; vec3 shadowCoord = vec3(uv, shadowZ); return getShadowPCF5x5(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF1) float getShadowSpotClusteredPCF1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return textureShadow(shadowMap, shadowCoord); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF3) float getShadowSpotClusteredPCF3(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return getShadowSpotPCF3x3(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF5) float getShadowSpotClusteredPCF5(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return getShadowPCF5x5(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams); } #endif `; var clusteredLightPS$1 = ` #include "lightBufferDefinesPS" #include "clusteredLightUtilsPS" #ifdef CLUSTER_COOKIES #include "clusteredLightCookiesPS" #endif #ifdef CLUSTER_SHADOWS #include "clusteredLightShadowsPS" #endif uniform highp usampler2D clusterWorldTexture; uniform highp sampler2D lightsTexture; #ifdef CLUSTER_SHADOWS uniform sampler2DShadow shadowAtlasTexture; #endif #ifdef CLUSTER_COOKIES uniform sampler2D cookieAtlasTexture; #endif uniform int clusterMaxCells; uniform int numClusteredLights; uniform int clusterTextureWidth; uniform vec3 clusterCellsCountByBoundsSize; uniform vec3 clusterBoundsMin; uniform vec3 clusterBoundsDelta; uniform ivec3 clusterCellsDot; uniform ivec3 clusterCellsMax; uniform vec2 shadowAtlasParams; struct ClusterLightData { uint flags; vec3 halfWidth; bool isSpot; vec3 halfHeight; int lightIndex; vec3 position; uint shape; vec3 direction; bool falloffModeLinear; vec3 color; float shadowIntensity; vec3 omniAtlasViewport; float range; vec4 cookieChannelMask; float biasesData; uint colorBFlagsData; float shadowBias; float shadowNormalBias; float anglesData; float innerConeAngleCos; float outerConeAngleCos; float cookieIntensity; bool isDynamic; bool isLightmapped; }; mat4 lightProjectionMatrix; vec4 sampleLightTextureF(const ClusterLightData clusterLightData, int index) { return texelFetch(lightsTexture, ivec2(index, clusterLightData.lightIndex), 0); } void decodeClusterLightCore(inout ClusterLightData clusterLightData, int lightIndex) { clusterLightData.lightIndex = lightIndex; vec4 halfData = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_COLOR_ANGLES_BIAS}); clusterLightData.anglesData = halfData.z; clusterLightData.biasesData = halfData.w; clusterLightData.colorBFlagsData = floatBitsToUint(halfData.y); vec2 colorRG = unpackHalf2x16(floatBitsToUint(halfData.x)); vec2 colorB_flags = unpackHalf2x16(clusterLightData.colorBFlagsData); clusterLightData.color = vec3(colorRG, colorB_flags.x) * {LIGHT_COLOR_DIVIDER}; vec4 lightPosRange = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_POSITION_RANGE}); clusterLightData.position = lightPosRange.xyz; clusterLightData.range = lightPosRange.w; vec4 lightDir_Flags = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_DIRECTION_FLAGS}); clusterLightData.direction = lightDir_Flags.xyz; clusterLightData.flags = floatBitsToUint(lightDir_Flags.w); clusterLightData.isSpot = (clusterLightData.flags & (1u << 30u)) != 0u; clusterLightData.shape = (clusterLightData.flags >> 28u) & 0x3u; clusterLightData.falloffModeLinear = (clusterLightData.flags & (1u << 27u)) == 0u; clusterLightData.shadowIntensity = float((clusterLightData.flags >> 0u) & 0xFFu) / 255.0; clusterLightData.cookieIntensity = float((clusterLightData.flags >> 8u) & 0xFFu) / 255.0; clusterLightData.isDynamic = (clusterLightData.flags & (1u << 22u)) != 0u; clusterLightData.isLightmapped = (clusterLightData.flags & (1u << 21u)) != 0u; } void decodeClusterLightSpot(inout ClusterLightData clusterLightData) { uint angleFlags = (clusterLightData.colorBFlagsData >> 16u) & 0xFFFFu; vec2 angleValues = unpackHalf2x16(floatBitsToUint(clusterLightData.anglesData)); float innerVal = angleValues.x; float outerVal = angleValues.y; float innerIsVersine = float(angleFlags & 1u); float outerIsVersine = float((angleFlags >> 1u) & 1u); clusterLightData.innerConeAngleCos = mix(innerVal, 1.0 - innerVal, innerIsVersine); clusterLightData.outerConeAngleCos = mix(outerVal, 1.0 - outerVal, outerIsVersine); } void decodeClusterLightOmniAtlasViewport(inout ClusterLightData clusterLightData) { clusterLightData.omniAtlasViewport = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_PROJ_MAT_0}).xyz; } void decodeClusterLightAreaData(inout ClusterLightData clusterLightData) { clusterLightData.halfWidth = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_AREA_DATA_WIDTH}).xyz; clusterLightData.halfHeight = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_AREA_DATA_HEIGHT}).xyz; } void decodeClusterLightProjectionMatrixData(inout ClusterLightData clusterLightData) { vec4 m0 = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_PROJ_MAT_0}); vec4 m1 = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_PROJ_MAT_1}); vec4 m2 = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_PROJ_MAT_2}); vec4 m3 = sampleLightTextureF(clusterLightData, {CLUSTER_TEXTURE_PROJ_MAT_3}); lightProjectionMatrix = mat4(m0, m1, m2, m3); } void decodeClusterLightShadowData(inout ClusterLightData clusterLightData) { vec2 biases = unpackHalf2x16(floatBitsToUint(clusterLightData.biasesData)); clusterLightData.shadowBias = biases.x; clusterLightData.shadowNormalBias = biases.y; } void decodeClusterLightCookieData(inout ClusterLightData clusterLightData) { uint cookieFlags = (clusterLightData.flags >> 23u) & 0x0Fu; clusterLightData.cookieChannelMask = vec4(uvec4(cookieFlags) & uvec4(1u, 2u, 4u, 8u)); clusterLightData.cookieChannelMask = step(1.0, clusterLightData.cookieChannelMask); } void evaluateLight( ClusterLightData light, vec3 worldNormal, vec3 viewDir, vec3 reflectionDir, #if defined(LIT_CLEARCOAT) vec3 clearcoatReflectionDir, #endif float gloss, vec3 specularity, vec3 geometricNormal, mat3 tbn, #if defined(LIT_IRIDESCENCE) vec3 iridescenceFresnel, #endif vec3 clearcoat_worldNormal, float clearcoat_gloss, float sheen_gloss, float iridescence_intensity ) { vec3 cookieAttenuation = vec3(1.0); float diffuseAttenuation = 1.0; float falloffAttenuation = 1.0; vec3 lightDirW = evalOmniLight(light.position); vec3 lightDirNormW = normalize(lightDirW); #ifdef CLUSTER_AREALIGHTS if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { decodeClusterLightAreaData(light); if (light.shape == {LIGHTSHAPE_RECT}) { calcRectLightValues(light.position, light.halfWidth, light.halfHeight); } else if (light.shape == {LIGHTSHAPE_DISK}) { calcDiskLightValues(light.position, light.halfWidth, light.halfHeight); } else { calcSphereLightValues(light.position, light.halfWidth, light.halfHeight); } falloffAttenuation = getFalloffWindow(light.range, lightDirW); } else #endif { if (light.falloffModeLinear) falloffAttenuation = getFalloffLinear(light.range, lightDirW); else falloffAttenuation = getFalloffInvSquared(light.range, lightDirW); } if (falloffAttenuation > 0.00001) { #ifdef CLUSTER_AREALIGHTS if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { if (light.shape == {LIGHTSHAPE_RECT}) { diffuseAttenuation = getRectLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0; } else if (light.shape == {LIGHTSHAPE_DISK}) { diffuseAttenuation = getDiskLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0; } else { diffuseAttenuation = getSphereLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0; } } else #endif { falloffAttenuation *= getLightDiffuse(worldNormal, viewDir, lightDirNormW); } if (light.isSpot) { decodeClusterLightSpot(light); falloffAttenuation *= getSpotEffect(light.direction, light.innerConeAngleCos, light.outerConeAngleCos, lightDirNormW); } #if defined(CLUSTER_COOKIES) || defined(CLUSTER_SHADOWS) if (falloffAttenuation > 0.00001) { if (light.shadowIntensity > 0.0 || light.cookieIntensity > 0.0) { if (light.isSpot) { decodeClusterLightProjectionMatrixData(light); } else { decodeClusterLightOmniAtlasViewport(light); } float shadowTextureResolution = shadowAtlasParams.x; float shadowEdgePixels = shadowAtlasParams.y; #ifdef CLUSTER_COOKIES if (light.cookieIntensity > 0.0) { decodeClusterLightCookieData(light); if (light.isSpot) { cookieAttenuation = getCookie2DClustered(TEXTURE_PASS(cookieAtlasTexture), lightProjectionMatrix, vPositionW, light.cookieIntensity, light.cookieChannelMask); } else { cookieAttenuation = getCookieCubeClustered(TEXTURE_PASS(cookieAtlasTexture), lightDirW, light.cookieIntensity, light.cookieChannelMask, shadowTextureResolution, shadowEdgePixels, light.omniAtlasViewport); } } #endif #ifdef CLUSTER_SHADOWS if (light.shadowIntensity > 0.0) { decodeClusterLightShadowData(light); vec4 shadowParams = vec4(shadowTextureResolution, light.shadowNormalBias, light.shadowBias, 1.0 / light.range); if (light.isSpot) { vec3 shadowCoord = getShadowCoordPerspZbufferNormalOffset(lightProjectionMatrix, shadowParams, geometricNormal); #if defined(CLUSTER_SHADOW_TYPE_PCF1) float shadow = getShadowSpotClusteredPCF1(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams); #elif defined(CLUSTER_SHADOW_TYPE_PCF3) float shadow = getShadowSpotClusteredPCF3(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams); #elif defined(CLUSTER_SHADOW_TYPE_PCF5) float shadow = getShadowSpotClusteredPCF5(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams); #elif defined(CLUSTER_SHADOW_TYPE_PCSS) float shadow = getShadowSpotClusteredPCSS(SHADOWMAP_PASS(shadowAtlasTexture), shadowCoord, shadowParams); #endif falloffAttenuation *= mix(1.0, shadow, light.shadowIntensity); } else { vec3 dir = normalOffsetPointShadow(shadowParams, light.position, lightDirW, lightDirNormW, geometricNormal); #if defined(CLUSTER_SHADOW_TYPE_PCF1) float shadow = getShadowOmniClusteredPCF1(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir); #elif defined(CLUSTER_SHADOW_TYPE_PCF3) float shadow = getShadowOmniClusteredPCF3(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir); #elif defined(CLUSTER_SHADOW_TYPE_PCF5) float shadow = getShadowOmniClusteredPCF5(SHADOWMAP_PASS(shadowAtlasTexture), shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir); #endif falloffAttenuation *= mix(1.0, shadow, light.shadowIntensity); } } #endif } } #endif #ifdef CLUSTER_AREALIGHTS if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { { vec3 areaDiffuse = (diffuseAttenuation * falloffAttenuation) * light.color * cookieAttenuation; #if defined(LIT_SPECULAR) areaDiffuse = mix(areaDiffuse, vec3(0), dLTCSpecFres); #endif dDiffuseLight += areaDiffuse; } #ifdef LIT_SPECULAR float areaLightSpecular; if (light.shape == {LIGHTSHAPE_RECT}) { areaLightSpecular = getRectLightSpecular(worldNormal, viewDir); } else if (light.shape == {LIGHTSHAPE_DISK}) { areaLightSpecular = getDiskLightSpecular(worldNormal, viewDir); } else { areaLightSpecular = getSphereLightSpecular(worldNormal, viewDir); } dSpecularLight += dLTCSpecFres * areaLightSpecular * falloffAttenuation * light.color * cookieAttenuation; #ifdef LIT_CLEARCOAT float areaLightSpecularCC; if (light.shape == {LIGHTSHAPE_RECT}) { areaLightSpecularCC = getRectLightSpecular(clearcoat_worldNormal, viewDir); } else if (light.shape == {LIGHTSHAPE_DISK}) { areaLightSpecularCC = getDiskLightSpecular(clearcoat_worldNormal, viewDir); } else { areaLightSpecularCC = getSphereLightSpecular(clearcoat_worldNormal, viewDir); } ccSpecularLight += ccLTCSpecFres * areaLightSpecularCC * falloffAttenuation * light.color * cookieAttenuation; #endif #endif } else #endif { { vec3 punctualDiffuse = falloffAttenuation * light.color * cookieAttenuation; #if defined(CLUSTER_AREALIGHTS) #if defined(LIT_SPECULAR) punctualDiffuse = mix(punctualDiffuse, vec3(0), specularity); #endif #endif dDiffuseLight += punctualDiffuse; } #ifdef LIT_SPECULAR vec3 halfDir = normalize(-lightDirNormW + viewDir); #ifdef LIT_SPECULAR_FRESNEL dSpecularLight += getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * getFresnel( dot(viewDir, halfDir), gloss, specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, iridescence_intensity #endif ); #else dSpecularLight += getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * specularity; #endif #ifdef LIT_CLEARCOAT #ifdef LIT_SPECULAR_FRESNEL ccSpecularLight += getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * getFresnelCC(dot(viewDir, halfDir)); #else ccSpecularLight += getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation; #endif #endif #ifdef LIT_SHEEN sSpecularLight += getLightSpecularSheen(halfDir, worldNormal, viewDir, lightDirNormW, sheen_gloss) * falloffAttenuation * light.color * cookieAttenuation; #endif #endif } } dAtten = falloffAttenuation; dLightDirNormW = lightDirNormW; } void evaluateClusterLight( int lightIndex, vec3 worldNormal, vec3 viewDir, vec3 reflectionDir, #if defined(LIT_CLEARCOAT) vec3 clearcoatReflectionDir, #endif float gloss, vec3 specularity, vec3 geometricNormal, mat3 tbn, #if defined(LIT_IRIDESCENCE) vec3 iridescenceFresnel, #endif vec3 clearcoat_worldNormal, float clearcoat_gloss, float sheen_gloss, float iridescence_intensity ) { ClusterLightData clusterLightData; decodeClusterLightCore(clusterLightData, lightIndex); #ifdef CLUSTER_MESH_DYNAMIC_LIGHTS bool acceptLightMask = clusterLightData.isDynamic; #else bool acceptLightMask = clusterLightData.isLightmapped; #endif if (acceptLightMask) evaluateLight( clusterLightData, worldNormal, viewDir, reflectionDir, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir, #endif gloss, specularity, geometricNormal, tbn, #if defined(LIT_IRIDESCENCE) iridescenceFresnel, #endif clearcoat_worldNormal, clearcoat_gloss, sheen_gloss, iridescence_intensity ); } void addClusteredLights( vec3 worldNormal, vec3 viewDir, vec3 reflectionDir, #if defined(LIT_CLEARCOAT) vec3 clearcoatReflectionDir, #endif float gloss, vec3 specularity, vec3 geometricNormal, mat3 tbn, #if defined(LIT_IRIDESCENCE) vec3 iridescenceFresnel, #endif vec3 clearcoat_worldNormal, float clearcoat_gloss, float sheen_gloss, float iridescence_intensity ) { if (numClusteredLights <= 1) return; ivec3 cellCoords = ivec3(floor((vPositionW - clusterBoundsMin) * clusterCellsCountByBoundsSize)); if (!(any(lessThan(cellCoords, ivec3(0))) || any(greaterThanEqual(cellCoords, clusterCellsMax)))) { int cellIndex = cellCoords.x * clusterCellsDot.x + cellCoords.y * clusterCellsDot.y + cellCoords.z * clusterCellsDot.z; int clusterV = cellIndex / clusterTextureWidth; int clusterU = cellIndex - clusterV * clusterTextureWidth; for (int lightCellIndex = 0; lightCellIndex < clusterMaxCells; lightCellIndex++) { uint lightIndex = texelFetch(clusterWorldTexture, ivec2(clusterU + lightCellIndex, clusterV), 0).x; if (lightIndex == 0u) break; evaluateClusterLight( int(lightIndex), worldNormal, viewDir, reflectionDir, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir, #endif gloss, specularity, geometricNormal, tbn, #if defined(LIT_IRIDESCENCE) iridescenceFresnel, #endif clearcoat_worldNormal, clearcoat_gloss, sheen_gloss, iridescence_intensity ); } } } `; var combinePS$1 = ` vec3 combineColor(vec3 albedo, vec3 sheenSpecularity, float clearcoatSpecularity) { vec3 ret = vec3(0); #ifdef LIT_OLD_AMBIENT ret += (dDiffuseLight - light_globalAmbient) * albedo + material_ambient * light_globalAmbient; #else ret += albedo * dDiffuseLight; #endif #ifdef LIT_SPECULAR ret += dSpecularLight; #endif #ifdef LIT_REFLECTIONS ret += dReflection.rgb * dReflection.a; #endif #ifdef LIT_SHEEN float sheenScaling = 1.0 - max(max(sheenSpecularity.r, sheenSpecularity.g), sheenSpecularity.b) * 0.157; ret = ret * sheenScaling + (sSpecularLight + sReflection.rgb) * sheenSpecularity; #endif #ifdef LIT_CLEARCOAT float clearCoatScaling = 1.0 - ccFresnel * clearcoatSpecularity; ret = ret * clearCoatScaling + (ccSpecularLight + ccReflection) * clearcoatSpecularity; #endif return ret; } `; var cookieBlit2DPS$1 = ` varying vec2 uv0; uniform sampler2D blitTexture; void main(void) { gl_FragColor = texture2D(blitTexture, uv0); } `; var cookieBlitCubePS$1 = ` varying vec2 uv0; uniform samplerCube blitTexture; uniform mat4 invViewProj; void main(void) { vec4 projPos = vec4(uv0 * 2.0 - 1.0, 0.5, 1.0); vec4 worldPos = invViewProj * projPos; gl_FragColor = textureCube(blitTexture, worldPos.xyz); } `; var cookieBlitVS$1 = ` attribute vec2 vertex_position; varying vec2 uv0; void main(void) { gl_Position = vec4(vertex_position, 0.5, 1.0); uv0 = vertex_position.xy * 0.5 + 0.5; #ifndef WEBGPU uv0.y = 1.0 - uv0.y; #endif } `; var cookiePS = ` vec4 getCookie2D(sampler2D tex, mat4 transform, float intensity) { vec4 projPos = transform * vec4(vPositionW, 1.0); projPos.xy /= projPos.w; return mix(vec4(1.0), texture2D(tex, projPos.xy), intensity); } vec4 getCookie2DClip(sampler2D tex, mat4 transform, float intensity) { vec4 projPos = transform * vec4(vPositionW, 1.0); projPos.xy /= projPos.w; if (projPos.x < 0.0 || projPos.x > 1.0 || projPos.y < 0.0 || projPos.y > 1.0 || projPos.z < 0.0) return vec4(0.0); return mix(vec4(1.0), texture2D(tex, projPos.xy), intensity); } vec4 getCookie2DXform(sampler2D tex, mat4 transform, float intensity, vec4 cookieMatrix, vec2 cookieOffset) { vec4 projPos = transform * vec4(vPositionW, 1.0); projPos.xy /= projPos.w; projPos.xy += cookieOffset; vec2 uv = mat2(cookieMatrix) * (projPos.xy-vec2(0.5)) + vec2(0.5); return mix(vec4(1.0), texture2D(tex, uv), intensity); } vec4 getCookie2DClipXform(sampler2D tex, mat4 transform, float intensity, vec4 cookieMatrix, vec2 cookieOffset) { vec4 projPos = transform * vec4(vPositionW, 1.0); projPos.xy /= projPos.w; projPos.xy += cookieOffset; if (projPos.x < 0.0 || projPos.x > 1.0 || projPos.y < 0.0 || projPos.y > 1.0 || projPos.z < 0.0) return vec4(0.0); vec2 uv = mat2(cookieMatrix) * (projPos.xy-vec2(0.5)) + vec2(0.5); return mix(vec4(1.0), texture2D(tex, uv), intensity); } vec4 getCookieCube(samplerCube tex, mat4 transform, float intensity) { return mix(vec4(1.0), textureCube(tex, dLightDirNormW * mat3(transform)), intensity); } `; var cubeMapProjectPS$1 = ` #if LIT_CUBEMAP_PROJECTION == BOX uniform vec3 envBoxMin; uniform vec3 envBoxMax; #endif vec3 cubeMapProject(vec3 nrdir) { #if LIT_CUBEMAP_PROJECTION == NONE return cubeMapRotate(nrdir); #endif #if LIT_CUBEMAP_PROJECTION == BOX nrdir = cubeMapRotate(nrdir); vec3 rbmax = (envBoxMax - vPositionW) / nrdir; vec3 rbmin = (envBoxMin - vPositionW) / nrdir; vec3 rbminmax = mix(rbmin, rbmax, vec3(greaterThan(nrdir, vec3(0.0)))); float fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z); vec3 posonbox = vPositionW + nrdir * fa; vec3 envBoxPos = (envBoxMin + envBoxMax) * 0.5; return normalize(posonbox - envBoxPos); #endif } `; var cubeMapRotatePS$1 = ` #ifdef CUBEMAP_ROTATION uniform mat3 cubeMapRotationMatrix; #endif vec3 cubeMapRotate(vec3 refDir) { #ifdef CUBEMAP_ROTATION return refDir * cubeMapRotationMatrix; #else return refDir; #endif } `; var debugOutputPS$1 = ` #ifdef DEBUG_ALBEDO_PASS gl_FragColor = vec4(gammaCorrectOutput(dAlbedo), 1.0); #endif #ifdef DEBUG_UV0_PASS gl_FragColor = vec4(litArgs_albedo , 1.0); #endif #ifdef DEBUG_WORLD_NORMAL_PASS gl_FragColor = vec4(litArgs_worldNormal * 0.5 + 0.5, 1.0); #endif #ifdef DEBUG_OPACITY_PASS gl_FragColor = vec4(vec3(litArgs_opacity) , 1.0); #endif #ifdef DEBUG_SPECULARITY_PASS gl_FragColor = vec4(litArgs_specularity, 1.0); #endif #ifdef DEBUG_GLOSS_PASS gl_FragColor = vec4(vec3(litArgs_gloss) , 1.0); #endif #ifdef DEBUG_METALNESS_PASS gl_FragColor = vec4(vec3(litArgs_metalness) , 1.0); #endif #ifdef DEBUG_AO_PASS gl_FragColor = vec4(vec3(litArgs_ao) , 1.0); #endif #ifdef DEBUG_EMISSION_PASS gl_FragColor = vec4(gammaCorrectOutput(litArgs_emission), 1.0); #endif `; var debugProcessFrontendPS$1 = ` #ifdef DEBUG_LIGHTING_PASS litArgs_albedo = vec3(0.5); #endif #ifdef DEBUG_UV0_PASS #ifdef VARYING_VUV0 litArgs_albedo = vec3(vUv0, 0); #else litArgs_albedo = vec3(0); #endif #endif `; var decodePS$1 = ` #ifndef _DECODE_INCLUDED_ #define _DECODE_INCLUDED_ vec3 decodeLinear(vec4 raw) { return raw.rgb; } float decodeGamma(float raw) { return pow(raw, 2.2); } vec3 decodeGamma(vec3 raw) { return pow(raw, vec3(2.2)); } vec3 decodeGamma(vec4 raw) { return pow(raw.xyz, vec3(2.2)); } vec3 decodeRGBM(vec4 raw) { vec3 color = (8.0 * raw.a) * raw.rgb; return color * color; } vec3 decodeRGBP(vec4 raw) { vec3 color = raw.rgb * (-raw.a * 7.0 + 8.0); return color * color; } vec3 decodeRGBE(vec4 raw) { if (raw.a == 0.0) { return vec3(0.0, 0.0, 0.0); } else { return raw.xyz * pow(2.0, raw.w * 255.0 - 128.0); } } vec4 passThrough(vec4 raw) { return raw; } vec3 unpackNormalXYZ(vec4 nmap) { return nmap.xyz * 2.0 - 1.0; } vec3 unpackNormalXY(vec4 nmap) { vec3 normal; normal.xy = nmap.wy * 2.0 - 1.0; normal.z = sqrt(1.0 - clamp(dot(normal.xy, normal.xy), 0.0, 1.0)); return normal; } #endif `; var detailModesPS$1 = ` #ifndef _DETAILMODES_INCLUDED_ #define _DETAILMODES_INCLUDED_ vec3 detailMode_mul(vec3 c1, vec3 c2) { return c1 * c2; } vec3 detailMode_add(vec3 c1, vec3 c2) { return c1 + c2; } vec3 detailMode_screen(vec3 c1, vec3 c2) { return 1.0 - (1.0 - c1)*(1.0 - c2); } vec3 detailMode_overlay(vec3 c1, vec3 c2) { return mix(1.0 - 2.0 * (1.0 - c1)*(1.0 - c2), 2.0 * c1 * c2, step(c1, vec3(0.5))); } vec3 detailMode_min(vec3 c1, vec3 c2) { return min(c1, c2); } vec3 detailMode_max(vec3 c1, vec3 c2) { return max(c1, c2); } #endif `; var diffusePS$1 = ` uniform vec3 material_diffuse; #ifdef STD_DIFFUSEDETAIL_TEXTURE #include "detailModesPS" #endif void getAlbedo() { dAlbedo = material_diffuse.rgb; #ifdef STD_DIFFUSE_TEXTURE vec3 albedoTexture = {STD_DIFFUSE_TEXTURE_DECODE}(texture2DBias({STD_DIFFUSE_TEXTURE_NAME}, {STD_DIFFUSE_TEXTURE_UV}, textureBias)).{STD_DIFFUSE_TEXTURE_CHANNEL}; #ifdef STD_DIFFUSEDETAIL_TEXTURE vec3 albedoDetail = {STD_DIFFUSEDETAIL_TEXTURE_DECODE}(texture2DBias({STD_DIFFUSEDETAIL_TEXTURE_NAME}, {STD_DIFFUSEDETAIL_TEXTURE_UV}, textureBias)).{STD_DIFFUSEDETAIL_TEXTURE_CHANNEL}; albedoTexture = detailMode_{STD_DIFFUSEDETAIL_DETAILMODE}(albedoTexture, albedoDetail); #endif dAlbedo *= albedoTexture; #endif #ifdef STD_DIFFUSE_VERTEX dAlbedo *= saturate(vVertexColor.{STD_DIFFUSE_VERTEX_CHANNEL}); #endif } `; var emissivePS$1 = ` uniform vec3 material_emissive; uniform float material_emissiveIntensity; void getEmission() { dEmission = material_emissive * material_emissiveIntensity; #ifdef STD_EMISSIVE_TEXTURE dEmission *= {STD_EMISSIVE_TEXTURE_DECODE}(texture2DBias({STD_EMISSIVE_TEXTURE_NAME}, {STD_EMISSIVE_TEXTURE_UV}, textureBias)).{STD_EMISSIVE_TEXTURE_CHANNEL}; #endif #ifdef STD_EMISSIVE_VERTEX dEmission *= saturate(vVertexColor.{STD_EMISSIVE_VERTEX_CHANNEL}); #endif } `; var encodePS$1 = ` vec4 encodeLinear(vec3 source) { return vec4(source, 1.0); } vec4 encodeGamma(vec3 source) { return vec4(pow(source + 0.0000001, vec3(1.0 / 2.2)), 1.0); } vec4 encodeRGBM(vec3 source) { vec4 result; result.rgb = pow(source.rgb, vec3(0.5)); result.rgb *= 1.0 / 8.0; result.a = saturate( max( max( result.r, result.g ), max( result.b, 1.0 / 255.0 ) ) ); result.a = ceil(result.a * 255.0) / 255.0; result.rgb /= result.a; return result; } vec4 encodeRGBP(vec3 source) { vec3 gamma = pow(source, vec3(0.5)); float maxVal = min(8.0, max(1.0, max(gamma.x, max(gamma.y, gamma.z)))); float v = 1.0 - ((maxVal - 1.0) / 7.0); v = ceil(v * 255.0) / 255.0; return vec4(gamma / (-v * 7.0 + 8.0), v); } vec4 encodeRGBE(vec3 source) { float maxVal = max(source.x, max(source.y, source.z)); if (maxVal < 1e-32) { return vec4(0, 0, 0, 0); } else { float e = ceil(log2(maxVal)); return vec4(source / pow(2.0, e), (e + 128.0) / 255.0); } } `; var endPS$1 = ` gl_FragColor.rgb = combineColor(litArgs_albedo, litArgs_sheen_specularity, litArgs_clearcoat_specularity); gl_FragColor.rgb += litArgs_emission; gl_FragColor.rgb = addFog(gl_FragColor.rgb); gl_FragColor.rgb = toneMap(gl_FragColor.rgb); gl_FragColor.rgb = gammaCorrectOutput(gl_FragColor.rgb); `; var envAtlasPS$1 = ` #ifndef _ENVATLAS_INCLUDED_ #define _ENVATLAS_INCLUDED_ const float atlasSize = 512.0; const float seamSize = 1.0 / atlasSize; vec2 mapUv(vec2 uv, vec4 rect) { return vec2(mix(rect.x + seamSize, rect.x + rect.z - seamSize, uv.x), mix(rect.y + seamSize, rect.y + rect.w - seamSize, uv.y)); } vec2 mapRoughnessUv(vec2 uv, float level) { float t = 1.0 / exp2(level); return mapUv(uv, vec4(0, 1.0 - t, t, t * 0.5)); } vec2 mapShinyUv(vec2 uv, float level) { float t = 1.0 / exp2(level); return mapUv(uv, vec4(1.0 - t, 1.0 - t, t, t * 0.5)); } #endif `; var envProcPS$1 = ` #ifdef LIT_SKYBOX_INTENSITY uniform float skyboxIntensity; #endif vec3 processEnvironment(vec3 color) { #ifdef LIT_SKYBOX_INTENSITY return color * skyboxIntensity; #else return color; #endif } `; var falloffInvSquaredPS$1 = ` float getFalloffWindow(float lightRadius, vec3 lightDir) { float sqrDist = dot(lightDir, lightDir); float invRadius = 1.0 / lightRadius; return square(saturate(1.0 - square(sqrDist * square(invRadius)))); } float getFalloffInvSquared(float lightRadius, vec3 lightDir) { float sqrDist = dot(lightDir, lightDir); float falloff = 1.0 / (sqrDist + 1.0); float invRadius = 1.0 / lightRadius; falloff *= 16.0; falloff *= square(saturate(1.0 - square(sqrDist * square(invRadius)))); return falloff; } `; var falloffLinearPS$1 = ` float getFalloffLinear(float lightRadius, vec3 lightDir) { float d = length(lightDir); return max(((lightRadius - d) / lightRadius), 0.0); } `; var floatAsUintPS$1 = ` #ifndef FLOAT_AS_UINT #define FLOAT_AS_UINT vec4 float2uint(float value) { uint intBits = floatBitsToUint(value); return vec4( float((intBits >> 24u) & 0xFFu) / 255.0, float((intBits >> 16u) & 0xFFu) / 255.0, float((intBits >> 8u) & 0xFFu) / 255.0, float(intBits & 0xFFu) / 255.0 ); } float uint2float(vec4 value) { uint intBits = (uint(value.r * 255.0) << 24u) | (uint(value.g * 255.0) << 16u) | (uint(value.b * 255.0) << 8u) | uint(value.a * 255.0); return uintBitsToFloat(intBits); } vec4 float2vec4(float value) { #if defined(CAPS_TEXTURE_FLOAT_RENDERABLE) return vec4(value, 1.0, 1.0, 1.0); #else return float2uint(value); #endif } #endif `; var fogPS$1 = ` float dBlendModeFogFactor = 1.0; #if (FOG != NONE) uniform vec3 fog_color; #if (FOG == LINEAR) uniform float fog_start; uniform float fog_end; #else uniform float fog_density; #endif #endif float getFogFactor() { float depth = gl_FragCoord.z / gl_FragCoord.w; float fogFactor = 0.0; #if (FOG == LINEAR) fogFactor = (fog_end - depth) / (fog_end - fog_start); #elif (FOG == EXP) fogFactor = exp(-depth * fog_density); #elif (FOG == EXP2) fogFactor = exp(-depth * depth * fog_density * fog_density); #endif return clamp(fogFactor, 0.0, 1.0); } vec3 addFog(vec3 color) { #if (FOG != NONE) return mix(fog_color * dBlendModeFogFactor, color, getFogFactor()); #endif return color; } `; var fresnelSchlickPS$1 = ` vec3 getFresnel( float cosTheta, float gloss, vec3 specularity #if defined(LIT_IRIDESCENCE) , vec3 iridescenceFresnel, float iridescenceIntensity #endif ) { float fresnel = pow(1.0 - saturate(cosTheta), 5.0); float glossSq = gloss * gloss; float specIntensity = max(specularity.r, max(specularity.g, specularity.b)); vec3 ret = specularity + (max(vec3(glossSq * specIntensity), specularity) - specularity) * fresnel; #if defined(LIT_IRIDESCENCE) return mix(ret, iridescenceFresnel, iridescenceIntensity); #else return ret; #endif } float getFresnelCC(float cosTheta) { float fresnel = pow(1.0 - saturate(cosTheta), 5.0); return 0.04 + (1.0 - 0.04) * fresnel; } `; var fullscreenQuadVS$1 = ` attribute vec2 vertex_position; varying vec2 vUv0; void main(void) { gl_Position = vec4(vertex_position, 0.5, 1.0); vUv0 = vertex_position.xy * 0.5 + 0.5; } `; var gammaPS$1 = ` #include "decodePS" #if (GAMMA == SRGB) float gammaCorrectInput(float color) { return decodeGamma(color); } vec3 gammaCorrectInput(vec3 color) { return decodeGamma(color); } vec4 gammaCorrectInput(vec4 color) { return vec4(decodeGamma(color.xyz), color.w); } vec3 gammaCorrectOutput(vec3 color) { return pow(color + 0.0000001, vec3(1.0 / 2.2)); } #else float gammaCorrectInput(float color) { return color; } vec3 gammaCorrectInput(vec3 color) { return color; } vec4 gammaCorrectInput(vec4 color) { return color; } vec3 gammaCorrectOutput(vec3 color) { return color; } #endif `; var glossPS$1 = ` #ifdef STD_GLOSS_CONSTANT uniform float material_gloss; #endif void getGlossiness() { dGlossiness = 1.0; #ifdef STD_GLOSS_CONSTANT dGlossiness *= material_gloss; #endif #ifdef STD_GLOSS_TEXTURE dGlossiness *= texture2DBias({STD_GLOSS_TEXTURE_NAME}, {STD_GLOSS_TEXTURE_UV}, textureBias).{STD_GLOSS_TEXTURE_CHANNEL}; #endif #ifdef STD_GLOSS_VERTEX dGlossiness *= saturate(vVertexColor.{STD_GLOSS_VERTEX_CHANNEL}); #endif #ifdef STD_GLOSS_INVERT dGlossiness = 1.0 - dGlossiness; #endif dGlossiness += 0.0000001; } `; var quadVS$1 = ` attribute vec2 aPosition; varying vec2 uv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); uv0 = getImageEffectUV((aPosition.xy + 1.0) * 0.5); } `; var immediateLinePS$1 = ` #include "gammaPS" varying vec4 color; void main(void) { gl_FragColor = vec4(gammaCorrectOutput(decodeGamma(color.rgb)), color.a); } `; var immediateLineVS$1 = ` attribute vec4 vertex_position; attribute vec4 vertex_color; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; varying vec4 color; void main(void) { color = vertex_color; gl_Position = matrix_viewProjection * matrix_model * vertex_position; } `; var iridescenceDiffractionPS$1 = ` uniform float material_iridescenceRefractionIndex; float iridescence_iorToFresnel(float transmittedIor, float incidentIor) { return pow((transmittedIor - incidentIor) / (transmittedIor + incidentIor), 2.0); } vec3 iridescence_iorToFresnel(vec3 transmittedIor, float incidentIor) { return pow((transmittedIor - vec3(incidentIor)) / (transmittedIor + vec3(incidentIor)), vec3(2.0)); } vec3 iridescence_fresnelToIor(vec3 f0) { vec3 sqrtF0 = sqrt(f0); return (vec3(1.0) + sqrtF0) / (vec3(1.0) - sqrtF0); } vec3 iridescence_sensitivity(float opd, vec3 shift) { float PI = 3.141592653589793; float phase = 2.0 * PI * opd * 1.0e-9; const vec3 val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); const vec3 pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); const vec3 var = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); vec3 xyz = val * sqrt(2.0 * PI * var) * cos(pos * phase + shift) * exp(-pow(phase, 2.0) * var); xyz.x += 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift[0]) * exp(-4.5282e+09 * pow(phase, 2.0)); xyz /= vec3(1.0685e-07); const mat3 XYZ_TO_REC709 = mat3( 3.2404542, -0.9692660, 0.0556434, -1.5371385, 1.8760108, -0.2040259, -0.4985314, 0.0415560, 1.0572252 ); return XYZ_TO_REC709 * xyz; } float iridescence_fresnel(float cosTheta, float f0) { float x = clamp(1.0 - cosTheta, 0.0, 1.0); float x2 = x * x; float x5 = x * x2 * x2; return f0 + (1.0 - f0) * x5; } vec3 iridescence_fresnel(float cosTheta, vec3 f0) { float x = clamp(1.0 - cosTheta, 0.0, 1.0); float x2 = x * x; float x5 = x * x2 * x2; return f0 + (vec3(1.0) - f0) * x5; } vec3 calcIridescence(float outsideIor, float cosTheta, vec3 base_f0, float iridescenceThickness) { float PI = 3.141592653589793; float iridescenceIor = mix(outsideIor, material_iridescenceRefractionIndex, smoothstep(0.0, 0.03, iridescenceThickness)); float sinTheta2Sq = pow(outsideIor / iridescenceIor, 2.0) * (1.0 - pow(cosTheta, 2.0)); float cosTheta2Sq = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3(1.0); } float cosTheta2 = sqrt(cosTheta2Sq); float r0 = iridescence_iorToFresnel(iridescenceIor, outsideIor); float r12 = iridescence_fresnel(cosTheta, r0); float r21 = r12; float t121 = 1.0 - r12; float phi12 = iridescenceIor < outsideIor ? PI : 0.0; float phi21 = PI - phi12; vec3 baseIor = iridescence_fresnelToIor(base_f0 + vec3(0.0001)); vec3 r1 = iridescence_iorToFresnel(baseIor, iridescenceIor); vec3 r23 = iridescence_fresnel(cosTheta2, r1); vec3 phi23 = vec3(0.0); if (baseIor[0] < iridescenceIor) phi23[0] = PI; if (baseIor[1] < iridescenceIor) phi23[1] = PI; if (baseIor[2] < iridescenceIor) phi23[2] = PI; float opd = 2.0 * iridescenceIor * iridescenceThickness * cosTheta2; vec3 phi = vec3(phi21) + phi23; vec3 r123Sq = clamp(r12 * r23, 1e-5, 0.9999); vec3 r123 = sqrt(r123Sq); vec3 rs = pow(t121, 2.0) * r23 / (1.0 - r123Sq); vec3 c0 = r12 + rs; vec3 i = c0; vec3 cm = rs - t121; for (int m = 1; m <= 2; m++) { cm *= r123; vec3 sm = 2.0 * iridescence_sensitivity(float(m) * opd, float(m) * phi); i += cm * sm; } return max(i, vec3(0.0)); } vec3 getIridescence(float cosTheta, vec3 specularity, float iridescenceThickness) { return calcIridescence(1.0, cosTheta, specularity, iridescenceThickness); } `; var iridescencePS$1 = ` #ifdef STD_IRIDESCENCE_CONSTANT uniform float material_iridescence; #endif void getIridescence() { float iridescence = 1.0; #ifdef STD_IRIDESCENCE_CONSTANT iridescence *= material_iridescence; #endif #ifdef STD_IRIDESCENCE_TEXTURE iridescence *= texture2DBias({STD_IRIDESCENCE_TEXTURE_NAME}, {STD_IRIDESCENCE_TEXTURE_UV}, textureBias).{STD_IRIDESCENCE_TEXTURE_CHANNEL}; #endif dIridescence = iridescence; } `; var iridescenceThicknessPS$1 = ` uniform float material_iridescenceThicknessMax; #ifdef STD_IRIDESCENCETHICKNESS_TEXTURE uniform float material_iridescenceThicknessMin; #endif void getIridescenceThickness() { #ifdef STD_IRIDESCENCETHICKNESS_TEXTURE float blend = texture2DBias({STD_IRIDESCENCETHICKNESS_TEXTURE_NAME}, {STD_IRIDESCENCETHICKNESS_TEXTURE_UV}, textureBias).{STD_IRIDESCENCETHICKNESS_TEXTURE_CHANNEL}; float iridescenceThickness = mix(material_iridescenceThicknessMin, material_iridescenceThicknessMax, blend); #else float iridescenceThickness = material_iridescenceThicknessMax; #endif dIridescenceThickness = iridescenceThickness; } `; var iorPS$1 = ` #ifdef STD_IOR_CONSTANT uniform float material_refractionIndex; #endif void getIor() { #ifdef STD_IOR_CONSTANT dIor = material_refractionIndex; #else dIor = 1.0 / 1.5; #endif } `; var lightDeclarationPS$1 = ` #if defined(LIGHT{i}) uniform vec3 light{i}_color; #if LIGHT{i}TYPE == DIRECTIONAL uniform vec3 light{i}_direction; #else #define LIT_CODE_LIGHTS_POINT uniform vec3 light{i}_position; uniform float light{i}_radius; #if LIGHT{i}TYPE == SPOT #define LIT_CODE_LIGHTS_SPOT uniform vec3 light{i}_direction; uniform float light{i}_innerConeAngle; uniform float light{i}_outerConeAngle; #endif #endif #if LIGHT{i}SHAPE != PUNCTUAL #define LIT_CODE_FALLOFF_SQUARED #if LIGHT{i}TYPE == DIRECTIONAL uniform vec3 light{i}_position; #endif uniform vec3 light{i}_halfWidth; uniform vec3 light{i}_halfHeight; #else #if LIGHT{i}FALLOFF == LINEAR #define LIT_CODE_FALLOFF_LINEAR #endif #if LIGHT{i}FALLOFF == INVERSESQUARED #define LIT_CODE_FALLOFF_SQUARED #endif #endif #if defined(LIGHT{i}CASTSHADOW) #if LIGHT{i}TYPE != OMNI uniform mat4 light{i}_shadowMatrix; #endif uniform float light{i}_shadowIntensity; uniform vec4 light{i}_shadowParams; #if LIGHT{i}SHADOWTYPE == PCSS_32F uniform float light{i}_shadowSearchArea; uniform vec4 light{i}_cameraParams; #if LIGHT{i}TYPE == DIRECTIONAL uniform vec4 light{i}_softShadowParams; #endif #endif #if LIGHT{i}TYPE == DIRECTIONAL uniform mat4 light{i}_shadowMatrixPalette[4]; uniform vec4 light{i}_shadowCascadeDistances; uniform int light{i}_shadowCascadeCount; uniform float light{i}_shadowCascadeBlend; #endif #if LIGHT{i}TYPE == OMNI #if defined(LIGHT{i}SHADOW_PCF) uniform samplerCubeShadow light{i}_shadowMap; #else uniform samplerCube light{i}_shadowMap; #endif #else #if defined(LIGHT{i}SHADOW_PCF) uniform sampler2DShadow light{i}_shadowMap; #else uniform sampler2D light{i}_shadowMap; #endif #endif #endif #if defined(LIGHT{i}COOKIE) #define LIT_CODE_COOKIE #if LIGHT{i}TYPE == OMNI uniform samplerCube light{i}_cookie; uniform float light{i}_cookieIntensity; uniform mat4 light{i}_shadowMatrix; #endif #if LIGHT{i}TYPE == SPOT uniform sampler2D light{i}_cookie; uniform float light{i}_cookieIntensity; #if !defined(LIGHT{i}CASTSHADOW) uniform mat4 light{i}_shadowMatrix; #endif #if defined(LIGHT{i}COOKIE_TRANSFORM) uniform vec4 light{i}_cookieMatrix; uniform vec2 light{i}_cookieOffset; #endif #endif #endif #endif `; var lightDiffuseLambertPS$1 = ` float getLightDiffuse(vec3 worldNormal, vec3 viewDir, vec3 lightDirNorm) { return max(dot(worldNormal, -lightDirNorm), 0.0); } `; var lightDirPointPS$1 = ` vec3 evalOmniLight(vec3 lightPosW) { return vPositionW - lightPosW; } `; var lightEvaluationPS$1 = ` #if defined(LIGHT{i}) evaluateLight{i}( #if defined(LIT_IRIDESCENCE) iridescenceFresnel #endif ); #endif `; var lightFunctionLightPS$1 = ` #if defined(LIGHT{i}) void evaluateLight{i}( #if defined(LIT_IRIDESCENCE) vec3 iridescenceFresnel #endif ) { vec3 lightColor = light{i}_color; #if LIGHT{i}TYPE == DIRECTIONAL && !defined(LIT_SHADOW_CATCHER) if (all(equal(lightColor, vec3(0.0)))) { return; } #endif #if LIGHT{i}TYPE == DIRECTIONAL dLightDirNormW = light{i}_direction; dAtten = 1.0; #else vec3 lightDirW = evalOmniLight(light{i}_position); dLightDirNormW = normalize(lightDirW); #if defined(LIGHT{i}COOKIE) #if LIGHT{i}TYPE == SPOT #ifdef LIGHT{i}COOKIE_FALLOFF #ifdef LIGHT{i}COOKIE_TRANSFORM vec3 cookieAttenuation = getCookie2DXform(light{i}_cookie, light{i}_shadowMatrix, light{i}_cookieIntensity, light{i}_cookieMatrix, light{i}_cookieOffset).{LIGHT{i}COOKIE_CHANNEL}; #else vec3 cookieAttenuation = getCookie2D(light{i}_cookie, light{i}_shadowMatrix, light{i}_cookieIntensity).{LIGHT{i}COOKIE_CHANNEL}; #endif #else #ifdef LIGHT{i}COOKIE_TRANSFORM vec3 cookieAttenuation = getCookie2DClipXform(light{i}_cookie, light{i}_shadowMatrix, light{i}_cookieIntensity, light{i}_cookieMatrix, light{i}_cookieOffset).{LIGHT{i}COOKIE_CHANNEL}; #else vec3 cookieAttenuation = getCookie2DClip(light{i}_cookie, light{i}_shadowMatrix, light{i}_cookieIntensity).{LIGHT{i}COOKIE_CHANNEL}; #endif #endif #endif #if LIGHT{i}TYPE == OMNI vec3 cookieAttenuation = getCookieCube(light{i}_cookie, light{i}_shadowMatrix, light{i}_cookieIntensity).{LIGHT{i}COOKIE_CHANNEL}; #endif lightColor *= cookieAttenuation; #endif #if LIGHT{i}SHAPE == PUNCTUAL #if LIGHT{i}FALLOFF == LINEAR dAtten = getFalloffLinear(light{i}_radius, lightDirW); #else dAtten = getFalloffInvSquared(light{i}_radius, lightDirW); #endif #else dAtten = getFalloffWindow(light{i}_radius, lightDirW); #endif #if LIGHT{i}TYPE == SPOT #if !defined(LIGHT{i}COOKIE) || defined(LIGHT{i}COOKIE_FALLOFF) dAtten *= getSpotEffect(light{i}_direction, light{i}_innerConeAngle, light{i}_outerConeAngle, dLightDirNormW); #endif #endif #endif if (dAtten < 0.00001) { return; } #if LIGHT{i}SHAPE != PUNCTUAL #if LIGHT{i}SHAPE == RECT calcRectLightValues(light{i}_position, light{i}_halfWidth, light{i}_halfHeight); #elif LIGHT{i}SHAPE == DISK calcDiskLightValues(light{i}_position, light{i}_halfWidth, light{i}_halfHeight); #elif LIGHT{i}SHAPE == SPHERE calcSphereLightValues(light{i}_position, light{i}_halfWidth, light{i}_halfHeight); #endif #endif #if LIGHT{i}SHAPE != PUNCTUAL #if LIGHT{i}TYPE == DIRECTIONAL float attenDiffuse = getLightDiffuse(litArgs_worldNormal, dViewDirW, dLightDirNormW); #else #if LIGHT{i}SHAPE == RECT float attenDiffuse = getRectLightDiffuse(litArgs_worldNormal, dViewDirW, lightDirW, dLightDirNormW) * 16.0; #elif LIGHT{i}SHAPE == DISK float attenDiffuse = getDiskLightDiffuse(litArgs_worldNormal, dViewDirW, lightDirW, dLightDirNormW) * 16.0; #elif LIGHT{i}SHAPE == SPHERE float attenDiffuse = getSphereLightDiffuse(litArgs_worldNormal, dViewDirW, lightDirW, dLightDirNormW) * 16.0; #endif #endif #else dAtten *= getLightDiffuse(litArgs_worldNormal, vec3(0.0), dLightDirNormW); #endif #ifdef LIGHT{i}CASTSHADOW #if LIGHT{i}TYPE == DIRECTIONAL float shadow = getShadow{i}(vec3(0.0)); #else float shadow = getShadow{i}(lightDirW); #endif shadow = mix(1.0, shadow, light{i}_shadowIntensity); dAtten *= shadow; #if defined(LIT_SHADOW_CATCHER) && LIGHT{i}TYPE == DIRECTIONAL dShadowCatcher *= shadow; #endif #endif #if LIGHT{i}SHAPE != PUNCTUAL #ifdef LIT_SPECULAR dDiffuseLight += ((attenDiffuse * dAtten) * lightColor) * (1.0 - dLTCSpecFres); #else dDiffuseLight += (attenDiffuse * dAtten) * lightColor; #endif #else #if defined(AREA_LIGHTS) && defined(LIT_SPECULAR) dDiffuseLight += (dAtten * lightColor) * (1.0 - litArgs_specularity); #else dDiffuseLight += dAtten * lightColor; #endif #endif #ifdef LIGHT{i}AFFECT_SPECULARITY #if LIGHT{i}SHAPE != PUNCTUAL #ifdef LIT_CLEARCOAT #if LIGHT{i}SHAPE == RECT ccSpecularLight += ccLTCSpecFres * getRectLightSpecular(litArgs_clearcoat_worldNormal, dViewDirW) * dAtten * lightColor; #elif LIGHT{i}SHAPE == DISK ccSpecularLight += ccLTCSpecFres * getDiskLightSpecular(litArgs_clearcoat_worldNormal, dViewDirW) * dAtten * lightColor; #elif LIGHT{i}SHAPE == SPHERE ccSpecularLight += ccLTCSpecFres * getSphereLightSpecular(litArgs_clearcoat_worldNormal, dViewDirW) * dAtten * lightColor; #endif #endif #ifdef LIT_SPECULAR #if LIGHT{i}SHAPE == RECT dSpecularLight += dLTCSpecFres * getRectLightSpecular(litArgs_worldNormal, dViewDirW) * dAtten * lightColor; #elif LIGHT{i}SHAPE == DISK dSpecularLight += dLTCSpecFres * getDiskLightSpecular(litArgs_worldNormal, dViewDirW) * dAtten * lightColor; #elif LIGHT{i}SHAPE == SPHERE dSpecularLight += dLTCSpecFres * getSphereLightSpecular(litArgs_worldNormal, dViewDirW) * dAtten * lightColor; #endif #endif #else #if LIGHT{i}TYPE == DIRECTIONAL && LIT_FRESNEL_MODEL != NONE #define LIGHT{i}FRESNEL #endif #ifdef LIT_SPECULAR vec3 halfDirW = normalize(-dLightDirNormW + dViewDirW); #endif #ifdef LIT_CLEARCOAT vec3 lightspecularCC = getLightSpecular(halfDirW, ccReflDirW, litArgs_clearcoat_worldNormal, dViewDirW, dLightDirNormW, litArgs_clearcoat_gloss, dTBN) * dAtten * lightColor; #ifdef LIGHT{i}FRESNEL lightspecularCC *= getFresnelCC(dot(dViewDirW, halfDirW)); #endif ccSpecularLight += lightspecularCC; #endif #ifdef LIT_SHEEN sSpecularLight += getLightSpecularSheen(halfDirW, litArgs_worldNormal, dViewDirW, dLightDirNormW, litArgs_sheen_gloss) * dAtten * lightColor; #endif #ifdef LIT_SPECULAR vec3 lightSpecular = getLightSpecular(halfDirW, dReflDirW, litArgs_worldNormal, dViewDirW, dLightDirNormW, litArgs_gloss, dTBN) * dAtten * lightColor; #ifdef LIGHT{i}FRESNEL #if defined(LIT_IRIDESCENCE) lightSpecular *= getFresnel(dot(dViewDirW, halfDirW), litArgs_gloss, litArgs_specularity, iridescenceFresnel, litArgs_iridescence_intensity); #else lightSpecular *= getFresnel(dot(dViewDirW, halfDirW), litArgs_gloss, litArgs_specularity); #endif #else lightSpecular *= litArgs_specularity; #endif dSpecularLight += lightSpecular; #endif #endif #endif } #endif `; var lightFunctionShadowPS$1 = ` #ifdef LIGHT{i}CASTSHADOW #ifdef LIGHT{i}_SHADOW_SAMPLE_POINT vec3 getShadowSampleCoordOmni{i}(vec4 shadowParams, vec3 worldPosition, vec3 lightPos, inout vec3 lightDir, vec3 lightDirNorm, vec3 normal) { #ifdef LIGHT{i}_SHADOW_SAMPLE_NORMAL_OFFSET float distScale = length(lightDir); vec3 surfacePosition = worldPosition + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; lightDir = surfacePosition - lightPos; #endif return lightDir; } #endif #ifndef LIGHT{i}_SHADOW_SAMPLE_POINT vec3 getShadowSampleCoord{i}(mat4 shadowTransform, vec4 shadowParams, vec3 worldPosition, vec3 lightPos, inout vec3 lightDir, vec3 lightDirNorm, vec3 normal) { vec3 surfacePosition = worldPosition; #ifdef LIGHT{i}_SHADOW_SAMPLE_SOURCE_ZBUFFER #ifdef LIGHT{i}_SHADOW_SAMPLE_NORMAL_OFFSET surfacePosition = surfacePosition + normal * shadowParams.y; #endif #else #ifdef LIGHT{i}_SHADOW_SAMPLE_NORMAL_OFFSET #ifdef LIGHT{i}_SHADOW_SAMPLE_ORTHO float distScale = 1.0; #else float distScale = abs(dot(vPositionW - lightPos, lightDirNorm)); #endif surfacePosition = surfacePosition + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; #endif #endif vec4 positionInShadowSpace = shadowTransform * vec4(surfacePosition, 1.0); #ifdef LIGHT{i}_SHADOW_SAMPLE_ORTHO positionInShadowSpace.z = saturate(positionInShadowSpace.z) - 0.0001; #else #ifdef LIGHT{i}_SHADOW_SAMPLE_SOURCE_ZBUFFER positionInShadowSpace.xyz /= positionInShadowSpace.w; #else positionInShadowSpace.xy /= positionInShadowSpace.w; positionInShadowSpace.z = length(lightDir) * shadowParams.w; #endif #endif return positionInShadowSpace.xyz; } #endif float getShadow{i}(vec3 lightDirW) { #if LIGHT{i}TYPE == OMNI vec3 shadowCoord = getShadowSampleCoordOmni{i}(light{i}_shadowParams, vPositionW, light{i}_position, lightDirW, dLightDirNormW, dVertexNormalW); #else #ifdef LIGHT{i}_SHADOW_CASCADES int cascadeIndex = getShadowCascadeIndex(light{i}_shadowCascadeDistances, light{i}_shadowCascadeCount); #ifdef LIGHT{i}_SHADOW_CASCADE_BLEND cascadeIndex = ditherShadowCascadeIndex(cascadeIndex, light{i}_shadowCascadeDistances, light{i}_shadowCascadeCount, light{i}_shadowCascadeBlend); #endif mat4 shadowMatrix = light{i}_shadowMatrixPalette[cascadeIndex]; #else mat4 shadowMatrix = light{i}_shadowMatrix; #endif #if LIGHT{i}TYPE == DIRECTIONAL vec3 shadowCoord = getShadowSampleCoord{i}(shadowMatrix, light{i}_shadowParams, vPositionW, vec3(0.0), lightDirW, dLightDirNormW, dVertexNormalW); #else vec3 shadowCoord = getShadowSampleCoord{i}(shadowMatrix, light{i}_shadowParams, vPositionW, light{i}_position, lightDirW, dLightDirNormW, dVertexNormalW); #endif #endif #if LIGHT{i}TYPE == DIRECTIONAL shadowCoord = fadeShadow(shadowCoord, light{i}_shadowCascadeDistances); #endif #if LIGHT{i}TYPE == DIRECTIONAL #if LIGHT{i}SHADOWTYPE == VSM_16F return getShadowVSM16(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, 5.54); #endif #if LIGHT{i}SHADOWTYPE == VSM_32F return getShadowVSM32(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, 15.0); #endif #if LIGHT{i}SHADOWTYPE == PCSS_32F #if LIGHT{i}SHAPE != PUNCTUAL vec2 shadowSearchArea = vec2(length(light{i}_halfWidth), length(light{i}_halfHeight)) * light{i}_shadowSearchArea; return getShadowPCSS(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, light{i}_cameraParams, shadowSearchArea, lightDirW); #else return getShadowPCSS(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, light{i}_cameraParams, light{i}_softShadowParams, lightDirW); #endif #endif #if LIGHT{i}SHADOWTYPE == PCF1_16F || LIGHT{i}SHADOWTYPE == PCF1_32F return getShadowPCF1x1(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF3_16F || LIGHT{i}SHADOWTYPE == PCF3_32F return getShadowPCF3x3(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF5_16F || LIGHT{i}SHADOWTYPE == PCF5_32F return getShadowPCF5x5(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams); #endif #endif #if LIGHT{i}TYPE == SPOT #if LIGHT{i}SHADOWTYPE == VSM_16F return getShadowSpotVSM16(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, 5.54, lightDirW); #endif #if LIGHT{i}SHADOWTYPE == VSM_32F return getShadowSpotVSM32(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, 15.0, lightDirW); #endif #if LIGHT{i}SHADOWTYPE == PCSS_32F #if LIGHT{i}SHAPE != PUNCTUAL vec2 shadowSearchArea = vec2(length(light{i}_halfWidth), length(light{i}_halfHeight)) * light{i}_shadowSearchArea; #else vec2 shadowSearchArea = vec2(light{i}_shadowSearchArea); #endif return getShadowSpotPCSS(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, light{i}_cameraParams, shadowSearchArea, lightDirW); #endif #if LIGHT{i}SHADOWTYPE == PCF1_16F || LIGHT{i}SHADOWTYPE == PCF1_32F return getShadowSpotPCF1x1(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF3_16F || LIGHT{i}SHADOWTYPE == PCF3_32F return getShadowSpotPCF3x3(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF5_16F || LIGHT{i}SHADOWTYPE == PCF5_32F return getShadowSpotPCF5x5(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams); #endif #endif #if LIGHT{i}TYPE == OMNI #if LIGHT{i}SHADOWTYPE == PCSS_32F #if LIGHT{i}SHAPE != PUNCTUAL vec2 shadowSearchArea = vec2(length(light{i}_halfWidth), length(light{i}_halfHeight)) * light{i}_shadowSearchArea; #else vec2 shadowSearchArea = vec2(light{i}_shadowSearchArea); #endif return getShadowOmniPCSS(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, light{i}_cameraParams, shadowSearchArea, lightDirW); #endif #if LIGHT{i}SHADOWTYPE == PCF1_16F || LIGHT{i}SHADOWTYPE == PCF1_32F return getShadowOmniPCF1x1(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, lightDirW); #endif #if LIGHT{i}SHADOWTYPE == PCF3_16F || LIGHT{i}SHADOWTYPE == PCF3_32F return getShadowOmniPCF3x3(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, lightDirW); #endif #endif } #endif `; var lightingPS$1 = ` #ifdef LIT_CLUSTERED_LIGHTS #define LIT_CODE_FALLOFF_LINEAR #define LIT_CODE_FALLOFF_SQUARED #define LIT_CODE_LIGHTS_POINT #define LIT_CODE_LIGHTS_SPOT #endif #ifdef AREA_LIGHTS uniform highp sampler2D areaLightsLutTex1; uniform highp sampler2D areaLightsLutTex2; #endif #ifdef LIT_LIGHTING #include "lightDiffuseLambertPS" #if defined(AREA_LIGHTS) || defined(LIT_CLUSTERED_AREA_LIGHTS) #include "ltcPS" #endif #endif #ifdef SHADOW_DIRECTIONAL #include "shadowCascadesPS" #endif #if defined(SHADOW_KIND_PCF1) #include "shadowPCF1PS" #endif #if defined(SHADOW_KIND_PCF3) #include "shadowPCF3PS" #endif #if defined(SHADOW_KIND_PCF5) #include "shadowPCF5PS" #endif #if defined(SHADOW_KIND_PCSS) #include "linearizeDepthPS" #include "shadowPCSSPS" #include "shadowSoftPS" #endif #if defined(SHADOW_KIND_VSM) #include "shadowEVSMPS" #endif #ifdef LIT_CODE_FALLOFF_LINEAR #include "falloffLinearPS" #endif #ifdef LIT_CODE_FALLOFF_SQUARED #include "falloffInvSquaredPS" #endif #ifdef LIT_CODE_LIGHTS_POINT #include "lightDirPointPS" #endif #ifdef LIT_CODE_LIGHTS_SPOT #include "spotPS" #endif #ifdef LIT_CODE_COOKIE #include "cookiePS" #endif #ifdef LIT_CLUSTERED_LIGHTS #include "clusteredLightPS" #endif #ifdef LIGHT_COUNT > 0 #include "lightFunctionShadowPS, LIGHT_COUNT" #include "lightFunctionLightPS, LIGHT_COUNT" #endif `; var lightmapAddPS$1 = ` void addLightMap( vec3 lightmap, vec3 dir, vec3 worldNormal, vec3 viewDir, vec3 reflectionDir, float gloss, vec3 specularity, vec3 vertexNormal, mat3 tbn #if defined(LIT_IRIDESCENCE) vec3 iridescenceFresnel, float iridescenceIntensity #endif ) { #if defined(LIT_SPECULAR) && defined(LIT_DIR_LIGHTMAP) if (dot(dir, dir) < 0.0001) { dDiffuseLight += lightmap; } else { float vlight = saturate(dot(dir, -vertexNormal)); float flight = saturate(dot(dir, -worldNormal)); float nlight = (flight / max(vlight, 0.01)) * 0.5; dDiffuseLight += lightmap * nlight * 2.0; vec3 halfDir = normalize(-dir + viewDir); vec3 specularLight = lightmap * getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, dir, gloss, tbn); #ifdef LIT_SPECULAR_FRESNEL specularLight *= getFresnel(dot(viewDir, halfDir), gloss, specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, iridescenceIntensity #endif ); #endif dSpecularLight += specularLight; } #else dDiffuseLight += lightmap; #endif } `; var lightmapPS$1 = ` #ifdef STD_LIGHTMAP_DIR vec3 dLightmapDir; uniform sampler2D texture_dirLightMap; #endif void getLightMap() { dLightmap = vec3(1.0); #ifdef STD_LIGHT_TEXTURE dLightmap *= {STD_LIGHT_TEXTURE_DECODE}(texture2DBias({STD_LIGHT_TEXTURE_NAME}, {STD_LIGHT_TEXTURE_UV}, textureBias)).{STD_LIGHT_TEXTURE_CHANNEL}; #ifdef STD_LIGHTMAP_DIR vec3 dir = texture2DBias(texture_dirLightMap, {STD_LIGHT_TEXTURE_UV}, textureBias).xyz * 2.0 - 1.0; float dirDot = dot(dir, dir); dLightmapDir = (dirDot > 0.001) ? dir / sqrt(dirDot) : vec3(0.0); #endif #endif #ifdef STD_LIGHT_VERTEX dLightmap *= saturate(vVertexColor.{STD_LIGHT_VERTEX_CHANNEL}); #endif } `; var lightSpecularAnisoGGXPS$1 = ` float calcLightSpecular(float gloss, vec3 worldNormal, vec3 viewDir, vec3 h, vec3 lightDirNorm, mat3 tbn) { float PI = 3.141592653589793; float roughness = max((1.0 - gloss) * (1.0 - gloss), 0.001); float alphaRoughness = roughness * roughness; float anisotropy = dAnisotropy; vec2 direction = dAnisotropyRotation; float at = mix(alphaRoughness, 1.0, anisotropy * anisotropy); float ab = clamp(alphaRoughness, 0.001, 1.0); vec3 anisotropicT = normalize(tbn * vec3(direction, 0.0)); vec3 anisotropicB = normalize(cross(tbn[2], anisotropicT)); float NoH = dot(worldNormal, h); float ToH = dot(anisotropicT, h); float BoH = dot(anisotropicB, h); float a2 = at * ab; vec3 v = vec3(ab * ToH, at * BoH, a2 * NoH); float v2 = dot(v, v); float w2 = a2 / v2; float D = a2 * w2 * w2 * (1.0 / PI); float ToV = dot(anisotropicT, viewDir); float BoV = dot(anisotropicB, viewDir); float ToL = dot(anisotropicT, -lightDirNorm); float BoL = dot(anisotropicB, -lightDirNorm); float NoV = dot(worldNormal, viewDir); float NoL = dot(worldNormal, -lightDirNorm); float lambdaV = NoL * length(vec3(at * ToV, ab * BoV, NoV)); float lambdaL = NoV * length(vec3(at * ToL, ab * BoL, NoL)); float G = 0.5 / (lambdaV + lambdaL); return D * G; } float getLightSpecular(vec3 h, vec3 reflDir, vec3 worldNormal, vec3 viewDir, vec3 lightDirNorm, float gloss, mat3 tbn) { return calcLightSpecular(gloss, worldNormal, viewDir, h, lightDirNorm, tbn); } `; var lightSpecularGGXPS$1 = ` float calcLightSpecular(float gloss, vec3 worldNormal, vec3 viewDir, vec3 h, vec3 lightDirNorm) { const float PI = 3.141592653589793; float roughness = max((1.0 - gloss) * (1.0 - gloss), 0.001); float alpha = roughness * roughness; float NoH = max(dot(worldNormal, h), 0.0); float NoV = max(dot(worldNormal, viewDir), 0.0); float NoL = max(dot(worldNormal, -lightDirNorm), 0.0); float NoH2 = NoH * NoH; float denom = NoH2 * (alpha - 1.0) + 1.0; float D = alpha / (PI * denom * denom); float alpha2 = alpha * alpha; float lambdaV = NoL * sqrt(NoV * NoV * (1.0 - alpha2) + alpha2); float lambdaL = NoV * sqrt(NoL * NoL * (1.0 - alpha2) + alpha2); float G = 0.5 / max(lambdaV + lambdaL, 0.00001); return D * G; } float getLightSpecular(vec3 h, vec3 reflDir, vec3 worldNormal, vec3 viewDir, vec3 lightDirNorm, float gloss, mat3 tbn) { return calcLightSpecular(gloss, worldNormal, viewDir, h, lightDirNorm); } `; var lightSpecularBlinnPS$1 = ` float calcLightSpecular(float gloss, vec3 worldNormal, vec3 h) { float nh = max( dot( h, worldNormal ), 0.0 ); float specPow = exp2(gloss * 11.0); specPow = max(specPow, 0.0001); return pow(nh, specPow) * (specPow + 2.0) / 8.0; } float getLightSpecular(vec3 h, vec3 reflDir, vec3 worldNormal, vec3 viewDir, vec3 lightDirNorm, float gloss, mat3 tbn) { return calcLightSpecular(gloss, worldNormal, h); } `; var lightSheenPS$1 = ` float sheenD(vec3 normal, vec3 h, float roughness) { const float PI = 3.141592653589793; float invR = 1.0 / (roughness * roughness); float cos2h = max(dot(normal, h), 0.0); cos2h *= cos2h; float sin2h = max(1.0 - cos2h, 0.0078125); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } float sheenV(vec3 normal, vec3 viewDir, vec3 light) { float NoV = max(dot(normal, viewDir), 0.000001); float NoL = max(dot(normal, light), 0.000001); return 1.0 / (4.0 * (NoL + NoV - NoL * NoV)); } float getLightSpecularSheen(vec3 h, vec3 worldNormal, vec3 viewDir, vec3 lightDirNorm, float sheenGloss) { float D = sheenD(worldNormal, h, sheenGloss); float V = sheenV(worldNormal, viewDir, -lightDirNorm); return D * V; } `; var linearizeDepthPS$1 = ` #ifndef LINEARIZE_DEPTH #define LINEARIZE_DEPTH float linearizeDepthWithParams(float z, vec4 cameraParams) { if (cameraParams.w == 0.0) return (cameraParams.z * cameraParams.y) / (cameraParams.y + z * (cameraParams.z - cameraParams.y)); else return cameraParams.z + z * (cameraParams.y - cameraParams.z); } #ifndef CAMERAPLANES #define CAMERAPLANES uniform vec4 camera_params; #endif float linearizeDepth(float z) { return linearizeDepthWithParams(z, camera_params); } #endif `; var litForwardBackendPS$1 = ` void evaluateBackend() { #ifdef LIT_SSAO litArgs_ao *= texture2DLod(ssaoTexture, gl_FragCoord.xy * ssaoTextureSizeInv, 0.0).r; #endif #ifdef LIT_NEEDS_NORMAL #ifdef LIT_SPECULAR getReflDir(litArgs_worldNormal, dViewDirW, litArgs_gloss, dTBN); #endif #ifdef LIT_CLEARCOAT ccReflDirW = normalize(-reflect(dViewDirW, litArgs_clearcoat_worldNormal)); #endif #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_METALNESS float f0 = 1.0 / litArgs_ior; f0 = (f0 - 1.0) / (f0 + 1.0); f0 *= f0; #ifdef LIT_SPECULARITY_FACTOR litArgs_specularity = getSpecularModulate(litArgs_specularity, litArgs_albedo, litArgs_metalness, f0, litArgs_specularityFactor); #else litArgs_specularity = getSpecularModulate(litArgs_specularity, litArgs_albedo, litArgs_metalness, f0, 1.0); #endif litArgs_albedo = getAlbedoModulate(litArgs_albedo, litArgs_metalness); #endif #ifdef LIT_IRIDESCENCE vec3 iridescenceFresnel = getIridescence(saturate(dot(dViewDirW, litArgs_worldNormal)), litArgs_specularity, litArgs_iridescence_thickness); #endif #endif #ifdef LIT_ADD_AMBIENT addAmbient(litArgs_worldNormal); #ifdef LIT_SPECULAR dDiffuseLight = dDiffuseLight * (1.0 - litArgs_specularity); #endif #ifdef LIT_SEPARATE_AMBIENT vec3 dAmbientLight = dDiffuseLight; dDiffuseLight = vec3(0); #endif #endif #ifndef LIT_OLD_AMBIENT dDiffuseLight *= material_ambient; #endif #ifdef LIT_AO #ifndef LIT_OCCLUDE_DIRECT occludeDiffuse(litArgs_ao); #endif #endif #ifdef LIT_LIGHTMAP addLightMap( litArgs_lightmap, litArgs_lightmapDir, litArgs_worldNormal, dViewDirW, dReflDirW, litArgs_gloss, litArgs_specularity, dVertexNormalW, dTBN #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, litArgs_iridescence_intensity #endif ); #endif #ifdef LIT_LIGHTING || LIT_REFLECTIONS #ifdef LIT_REFLECTIONS #ifdef LIT_CLEARCOAT addReflectionCC(ccReflDirW, litArgs_clearcoat_gloss); #ifdef LIT_SPECULAR_FRESNEL ccFresnel = getFresnelCC(dot(dViewDirW, litArgs_clearcoat_worldNormal)); ccReflection *= ccFresnel; #else ccFresnel = 0.0; #endif #endif #ifdef LIT_SPECULARITY_FACTOR ccReflection *= litArgs_specularityFactor; #endif #ifdef LIT_SHEEN addReflectionSheen(litArgs_worldNormal, dViewDirW, litArgs_sheen_gloss); #endif addReflection(dReflDirW, litArgs_gloss); #ifdef LIT_FRESNEL_MODEL dReflection.rgb *= getFresnel( dot(dViewDirW, litArgs_worldNormal), litArgs_gloss, litArgs_specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, litArgs_iridescence_intensity #endif ); #else dReflection.rgb *= litArgs_specularity; #endif #endif #ifdef AREA_LIGHTS dSpecularLight *= litArgs_specularity; #ifdef LIT_SPECULAR calcLTCLightValues(litArgs_gloss, litArgs_worldNormal, dViewDirW, litArgs_specularity, litArgs_clearcoat_gloss, litArgs_clearcoat_worldNormal, litArgs_clearcoat_specularity); #endif #endif #ifdef LIGHT_COUNT > 0 #include "lightEvaluationPS, LIGHT_COUNT" #endif #ifdef LIT_CLUSTERED_LIGHTS addClusteredLights(litArgs_worldNormal, dViewDirW, dReflDirW, #if defined(LIT_CLEARCOAT) ccReflDirW, #endif litArgs_gloss, litArgs_specularity, dVertexNormalW, dTBN, #if defined(LIT_IRIDESCENCE) iridescenceFresnel, #endif litArgs_clearcoat_worldNormal, litArgs_clearcoat_gloss, litArgs_sheen_gloss, litArgs_iridescence_intensity ); #endif #ifdef AREA_LIGHTS #ifdef LIT_CLEARCOAT litArgs_clearcoat_specularity = 1.0; #endif #ifdef LIT_SPECULAR litArgs_specularity = vec3(1); #endif #endif #ifdef LIT_REFRACTION addRefraction( litArgs_worldNormal, dViewDirW, litArgs_thickness, litArgs_gloss, litArgs_specularity, litArgs_albedo, litArgs_transmission, litArgs_ior, litArgs_dispersion #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, litArgs_iridescence_intensity #endif ); #endif #endif #ifdef LIT_AO #ifdef LIT_OCCLUDE_DIRECT occludeDiffuse(litArgs_ao); #endif #if LIT_OCCLUDE_SPECULAR != NONE occludeSpecular(litArgs_gloss, litArgs_ao, litArgs_worldNormal, dViewDirW); #endif #endif #if !defined(LIT_OPACITY_FADES_SPECULAR) #if LIT_BLEND_TYPE == NORMAL || LIT_BLEND_TYPE == PREMULTIPLIED float specLum = dot((dSpecularLight + dReflection.rgb * dReflection.a), vec3( 0.2126, 0.7152, 0.0722 )); #ifdef LIT_CLEARCOAT specLum += dot(ccSpecularLight * litArgs_clearcoat_specularity + ccReflection * litArgs_clearcoat_specularity, vec3( 0.2126, 0.7152, 0.0722 )); #endif litArgs_opacity = clamp(litArgs_opacity + gammaCorrectInput(specLum), 0.0, 1.0); #endif litArgs_opacity *= material_alphaFade; #endif #ifdef LIT_LIGHTMAP_BAKING #ifdef LIT_LIGHTMAP_BAKING_COLOR #include "bakeLmEndPS" #endif #ifdef LIT_LIGHTMAP_BAKING_DIR #include "bakeDirLmEndPS" #endif #else #include "endPS" #include "outputAlphaPS" #endif #ifdef LIT_MSDF gl_FragColor = applyMsdf(gl_FragColor); #endif #include "outputPS" #include "debugOutputPS" #ifdef LIT_SHADOW_CATCHER gl_FragColor.rgb = vec3(dShadowCatcher); #endif } `; var litForwardDeclarationPS$1 = ` vec3 sReflection; vec3 dVertexNormalW; vec3 dTangentW; vec3 dBinormalW; vec3 dViewDirW; vec3 dReflDirW; vec3 ccReflDirW; vec3 dLightDirNormW; float dAtten; mat3 dTBN; vec4 dReflection; vec3 dDiffuseLight; vec3 dSpecularLight; float ccFresnel; vec3 ccReflection; vec3 ccSpecularLight; float ccSpecularityNoFres; vec3 sSpecularLight; #ifdef LIT_DISPERSION uniform float material_dispersion; #endif #ifndef LIT_OPACITY_FADES_SPECULAR uniform float material_alphaFade; #endif #ifdef LIT_SSAO uniform sampler2D ssaoTexture; uniform vec2 ssaoTextureSizeInv; #endif #ifdef LIT_SHADOW_CATCHER float dShadowCatcher = 1.0; #endif #if LIGHT_COUNT > 0 #include "lightDeclarationPS, LIGHT_COUNT" #endif #ifdef LIT_SPECULAR #if LIT_FRESNEL_MODEL == NONE && !defined(LIT_REFLECTIONS) && !defined(LIT_DIFFUSE_MAP) #define LIT_OLD_AMBIENT #endif #endif #ifdef STD_LIGHTMAP_DIR uniform float bakeDir; #endif #ifdef LIT_LIGHTMAP_BAKING_ADD_AMBIENT uniform float ambientBakeOcclusionContrast; uniform float ambientBakeOcclusionBrightness; #endif `; var litForwardMainPS$1 = ` void main(void) { #include "litUserMainStartPS" dReflection = vec4(0); #ifdef LIT_CLEARCOAT ccSpecularLight = vec3(0); ccReflection = vec3(0); #endif #if LIT_NONE_SLICE_MODE == SLICED #include "startNineSlicedPS" #elif LIT_NONE_SLICE_MODE == TILED #include "startNineSlicedTiledPS" #endif #ifdef LIT_NEEDS_NORMAL dVertexNormalW = normalize(vNormalW); #ifdef LIT_TANGENTS #if defined(LIT_HEIGHTS) || defined(LIT_USE_NORMALS) || defined(LIT_USE_CLEARCOAT_NORMALS) || defined(LIT_GGX_SPECULAR) dTangentW = vTangentW; dBinormalW = vBinormalW; #endif #endif getViewDir(); #ifdef LIT_TBN getTBN(dTangentW, dBinormalW, dVertexNormalW); #ifdef LIT_TWO_SIDED_LIGHTING handleTwoSidedLighting(); #endif #endif #endif evaluateFrontend(); #include "debugProcessFrontendPS" evaluateBackend(); #include "litUserMainEndPS" } `; var litForwardPostCodePS$1 = ` #ifdef LIT_NEEDS_NORMAL #include "cubeMapRotatePS" #include "cubeMapProjectPS" #include "envProcPS" #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_METALNESS #include "metalnessModulatePS" #endif #if LIT_FRESNEL_MODEL == SCHLICK #include "fresnelSchlickPS" #endif #ifdef LIT_IRIDESCENCE #include "iridescenceDiffractionPS" #endif #endif #ifdef LIT_AO #include "aoDiffuseOccPS" #include "aoSpecOccPS" #endif #if LIT_REFLECTION_SOURCE == ENVATLASHQ #include "envAtlasPS" #include "reflectionEnvHQPS" #elif LIT_REFLECTION_SOURCE == ENVATLAS #include "envAtlasPS" #include "reflectionEnvPS" #elif LIT_REFLECTION_SOURCE == CUBEMAP #include "reflectionCubePS" #elif LIT_REFLECTION_SOURCE == SPHEREMAP #include "reflectionSpherePS" #endif #ifdef LIT_REFLECTIONS #ifdef LIT_CLEARCOAT #include "reflectionCCPS" #endif #ifdef LIT_SHEEN #include "reflectionSheenPS" #endif #endif #ifdef LIT_REFRACTION #if defined(LIT_DYNAMIC_REFRACTION) #include "refractionDynamicPS" #elif defined(LIT_REFLECTIONS) #include "refractionCubePS" #endif #endif #ifdef LIT_SHEEN #include "lightSheenPS" #endif uniform vec3 material_ambient; #ifdef LIT_SPECULAR #ifdef LIT_LIGHTING #ifdef LIT_GGX_SPECULAR #ifdef LIT_ANISOTROPY #include "lightSpecularAnisoGGXPS" #else #include "lightSpecularGGXPS" #endif #else #include "lightSpecularBlinnPS" #endif #endif #endif #include "combinePS" #ifdef LIT_LIGHTMAP #include "lightmapAddPS" #endif #ifdef LIT_ADD_AMBIENT #include "ambientPS" #endif #ifdef LIT_MSDF #include "msdfPS" #endif #ifdef LIT_NEEDS_NORMAL #include "viewDirPS" #ifdef LIT_SPECULAR #ifdef LIT_ANISOTROPY #include "reflDirAnisoPS" #else #include "reflDirPS" #endif #endif #endif #include "lightingPS" `; var litForwardPreCodePS$1 = ` #include "basePS" #include "sphericalPS" #include "decodePS" #include "gammaPS" #include "tonemappingPS" #include "fogPS" #if LIT_NONE_SLICE_MODE == SLICED #include "baseNineSlicedPS" #elif LIT_NONE_SLICE_MODE == TILED #include "baseNineSlicedTiledPS" #endif #ifdef LIT_TBN #include "TBNPS" #ifdef LIT_TWO_SIDED_LIGHTING #include "twoSidedLightingPS" #endif #endif `; var litMainPS$1 = ` #include "varyingsPS" #include "litUserDeclarationPS" #include "frontendDeclPS" #if defined(PICK_PASS) || defined(PREPASS_PASS) #include "frontendCodePS" #include "litUserCodePS" #include "litOtherMainPS" #elif defined(SHADOW_PASS) #include "frontendCodePS" #include "litUserCodePS" #include "litShadowMainPS" #else #include "litForwardDeclarationPS" #include "litForwardPreCodePS" #include "frontendCodePS" #include "litForwardPostCodePS" #include "litForwardBackendPS" #include "litUserCodePS" #include "litForwardMainPS" #endif `; var litMainVS$1 = ` #include "varyingsVS" #include "litUserDeclarationVS" #ifdef VERTEX_COLOR attribute vec4 vertex_color; #endif #ifdef NINESLICED varying vec2 vMask; varying vec2 vTiledUv; uniform mediump vec4 innerOffset; uniform mediump vec2 outerScale; uniform mediump vec4 atlasRect; #endif vec3 dPositionW; mat4 dModelMatrix; #include "transformCoreVS" #ifdef UV0 attribute vec2 vertex_texCoord0; #include "uv0VS" #endif #ifdef UV1 attribute vec2 vertex_texCoord1; #include "uv1VS" #endif #ifdef LINEAR_DEPTH #ifndef VIEWMATRIX #define VIEWMATRIX uniform mat4 matrix_view; #endif #endif #include "transformVS" #ifdef NORMALS #include "normalCoreVS" #include "normalVS" #endif #ifdef TANGENTS attribute vec4 vertex_tangent; #endif #include "uvTransformUniformsPS, UV_TRANSFORMS_COUNT" #ifdef MSDF #include "msdfVS" #endif #include "litUserCodeVS" #ifdef VERTEX_COLOR vec3 decodeGamma(vec3 raw) { return pow(raw, vec3(2.2)); } vec4 gammaCorrectInput(vec4 color) { return vec4(decodeGamma(color.xyz), color.w); } #endif void main(void) { #include "litUserMainStartVS" gl_PointSize = 1.0; gl_Position = getPosition(); vPositionW = getWorldPosition(); #ifdef NORMALS vNormalW = getNormal(); #endif #ifdef TANGENTS vTangentW = normalize(dNormalMatrix * vertex_tangent.xyz); vBinormalW = cross(vNormalW, vTangentW) * vertex_tangent.w; #elif defined(GGX_SPECULAR) vObjectSpaceUpW = normalize(dNormalMatrix * vec3(0, 1, 0)); #endif #ifdef UV0 vec2 uv0 = getUv0(); #ifdef UV0_UNMODIFIED vUv0 = uv0; #endif #endif #ifdef UV1 vec2 uv1 = getUv1(); #ifdef UV1_UNMODIFIED vUv1 = uv1; #endif #endif #include "uvTransformVS, UV_TRANSFORMS_COUNT" #ifdef VERTEX_COLOR #ifdef STD_VERTEX_COLOR_GAMMA vVertexColor = gammaCorrectInput(vertex_color); #else vVertexColor = vertex_color; #endif #endif #ifdef LINEAR_DEPTH vLinearDepth = -(matrix_view * vec4(vPositionW, 1.0)).z; #endif #ifdef MSDF unpackMsdfParams(); #endif #include "litUserMainEndVS" } `; var litOtherMainPS$1 = ` #ifdef PICK_PASS #include "pickPS" #endif #ifdef PREPASS_PASS #include "floatAsUintPS" #endif void main(void) { #include "litUserMainStartPS" evaluateFrontend(); #ifdef PICK_PASS pcFragColor0 = getPickOutput(); #ifdef DEPTH_PICK_PASS pcFragColor1 = getPickDepth(); #endif #endif #ifdef PREPASS_PASS gl_FragColor = float2vec4(vLinearDepth); #endif #include "litUserMainEndPS" } `; var litShaderArgsPS$1 = ` vec3 litArgs_albedo; float litArgs_opacity; vec3 litArgs_emission; vec3 litArgs_worldNormal; float litArgs_ao; vec3 litArgs_lightmap; vec3 litArgs_lightmapDir; float litArgs_metalness; vec3 litArgs_specularity; float litArgs_specularityFactor; float litArgs_gloss; float litArgs_sheen_gloss; vec3 litArgs_sheen_specularity; float litArgs_transmission; float litArgs_thickness; float litArgs_ior; float litArgs_dispersion; float litArgs_iridescence_intensity; float litArgs_iridescence_thickness; vec3 litArgs_clearcoat_worldNormal; float litArgs_clearcoat_specularity; float litArgs_clearcoat_gloss; `; var litShaderCorePS$1 = ` #if LIT_NONE_SLICE_MODE == TILED const float textureBias = -1000.0; #else uniform float textureBias; #endif #include "litShaderArgsPS" `; var litShadowMainPS$1 = ` #if LIGHT_TYPE != DIRECTIONAL uniform vec3 view_position; uniform float light_radius; #endif #if SHADOW_TYPE == PCSS_32F #include "linearizeDepthPS" #endif void main(void) { #include "litUserMainStartPS" evaluateFrontend(); #ifdef PERSPECTIVE_DEPTH float depth = gl_FragCoord.z; #if SHADOW_TYPE == PCSS_32F #if LIGHT_TYPE != DIRECTIONAL depth = linearizeDepthWithParams(depth, camera_params); #endif #endif #else float depth = min(distance(view_position, vPositionW) / light_radius, 0.99999); #define MODIFIED_DEPTH #endif #if SHADOW_TYPE == VSM_16F || SHADOW_TYPE == VSM_32F #if SHADOW_TYPE == VSM_32F float exponent = 15.0; #else float exponent = 5.54; #endif depth = 2.0 * depth - 1.0; depth = exp(exponent * depth); gl_FragColor = vec4(depth, depth*depth, 1.0, 1.0); #else #if SHADOW_TYPE == PCSS_32F gl_FragColor.r = depth; #else #ifdef MODIFIED_DEPTH gl_FragDepth = depth; #endif gl_FragColor = vec4(1.0); #endif #endif #include "litUserMainEndPS" } `; var ltcPS$1 = ` mat3 transposeMat3( const in mat3 m ) { mat3 tmp; tmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ); tmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ); tmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ); return tmp; } vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { const float LUT_SIZE = 64.0; const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; const float LUT_BIAS = 0.5 / LUT_SIZE; float dotNV = saturate( dot( N, V ) ); vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); uv = uv * LUT_SCALE + LUT_BIAS; return uv; } float LTC_ClippedSphereFormFactor( const in vec3 f ) { float l = length( f ); return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); } vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { float x = dot( v1, v2 ); float y = abs( x ); float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; float b = 3.4175940 + ( 4.1616724 + y ) * y; float v = a / b; float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; return cross( v1, v2 ) * theta_sintheta; } struct Coords { vec3 coord0; vec3 coord1; vec3 coord2; vec3 coord3; }; float LTC_EvaluateRect( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in Coords rectCoords) { vec3 v1 = rectCoords.coord1 - rectCoords.coord0; vec3 v2 = rectCoords.coord3 - rectCoords.coord0; vec3 lightNormal = cross( v1, v2 ); float factor = sign(-dot( lightNormal, P - rectCoords.coord0 )); vec3 T1, T2; T1 = normalize( V - N * dot( V, N ) ); T2 = factor * cross( N, T1 ); mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); vec3 coords[ 4 ]; coords[ 0 ] = mat * ( rectCoords.coord0 - P ); coords[ 1 ] = mat * ( rectCoords.coord1 - P ); coords[ 2 ] = mat * ( rectCoords.coord2 - P ); coords[ 3 ] = mat * ( rectCoords.coord3 - P ); coords[ 0 ] = normalize( coords[ 0 ] ); coords[ 1 ] = normalize( coords[ 1 ] ); coords[ 2 ] = normalize( coords[ 2 ] ); coords[ 3 ] = normalize( coords[ 3 ] ); vec3 vectorFormFactor = vec3( 0.0 ); vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); return result; } Coords dLTCCoords; Coords getLTCLightCoords(vec3 lightPos, vec3 halfWidth, vec3 halfHeight){ Coords coords; coords.coord0 = lightPos + halfWidth - halfHeight; coords.coord1 = lightPos - halfWidth - halfHeight; coords.coord2 = lightPos - halfWidth + halfHeight; coords.coord3 = lightPos + halfWidth + halfHeight; return coords; } float dSphereRadius; Coords getSphereLightCoords(vec3 lightPos, vec3 halfWidth, vec3 halfHeight){ dSphereRadius = max(length(halfWidth), length(halfHeight)); vec3 f = reflect(normalize(lightPos - view_position), vNormalW); vec3 w = normalize(cross(f, halfHeight)); vec3 h = normalize(cross(f, w)); return getLTCLightCoords(lightPos, w * dSphereRadius, h * dSphereRadius); } vec2 dLTCUV; #ifdef LIT_CLEARCOAT vec2 ccLTCUV; #endif vec2 getLTCLightUV(float gloss, vec3 worldNormal, vec3 viewDir) { float roughness = max((1.0 - gloss) * (1.0 - gloss), 0.001); return LTC_Uv( worldNormal, viewDir, roughness ); } vec3 dLTCSpecFres; #ifdef LIT_CLEARCOAT vec3 ccLTCSpecFres; #endif vec3 getLTCLightSpecFres(vec2 uv, vec3 specularity) { vec4 t2 = texture2DLod(areaLightsLutTex2, uv, 0.0); return specularity * t2.x + ( vec3( 1.0 ) - specularity) * t2.y; } void calcLTCLightValues(float gloss, vec3 worldNormal, vec3 viewDir, vec3 specularity, float clearcoatGloss, vec3 clearcoatWorldNormal, float clearcoatSpecularity) { dLTCUV = getLTCLightUV(gloss, worldNormal, viewDir); dLTCSpecFres = getLTCLightSpecFres(dLTCUV, specularity); #ifdef LIT_CLEARCOAT ccLTCUV = getLTCLightUV(clearcoatGloss, clearcoatWorldNormal, viewDir); ccLTCSpecFres = getLTCLightSpecFres(ccLTCUV, vec3(clearcoatSpecularity)); #endif } void calcRectLightValues(vec3 lightPos, vec3 halfWidth, vec3 halfHeight) { dLTCCoords = getLTCLightCoords(lightPos, halfWidth, halfHeight); } void calcDiskLightValues(vec3 lightPos, vec3 halfWidth, vec3 halfHeight) { calcRectLightValues(lightPos, halfWidth, halfHeight); } void calcSphereLightValues(vec3 lightPos, vec3 halfWidth, vec3 halfHeight) { dLTCCoords = getSphereLightCoords(lightPos, halfWidth, halfHeight); } vec3 SolveCubic(vec4 Coefficient) { float pi = 3.14159; Coefficient.xyz /= Coefficient.w; Coefficient.yz /= 3.0; float A = Coefficient.w; float B = Coefficient.z; float C = Coefficient.y; float D = Coefficient.x; vec3 Delta = vec3( -Coefficient.z * Coefficient.z + Coefficient.y, -Coefficient.y * Coefficient.z + Coefficient.x, dot(vec2(Coefficient.z, -Coefficient.y), Coefficient.xy) ); float Discriminant = dot(vec2(4.0 * Delta.x, -Delta.y), Delta.zy); vec2 xlc, xsc; { float A_a = 1.0; float C_a = Delta.x; float D_a = -2.0 * B * Delta.x + Delta.y; float Theta = atan(sqrt(Discriminant), -D_a) / 3.0; float x_1a = 2.0 * sqrt(-C_a) * cos(Theta); float x_3a = 2.0 * sqrt(-C_a) * cos(Theta + (2.0 / 3.0) * pi); float xl; if ((x_1a + x_3a) > 2.0 * B) xl = x_1a; else xl = x_3a; xlc = vec2(xl - B, A); } { float A_d = D; float C_d = Delta.z; float D_d = -D * Delta.y + 2.0 * C * Delta.z; float Theta = atan(D * sqrt(Discriminant), -D_d) / 3.0; float x_1d = 2.0 * sqrt(-C_d) * cos(Theta); float x_3d = 2.0 * sqrt(-C_d) * cos(Theta + (2.0 / 3.0) * pi); float xs; if (x_1d + x_3d < 2.0 * C) xs = x_1d; else xs = x_3d; xsc = vec2(-D, xs + C); } float E = xlc.y * xsc.y; float F = -xlc.x * xsc.y - xlc.y * xsc.x; float G = xlc.x * xsc.x; vec2 xmc = vec2(C * F - B * G, -B * F + C * E); vec3 Root = vec3(xsc.x / xsc.y, xmc.x / xmc.y, xlc.x / xlc.y); if (Root.x < Root.y && Root.x < Root.z) Root.xyz = Root.yxz; else if (Root.z < Root.x && Root.z < Root.y) Root.xyz = Root.xzy; return Root; } float LTC_EvaluateDisk(vec3 N, vec3 V, vec3 P, mat3 Minv, Coords points) { vec3 T1 = normalize(V - N * dot(V, N)); vec3 T2 = cross(N, T1); mat3 R = transposeMat3( mat3( T1, T2, N ) ); vec3 L_[ 3 ]; L_[ 0 ] = R * ( points.coord0 - P ); L_[ 1 ] = R * ( points.coord1 - P ); L_[ 2 ] = R * ( points.coord2 - P ); vec3 C = 0.5 * (L_[0] + L_[2]); vec3 V1 = 0.5 * (L_[1] - L_[2]); vec3 V2 = 0.5 * (L_[1] - L_[0]); C = Minv * C; V1 = Minv * V1; V2 = Minv * V2; float a, b; float d11 = dot(V1, V1); float d22 = dot(V2, V2); float d12 = dot(V1, V2); if (abs(d12) / sqrt(d11 * d22) > 0.0001) { float tr = d11 + d22; float det = -d12 * d12 + d11 * d22; det = sqrt(det); float u = 0.5 * sqrt(tr - 2.0 * det); float v = 0.5 * sqrt(tr + 2.0 * det); float e_max = (u + v) * (u + v); float e_min = (u - v) * (u - v); vec3 V1_, V2_; if (d11 > d22) { V1_ = d12 * V1 + (e_max - d11) * V2; V2_ = d12 * V1 + (e_min - d11) * V2; } else { V1_ = d12*V2 + (e_max - d22)*V1; V2_ = d12*V2 + (e_min - d22)*V1; } a = 1.0 / e_max; b = 1.0 / e_min; V1 = normalize(V1_); V2 = normalize(V2_); } else { a = 1.0 / dot(V1, V1); b = 1.0 / dot(V2, V2); V1 *= sqrt(a); V2 *= sqrt(b); } vec3 V3 = normalize(cross(V1, V2)); if (dot(C, V3) < 0.0) V3 *= -1.0; float L = dot(V3, C); float x0 = dot(V1, C) / L; float y0 = dot(V2, C) / L; float E1 = inversesqrt(a); float E2 = inversesqrt(b); a *= L * L; b *= L * L; float c0 = a * b; float c1 = a * b * (1.0 + x0 * x0 + y0 * y0) - a - b; float c2 = 1.0 - a * (1.0 + x0 * x0) - b * (1.0 + y0 * y0); float c3 = 1.0; vec3 roots = SolveCubic(vec4(c0, c1, c2, c3)); float e1 = roots.x; float e2 = roots.y; float e3 = roots.z; vec3 avgDir = vec3(a * x0 / (a - e2), b * y0 / (b - e2), 1.0); mat3 rotate = mat3(V1, V2, V3); avgDir = rotate * avgDir; avgDir = normalize(avgDir); float L1 = sqrt(-e2 / e3); float L2 = sqrt(-e2 / e1); float formFactor = max(0.0, L1 * L2 * inversesqrt((1.0 + L1 * L1) * (1.0 + L2 * L2))); const float LUT_SIZE = 64.0; const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; const float LUT_BIAS = 0.5 / LUT_SIZE; vec2 uv = vec2(avgDir.z * 0.5 + 0.5, formFactor); uv = uv*LUT_SCALE + LUT_BIAS; float scale = texture2DLod(areaLightsLutTex2, uv, 0.0).w; return formFactor*scale; } float FixNan(float value) { #ifdef WEBGPU return value != value ? 0.0 : value; #else return isnan(value) ? 0.0 : value; #endif } float getRectLightDiffuse(vec3 worldNormal, vec3 viewDir, vec3 lightDir, vec3 lightDirNorm) { return LTC_EvaluateRect( worldNormal, viewDir, vPositionW, mat3( 1.0 ), dLTCCoords ); } float getDiskLightDiffuse(vec3 worldNormal, vec3 viewDir, vec3 lightDir, vec3 lightDirNorm) { return FixNan(LTC_EvaluateDisk( worldNormal, viewDir, vPositionW, mat3( 1.0 ), dLTCCoords )); } float getSphereLightDiffuse(vec3 worldNormal, vec3 viewDir, vec3 lightDir, vec3 lightDirNorm) { float falloff = dSphereRadius / (dot(lightDir, lightDir) + dSphereRadius); return FixNan(getLightDiffuse(worldNormal, viewDir, lightDirNorm) * falloff); } mat3 getLTCLightInvMat(vec2 uv) { vec4 t1 = texture2DLod(areaLightsLutTex1, uv, 0.0); return mat3( vec3( t1.x, 0, t1.y ), vec3( 0, 1, 0 ), vec3( t1.z, 0, t1.w ) ); } float calcRectLightSpecular(vec3 worldNormal, vec3 viewDir, vec2 uv) { mat3 mInv = getLTCLightInvMat(uv); return LTC_EvaluateRect( worldNormal, viewDir, vPositionW, mInv, dLTCCoords ); } float getRectLightSpecular(vec3 worldNormal, vec3 viewDir) { return calcRectLightSpecular(worldNormal, viewDir, dLTCUV); } float calcDiskLightSpecular(vec3 worldNormal, vec3 viewDir, vec2 uv) { mat3 mInv = getLTCLightInvMat(uv); return LTC_EvaluateDisk( worldNormal, viewDir, vPositionW, mInv, dLTCCoords ); } float getDiskLightSpecular(vec3 worldNormal, vec3 viewDir) { return calcDiskLightSpecular(worldNormal, viewDir, dLTCUV); } float getSphereLightSpecular(vec3 worldNormal, vec3 viewDir) { return calcDiskLightSpecular(worldNormal, viewDir, dLTCUV); } `; var metalnessPS$1 = ` #ifdef STD_METALNESS_CONSTANT uniform float material_metalness; #endif void getMetalness() { float metalness = 1.0; #ifdef STD_METALNESS_CONSTANT metalness *= material_metalness; #endif #ifdef STD_METALNESS_TEXTURE metalness *= texture2DBias({STD_METALNESS_TEXTURE_NAME}, {STD_METALNESS_TEXTURE_UV}, textureBias).{STD_METALNESS_TEXTURE_CHANNEL}; #endif #ifdef STD_METALNESS_VERTEX metalness *= saturate(vVertexColor.{STD_METALNESS_VERTEX_CHANNEL}); #endif dMetalness = metalness; } `; var msdfPS$1 = ` uniform sampler2D texture_msdfMap; float median(float r, float g, float b) { return max(min(r, g), min(max(r, g), b)); } float map (float min, float max, float v) { return (v - min) / (max - min); } uniform float font_sdfIntensity; uniform float font_pxrange; uniform float font_textureWidth; #ifndef LIT_MSDF_TEXT_ATTRIBUTE uniform vec4 outline_color; uniform float outline_thickness; uniform vec4 shadow_color; uniform vec2 shadow_offset; #else varying vec4 outline_color; varying float outline_thickness; varying vec4 shadow_color; varying vec2 shadow_offset; #endif vec4 applyMsdf(vec4 color) { color.rgb = gammaCorrectInput(color.rgb); vec3 tsample = texture2D(texture_msdfMap, vUv0).rgb; vec2 uvShdw = vUv0 - shadow_offset; vec3 ssample = texture2D(texture_msdfMap, uvShdw).rgb; float sigDist = median(tsample.r, tsample.g, tsample.b); float sigDistShdw = median(ssample.r, ssample.g, ssample.b); float smoothingMax = 0.2; vec2 w = fwidth(vUv0); float smoothing = clamp(w.x * font_textureWidth / font_pxrange, 0.0, smoothingMax); float mapMin = 0.05; float mapMax = clamp(1.0 - font_sdfIntensity, mapMin, 1.0); float sigDistInner = map(mapMin, mapMax, sigDist); float sigDistOutline = map(mapMin, mapMax, sigDist + outline_thickness); sigDistShdw = map(mapMin, mapMax, sigDistShdw + outline_thickness); float center = 0.5; float inside = smoothstep(center-smoothing, center+smoothing, sigDistInner); float outline = smoothstep(center-smoothing, center+smoothing, sigDistOutline); float shadow = smoothstep(center-smoothing, center+smoothing, sigDistShdw); vec4 tcolor = (outline > inside) ? outline * vec4(outline_color.a * outline_color.rgb, outline_color.a) : vec4(0.0); tcolor = mix(tcolor, color, inside); vec4 scolor = (shadow > outline) ? shadow * vec4(shadow_color.a * shadow_color.rgb, shadow_color.a) : tcolor; tcolor = mix(scolor, tcolor, outline); tcolor.rgb = gammaCorrectOutput(tcolor.rgb); return tcolor; } `; var metalnessModulatePS$1 = ` vec3 getSpecularModulate(in vec3 specularity, in vec3 albedo, in float metalness, in float f0, in float specularityFactor) { vec3 dielectricF0 = f0 * specularity * specularityFactor; return mix(dielectricF0, albedo, metalness); } vec3 getAlbedoModulate(in vec3 albedo, in float metalness) { return albedo * (1.0 - metalness); } `; var morphPS$1 = ` varying vec2 uv0; uniform sampler2DArray morphTexture; uniform highp float morphFactor[{MORPH_TEXTURE_MAX_COUNT}]; uniform highp uint morphIndex[{MORPH_TEXTURE_MAX_COUNT}]; uniform int count; #ifdef MORPH_INT uniform vec3 aabbSize; uniform vec3 aabbMin; #endif void main (void) { highp vec3 color = vec3(0, 0, 0); ivec2 pixelCoords = ivec2(uv0 * vec2(textureSize(morphTexture, 0).xy)); for (int i = 0; i < count; i++) { uint textureIndex = morphIndex[i]; vec3 delta = texelFetch(morphTexture, ivec3(pixelCoords, int(textureIndex)), 0).xyz; color += morphFactor[i] * delta; } #ifdef MORPH_INT color = (color - aabbMin) / aabbSize * 65535.0; gl_FragColor = uvec4(color, 1u); #else gl_FragColor = vec4(color, 1.0); #endif } `; var morphVS$1 = ` attribute vec2 vertex_position; varying vec2 uv0; void main(void) { gl_Position = vec4(vertex_position, 0.5, 1.0); uv0 = vertex_position.xy * 0.5 + 0.5; } `; var msdfVS$1 = ` attribute vec3 vertex_outlineParameters; attribute vec3 vertex_shadowParameters; varying vec4 outline_color; varying float outline_thickness; varying vec4 shadow_color; varying vec2 shadow_offset; void unpackMsdfParams() { vec3 little = mod(vertex_outlineParameters, 256.); vec3 big = (vertex_outlineParameters - little) / 256.; outline_color.rb = little.xy / 255.; outline_color.ga = big.xy / 255.; outline_thickness = little.z / 255. * 0.2; little = mod(vertex_shadowParameters, 256.); big = (vertex_shadowParameters - little) / 256.; shadow_color.rb = little.xy / 255.; shadow_color.ga = big.xy / 255.; shadow_offset = (vec2(little.z, big.z) / 127. - 1.) * 0.005; } `; var normalVS$1 = ` mat3 dNormalMatrix; vec3 getNormal() { dNormalMatrix = getNormalMatrix(dModelMatrix); vec3 localNormal = getLocalNormal(vertex_normal); return normalize(dNormalMatrix * localNormal); } `; var normalCoreVS$1 = ` attribute vec3 vertex_normal; uniform mat3 matrix_normal; #ifdef MORPHING_NORMAL #ifdef MORPHING_INT uniform highp usampler2D morphNormalTex; #else uniform highp sampler2D morphNormalTex; #endif #endif vec3 getLocalNormal(vec3 vertexNormal) { vec3 localNormal = vertex_normal; #ifdef MORPHING_NORMAL ivec2 morphUV = getTextureMorphCoords(); #ifdef MORPHING_INT vec3 morphNormal = vec3(texelFetch(morphNormalTex, ivec2(morphUV), 0).xyz) / 65535.0 * 2.0 - 1.0; #else vec3 morphNormal = texelFetch(morphNormalTex, ivec2(morphUV), 0).xyz; #endif localNormal += morphNormal; #endif return localNormal; } #if defined(SKIN) || defined(BATCH) mat3 getNormalMatrix(mat4 modelMatrix) { return mat3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz); } #elif defined(INSTANCING) mat3 getNormalMatrix(mat4 modelMatrix) { return mat3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz); } #else mat3 getNormalMatrix(mat4 modelMatrix) { return matrix_normal; } #endif `; var normalMapPS$1 = ` #ifdef STD_NORMAL_TEXTURE uniform float material_bumpiness; #endif #ifdef STD_NORMALDETAIL_TEXTURE uniform float material_normalDetailMapBumpiness; vec3 blendNormals(vec3 n1, vec3 n2) { n1 += vec3(0, 0, 1); n2 *= vec3(-1, -1, 1); return n1 * dot(n1, n2) / n1.z - n2; } #endif void getNormal() { #ifdef STD_NORMAL_TEXTURE vec3 normalMap = {STD_NORMAL_TEXTURE_DECODE}(texture2DBias({STD_NORMAL_TEXTURE_NAME}, {STD_NORMAL_TEXTURE_UV}, textureBias)); normalMap = mix(vec3(0.0, 0.0, 1.0), normalMap, material_bumpiness); #ifdef STD_NORMALDETAIL_TEXTURE vec3 normalDetailMap = {STD_NORMALDETAIL_TEXTURE_DECODE}(texture2DBias({STD_NORMALDETAIL_TEXTURE_NAME}, {STD_NORMALDETAIL_TEXTURE_UV}, textureBias)); normalDetailMap = mix(vec3(0.0, 0.0, 1.0), normalDetailMap, material_normalDetailMapBumpiness); normalMap = blendNormals(normalMap, normalDetailMap); #endif dNormalW = normalize(dTBN * normalMap); #else dNormalW = dVertexNormalW; #endif } `; var opacityPS$1 = ` uniform float material_opacity; void getOpacity() { dAlpha = material_opacity; #ifdef STD_OPACITY_TEXTURE dAlpha *= texture2DBias({STD_OPACITY_TEXTURE_NAME}, {STD_OPACITY_TEXTURE_UV}, textureBias).{STD_OPACITY_TEXTURE_CHANNEL}; #endif #ifdef STD_OPACITY_VERTEX dAlpha *= clamp(vVertexColor.{STD_OPACITY_VERTEX_CHANNEL}, 0.0, 1.0); #endif } `; var opacityDitherPS$1 = ` #if STD_OPACITY_DITHER == BAYER8 #include "bayerPS" #endif uniform vec4 blueNoiseJitter; #if STD_OPACITY_DITHER == BLUENOISE uniform sampler2D blueNoiseTex32; #endif void opacityDither(float alpha, float id) { #if STD_OPACITY_DITHER == BAYER8 float noise = bayer8(floor(mod(gl_FragCoord.xy + blueNoiseJitter.xy + id, 8.0))) / 64.0; #else #if STD_OPACITY_DITHER == BLUENOISE vec2 uv = fract(gl_FragCoord.xy / 32.0 + blueNoiseJitter.xy + id); float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y; #endif #if STD_OPACITY_DITHER == IGNNOISE vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189); float noise = fract(magic.z * fract(dot(gl_FragCoord.xy + blueNoiseJitter.xy + id, magic.xy))); #endif #endif noise = pow(noise, 2.2); if (alpha < noise) discard; } `; var outputPS$1 = ` `; var outputAlphaPS$1 = ` #if LIT_BLEND_TYPE == NORMAL || LIT_BLEND_TYPE == ADDITIVEALPHA || defined(LIT_ALPHA_TO_COVERAGE) gl_FragColor.a = litArgs_opacity; #elif LIT_BLEND_TYPE == PREMULTIPLIED gl_FragColor.rgb *= litArgs_opacity; gl_FragColor.a = litArgs_opacity; #else gl_FragColor.a = 1.0; #endif `; var outputTex2DPS$1 = ` varying vec2 vUv0; uniform sampler2D source; void main(void) { gl_FragColor = texture2D(source, vUv0); } `; var sheenPS$1 = ` uniform vec3 material_sheen; void getSheen() { vec3 sheenColor = material_sheen; #ifdef STD_SHEEN_TEXTURE sheenColor *= {STD_SHEEN_TEXTURE_DECODE}(texture2DBias({STD_SHEEN_TEXTURE_NAME}, {STD_SHEEN_TEXTURE_UV}, textureBias)).{STD_SHEEN_TEXTURE_CHANNEL}; #endif #ifdef STD_SHEEN_VERTEX sheenColor *= saturate(vVertexColor.{STD_SHEEN_VERTEX_CHANNEL}); #endif sSpecularity = sheenColor; } `; var sheenGlossPS$1 = ` uniform float material_sheenGloss; void getSheenGlossiness() { float sheenGlossiness = material_sheenGloss; #ifdef STD_SHEENGLOSS_TEXTURE sheenGlossiness *= texture2DBias({STD_SHEENGLOSS_TEXTURE_NAME}, {STD_SHEENGLOSS_TEXTURE_UV}, textureBias).{STD_SHEENGLOSS_TEXTURE_CHANNEL}; #endif #ifdef STD_SHEENGLOSS_VERTEX sheenGlossiness *= saturate(vVertexColor.{STD_SHEENGLOSS_VERTEX_CHANNEL}); #endif #ifdef STD_SHEENGLOSS_INVERT sheenGlossiness = 1.0 - sheenGlossiness; #endif sGlossiness = sheenGlossiness + 0.0000001; } `; var parallaxPS$1 = ` uniform float material_heightMapFactor; void getParallax() { float parallaxScale = material_heightMapFactor; float height = texture2DBias({STD_HEIGHT_TEXTURE_NAME}, {STD_HEIGHT_TEXTURE_UV}, textureBias).{STD_HEIGHT_TEXTURE_CHANNEL}; height = height * parallaxScale - parallaxScale * 0.5; vec3 viewDirT = dViewDirW * dTBN; viewDirT.z += 0.42; dUvOffset = height * (viewDirT.xy / viewDirT.z); } `; var pickPS$1 = ` uniform uint meshInstanceId; vec4 getPickOutput() { const vec4 inv = vec4(1.0 / 255.0); const uvec4 shifts = uvec4(16, 8, 0, 24); uvec4 col = (uvec4(meshInstanceId) >> shifts) & uvec4(0xff); return vec4(col) * inv; } #ifdef DEPTH_PICK_PASS #include "floatAsUintPS" vec4 getPickDepth() { return float2uint(gl_FragCoord.z); } #endif `; var reflDirPS$1 = ` void getReflDir(vec3 worldNormal, vec3 viewDir, float gloss, mat3 tbn) { dReflDirW = normalize(-reflect(viewDir, worldNormal)); } `; var reflDirAnisoPS$1 = ` void getReflDir(vec3 worldNormal, vec3 viewDir, float gloss, mat3 tbn) { float roughness = sqrt(1.0 - min(gloss, 1.0)); vec2 direction = dAnisotropyRotation; vec3 anisotropicT = normalize(tbn * vec3(direction, 0.0)); vec3 anisotropicB = normalize(cross(tbn[2], anisotropicT)); float anisotropy = dAnisotropy; vec3 anisotropicDirection = anisotropicB; vec3 anisotropicTangent = cross(anisotropicDirection, viewDir); vec3 anisotropicNormal = cross(anisotropicTangent, anisotropicDirection); float bendFactor = 1.0 - anisotropy * (1.0 - roughness); float bendFactor4 = bendFactor * bendFactor * bendFactor * bendFactor; vec3 bentNormal = normalize(mix(normalize(anisotropicNormal), normalize(worldNormal), bendFactor4)); dReflDirW = reflect(-viewDir, bentNormal); } `; var reflectionCCPS$1 = ` #ifdef LIT_CLEARCOAT void addReflectionCC(vec3 reflDir, float gloss) { ccReflection += calcReflection(reflDir, gloss); } #endif `; var reflectionCubePS$1 = ` uniform samplerCube texture_cubeMap; uniform float material_reflectivity; vec3 calcReflection(vec3 reflDir, float gloss) { vec3 lookupVec = cubeMapProject(reflDir); lookupVec.x *= -1.0; return {reflectionDecode}(textureCube(texture_cubeMap, lookupVec)); } void addReflection(vec3 reflDir, float gloss) { dReflection += vec4(calcReflection(reflDir, gloss), material_reflectivity); } `; var reflectionEnvHQPS$1 = ` #ifndef ENV_ATLAS #define ENV_ATLAS uniform sampler2D texture_envAtlas; #endif uniform samplerCube texture_cubeMap; uniform float material_reflectivity; vec3 calcReflection(vec3 reflDir, float gloss) { vec3 dir = cubeMapProject(reflDir) * vec3(-1.0, 1.0, 1.0); vec2 uv = toSphericalUv(dir); float level = saturate(1.0 - gloss) * 5.0; float ilevel = floor(level); float flevel = level - ilevel; vec3 sharp = {reflectionCubemapDecode}(textureCube(texture_cubeMap, dir)); vec3 roughA = {reflectionDecode}(texture2D(texture_envAtlas, mapRoughnessUv(uv, ilevel))); vec3 roughB = {reflectionDecode}(texture2D(texture_envAtlas, mapRoughnessUv(uv, ilevel + 1.0))); return processEnvironment(mix(sharp, mix(roughA, roughB, flevel), min(level, 1.0))); } void addReflection(vec3 reflDir, float gloss) { dReflection += vec4(calcReflection(reflDir, gloss), material_reflectivity); } `; var reflectionEnvPS$1 = ` #ifndef ENV_ATLAS #define ENV_ATLAS uniform sampler2D texture_envAtlas; #endif uniform float material_reflectivity; float shinyMipLevel(vec2 uv) { vec2 dx = dFdx(uv); vec2 dy = dFdy(uv); vec2 uv2 = vec2(fract(uv.x + 0.5), uv.y); vec2 dx2 = dFdx(uv2); vec2 dy2 = dFdy(uv2); float maxd = min(max(dot(dx, dx), dot(dy, dy)), max(dot(dx2, dx2), dot(dy2, dy2))); return clamp(0.5 * log2(maxd) - 1.0 + textureBias, 0.0, 5.0); } vec3 calcReflection(vec3 reflDir, float gloss) { vec3 dir = cubeMapProject(reflDir) * vec3(-1.0, 1.0, 1.0); vec2 uv = toSphericalUv(dir); float level = saturate(1.0 - gloss) * 5.0; float ilevel = floor(level); float level2 = shinyMipLevel(uv * atlasSize); float ilevel2 = floor(level2); vec2 uv0, uv1; float weight; if (ilevel == 0.0) { uv0 = mapShinyUv(uv, ilevel2); uv1 = mapShinyUv(uv, ilevel2 + 1.0); weight = level2 - ilevel2; } else { uv0 = uv1 = mapRoughnessUv(uv, ilevel); weight = 0.0; } vec3 linearA = {reflectionDecode}(texture2D(texture_envAtlas, uv0)); vec3 linearB = {reflectionDecode}(texture2D(texture_envAtlas, uv1)); vec3 linear0 = mix(linearA, linearB, weight); vec3 linear1 = {reflectionDecode}(texture2D(texture_envAtlas, mapRoughnessUv(uv, ilevel + 1.0))); return processEnvironment(mix(linear0, linear1, level - ilevel)); } void addReflection(vec3 reflDir, float gloss) { dReflection += vec4(calcReflection(reflDir, gloss), material_reflectivity); } `; var reflectionSpherePS$1 = ` #ifndef VIEWMATRIX #define VIEWMATRIX uniform mat4 matrix_view; #endif uniform sampler2D texture_sphereMap; uniform float material_reflectivity; vec3 calcReflection(vec3 reflDir, float gloss) { vec3 reflDirV = (mat3(matrix_view) * reflDir); float m = 2.0 * sqrt(dot(reflDirV.xy, reflDirV.xy) + (reflDirV.z + 1.0) * (reflDirV.z + 1.0)); vec2 sphereMapUv = reflDirV.xy / m + 0.5; return {reflectionDecode}(texture2D(texture_sphereMap, sphereMapUv)); } void addReflection(vec3 reflDir, float gloss) { dReflection += vec4(calcReflection(reflDir, gloss), material_reflectivity); } `; var reflectionSheenPS$1 = ` void addReflectionSheen(vec3 worldNormal, vec3 viewDir, float gloss) { float NoV = dot(worldNormal, viewDir); float alphaG = gloss * gloss; float a = gloss < 0.25 ? -339.2 * alphaG + 161.4 * gloss - 25.9 : -8.48 * alphaG + 14.3 * gloss - 9.95; float b = gloss < 0.25 ? 44.0 * alphaG - 23.7 * gloss + 3.26 : 1.97 * alphaG - 3.27 * gloss + 0.72; float DG = exp( a * NoV + b ) + ( gloss < 0.25 ? 0.0 : 0.1 * ( gloss - 0.25 ) ); sReflection += calcReflection(worldNormal, 0.0) * saturate(DG); } `; var refractionCubePS$1 = ` vec3 refract2(vec3 viewVec, vec3 normal, float IOR) { float vn = dot(viewVec, normal); float k = 1.0 - IOR * IOR * (1.0 - vn * vn); vec3 refrVec = IOR * viewVec - (IOR * vn + sqrt(k)) * normal; return refrVec; } void addRefraction( vec3 worldNormal, vec3 viewDir, float thickness, float gloss, vec3 specularity, vec3 albedo, float transmission, float refractionIndex, float dispersion #if defined(LIT_IRIDESCENCE) , vec3 iridescenceFresnel, float iridescenceIntensity #endif ) { vec4 tmpRefl = dReflection; vec3 reflectionDir = refract2(-viewDir, worldNormal, refractionIndex); dReflection = vec4(0); addReflection(reflectionDir, gloss); dDiffuseLight = mix(dDiffuseLight, dReflection.rgb * albedo, transmission); dReflection = tmpRefl; } `; var refractionDynamicPS$1 = ` uniform float material_invAttenuationDistance; uniform vec3 material_attenuation; vec3 evalRefractionColor(vec3 refractionVector, float gloss, float refractionIndex) { vec4 pointOfRefraction = vec4(vPositionW + refractionVector, 1.0); vec4 projectionPoint = matrix_viewProjection * pointOfRefraction; vec2 uv = getGrabScreenPos(projectionPoint); float iorToRoughness = (1.0 - gloss) * clamp((1.0 / refractionIndex) * 2.0 - 2.0, 0.0, 1.0); float refractionLod = log2(uScreenSize.x) * iorToRoughness; vec3 refraction = texture2DLod(uSceneColorMap, uv, refractionLod).rgb; #ifdef SCENE_COLORMAP_GAMMA refraction = decodeGamma(refraction); #endif return refraction; } void addRefraction( vec3 worldNormal, vec3 viewDir, float thickness, float gloss, vec3 specularity, vec3 albedo, float transmission, float refractionIndex, float dispersion #if defined(LIT_IRIDESCENCE) , vec3 iridescenceFresnel, float iridescenceIntensity #endif ) { vec3 modelScale; modelScale.x = length(vec3(matrix_model[0].xyz)); modelScale.y = length(vec3(matrix_model[1].xyz)); modelScale.z = length(vec3(matrix_model[2].xyz)); vec3 scale = thickness * modelScale; vec3 refractionVector = normalize(refract(-viewDir, worldNormal, refractionIndex)) * scale; vec3 refraction = evalRefractionColor(refractionVector, gloss, refractionIndex); #ifdef LIT_DISPERSION float halfSpread = (1.0 / refractionIndex - 1.0) * 0.025 * dispersion; float refractionIndexR = refractionIndex - halfSpread; refractionVector = normalize(refract(-viewDir, worldNormal, refractionIndexR)) * scale; refraction.r = evalRefractionColor(refractionVector, gloss, refractionIndexR).r; float refractionIndexB = refractionIndex + halfSpread; refractionVector = normalize(refract(-viewDir, worldNormal, refractionIndexB)) * scale; refraction.b = evalRefractionColor(refractionVector, gloss, refractionIndexB).b; #endif vec3 transmittance; if (material_invAttenuationDistance != 0.0) { vec3 attenuation = -log(material_attenuation) * material_invAttenuationDistance; transmittance = exp(-attenuation * length(refractionVector)); } else { transmittance = vec3(1.0); } vec3 fresnel = vec3(1.0) - getFresnel( dot(viewDir, worldNormal), gloss, specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, iridescenceIntensity #endif ); dDiffuseLight = mix(dDiffuseLight, refraction * transmittance * fresnel, transmission); } `; var reprojectPS$1 = ` varying vec2 vUv0; #ifdef CUBEMAP_SOURCE uniform samplerCube sourceCube; #else uniform sampler2D sourceTex; #endif #ifdef USE_SAMPLES_TEX uniform sampler2D samplesTex; uniform vec2 samplesTexInverseSize; #endif uniform vec3 params; float targetFace() { return params.x; } float targetTotalPixels() { return params.y; } float sourceTotalPixels() { return params.z; } float PI = 3.141592653589793; float saturate(float x) { return clamp(x, 0.0, 1.0); } #include "decodePS" #include "encodePS" vec3 modifySeams(vec3 dir, float scale) { vec3 adir = abs(dir); float M = max(max(adir.x, adir.y), adir.z); return dir / M * vec3( adir.x == M ? 1.0 : scale, adir.y == M ? 1.0 : scale, adir.z == M ? 1.0 : scale ); } vec2 toSpherical(vec3 dir) { return vec2(dir.xz == vec2(0.0) ? 0.0 : atan(dir.x, dir.z), asin(dir.y)); } vec3 fromSpherical(vec2 uv) { return vec3(cos(uv.y) * sin(uv.x), sin(uv.y), cos(uv.y) * cos(uv.x)); } vec3 getDirectionEquirect() { return fromSpherical((vec2(vUv0.x, 1.0 - vUv0.y) * 2.0 - 1.0) * vec2(PI, PI * 0.5)); } float signNotZero(float k){ return(k >= 0.0) ? 1.0 : -1.0; } vec2 signNotZero(vec2 v) { return vec2(signNotZero(v.x), signNotZero(v.y)); } vec3 octDecode(vec2 o) { vec3 v = vec3(o.x, 1.0 - abs(o.x) - abs(o.y), o.y); if (v.y < 0.0) { v.xz = (1.0 - abs(v.zx)) * signNotZero(v.xz); } return normalize(v); } vec3 getDirectionOctahedral() { return octDecode(vec2(vUv0.x, 1.0 - vUv0.y) * 2.0 - 1.0); } vec2 octEncode(in vec3 v) { float l1norm = abs(v.x) + abs(v.y) + abs(v.z); vec2 result = v.xz * (1.0 / l1norm); if (v.y < 0.0) { result = (1.0 - abs(result.yx)) * signNotZero(result.xy); } return result; } #ifdef CUBEMAP_SOURCE vec4 sampleCubemap(vec3 dir) { return textureCube(sourceCube, modifySeams(dir, 1.0)); } vec4 sampleCubemap(vec2 sph) { return sampleCubemap(fromSpherical(sph)); } vec4 sampleCubemap(vec3 dir, float mipLevel) { return textureCubeLod(sourceCube, modifySeams(dir, 1.0), mipLevel); } vec4 sampleCubemap(vec2 sph, float mipLevel) { return sampleCubemap(fromSpherical(sph), mipLevel); } #else vec4 sampleEquirect(vec2 sph) { vec2 uv = sph / vec2(PI * 2.0, PI) + 0.5; return texture2D(sourceTex, vec2(uv.x, 1.0 - uv.y)); } vec4 sampleEquirect(vec3 dir) { return sampleEquirect(toSpherical(dir)); } vec4 sampleEquirect(vec2 sph, float mipLevel) { vec2 uv = sph / vec2(PI * 2.0, PI) + 0.5; return texture2DLod(sourceTex, vec2(uv.x, 1.0 - uv.y), mipLevel); } vec4 sampleEquirect(vec3 dir, float mipLevel) { return sampleEquirect(toSpherical(dir), mipLevel); } vec4 sampleOctahedral(vec3 dir) { vec2 uv = octEncode(dir) * 0.5 + 0.5; return texture2D(sourceTex, vec2(uv.x, 1.0 - uv.y)); } vec4 sampleOctahedral(vec2 sph) { return sampleOctahedral(fromSpherical(sph)); } vec4 sampleOctahedral(vec3 dir, float mipLevel) { vec2 uv = octEncode(dir) * 0.5 + 0.5; return texture2DLod(sourceTex, vec2(uv.x, 1.0 - uv.y), mipLevel); } vec4 sampleOctahedral(vec2 sph, float mipLevel) { return sampleOctahedral(fromSpherical(sph), mipLevel); } #endif vec3 getDirectionCubemap() { vec2 st = vUv0 * 2.0 - 1.0; float face = targetFace(); vec3 vec; if (face == 0.0) { vec = vec3(1, -st.y, -st.x); } else if (face == 1.0) { vec = vec3(-1, -st.y, st.x); } else if (face == 2.0) { vec = vec3(st.x, 1, st.y); } else if (face == 3.0) { vec = vec3(st.x, -1, -st.y); } else if (face == 4.0) { vec = vec3(st.x, -st.y, 1); } else { vec = vec3(-st.x, -st.y, -1); } return normalize(modifySeams(vec, 1.0)); } mat3 matrixFromVector(vec3 n) { float a = 1.0 / (1.0 + n.z); float b = -n.x * n.y * a; vec3 b1 = vec3(1.0 - n.x * n.x * a, b, -n.x); vec3 b2 = vec3(b, 1.0 - n.y * n.y * a, -n.y); return mat3(b1, b2, n); } mat3 matrixFromVectorSlow(vec3 n) { vec3 up = (1.0 - abs(n.y) <= 0.0000001) ? vec3(0.0, 0.0, n.y > 0.0 ? 1.0 : -1.0) : vec3(0.0, 1.0, 0.0); vec3 x = normalize(cross(up, n)); vec3 y = cross(n, x); return mat3(x, y, n); } vec4 reproject() { if ({NUM_SAMPLES} <= 1) { return {ENCODE_FUNC}({DECODE_FUNC}({SOURCE_FUNC}({TARGET_FUNC}()))); } else { vec3 t = {TARGET_FUNC}(); vec3 tu = dFdx(t); vec3 tv = dFdy(t); vec3 result = vec3(0.0); for (float u = 0.0; u < {NUM_SAMPLES_SQRT}; ++u) { for (float v = 0.0; v < {NUM_SAMPLES_SQRT}; ++v) { result += {DECODE_FUNC}({SOURCE_FUNC}(normalize(t + tu * (u / {NUM_SAMPLES_SQRT} - 0.5) + tv * (v / {NUM_SAMPLES_SQRT} - 0.5)))); } } return {ENCODE_FUNC}(result / ({NUM_SAMPLES_SQRT} * {NUM_SAMPLES_SQRT})); } } vec4 unpackFloat = vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0); #ifdef USE_SAMPLES_TEX void unpackSample(int i, out vec3 L, out float mipLevel) { float u = (float(i * 4) + 0.5) * samplesTexInverseSize.x; float v = (floor(u) + 0.5) * samplesTexInverseSize.y; vec4 raw; raw.x = dot(texture2D(samplesTex, vec2(u, v)), unpackFloat); u += samplesTexInverseSize.x; raw.y = dot(texture2D(samplesTex, vec2(u, v)), unpackFloat); u += samplesTexInverseSize.x; raw.z = dot(texture2D(samplesTex, vec2(u, v)), unpackFloat); u += samplesTexInverseSize.x; raw.w = dot(texture2D(samplesTex, vec2(u, v)), unpackFloat); L.xyz = raw.xyz * 2.0 - 1.0; mipLevel = raw.w * 8.0; } vec4 prefilterSamples() { mat3 vecSpace = matrixFromVectorSlow({TARGET_FUNC}()); vec3 L; float mipLevel; vec3 result = vec3(0.0); float totalWeight = 0.0; for (int i = 0; i < {NUM_SAMPLES}; ++i) { unpackSample(i, L, mipLevel); result += {DECODE_FUNC}({SOURCE_FUNC}(vecSpace * L, mipLevel)) * L.z; totalWeight += L.z; } return {ENCODE_FUNC}(result / totalWeight); } vec4 prefilterSamplesUnweighted() { mat3 vecSpace = matrixFromVectorSlow({TARGET_FUNC}()); vec3 L; float mipLevel; vec3 result = vec3(0.0); float totalWeight = 0.0; for (int i = 0; i < {NUM_SAMPLES}; ++i) { unpackSample(i, L, mipLevel); result += {DECODE_FUNC}({SOURCE_FUNC}(vecSpace * L, mipLevel)); } return {ENCODE_FUNC}(result / float({NUM_SAMPLES})); } #endif void main(void) { gl_FragColor = {PROCESS_FUNC}(); } `; var reprojectVS$1 = ` attribute vec2 vertex_position; uniform vec4 uvMod; varying vec2 vUv0; void main(void) { gl_Position = vec4(vertex_position, 0.5, 1.0); vUv0 = getImageEffectUV((vertex_position.xy * 0.5 + 0.5) * uvMod.xy + uvMod.zw); } `; var screenDepthPS$1 = ` uniform highp sampler2D uSceneDepthMap; #ifndef SCREENSIZE #define SCREENSIZE uniform vec4 uScreenSize; #endif #ifndef VIEWMATRIX #define VIEWMATRIX uniform mat4 matrix_view; #endif #ifndef LINEARIZE_DEPTH #define LINEARIZE_DEPTH #ifndef CAMERAPLANES #define CAMERAPLANES uniform vec4 camera_params; #endif float linearizeDepth(float z) { if (camera_params.w == 0.0) return (camera_params.z * camera_params.y) / (camera_params.y + z * (camera_params.z - camera_params.y)); else return camera_params.z + z * (camera_params.y - camera_params.z); } #endif float delinearizeDepth(float linearDepth) { if (camera_params.w == 0.0) { return (camera_params.y * (camera_params.z - linearDepth)) / (linearDepth * (camera_params.z - camera_params.y)); } else { return (linearDepth - camera_params.z) / (camera_params.y - camera_params.z); } } float getLinearScreenDepth(vec2 uv) { #ifdef SCENE_DEPTHMAP_LINEAR #ifdef SCENE_DEPTHMAP_FLOAT return texture2D(uSceneDepthMap, uv).r; #else ivec2 textureSize = textureSize(uSceneDepthMap, 0); ivec2 texel = ivec2(uv * vec2(textureSize)); vec4 data = texelFetch(uSceneDepthMap, texel, 0); uint intBits = (uint(data.r * 255.0) << 24u) | (uint(data.g * 255.0) << 16u) | (uint(data.b * 255.0) << 8u) | uint(data.a * 255.0); return uintBitsToFloat(intBits); #endif #else return linearizeDepth(texture2D(uSceneDepthMap, uv).r); #endif } #ifndef VERTEXSHADER float getLinearScreenDepth() { vec2 uv = gl_FragCoord.xy * uScreenSize.zw; return getLinearScreenDepth(uv); } #endif float getLinearDepth(vec3 pos) { return -(matrix_view * vec4(pos, 1.0)).z; } `; var shadowCascadesPS$1 = ` int getShadowCascadeIndex(vec4 shadowCascadeDistances, int shadowCascadeCount) { float depth = 1.0 / gl_FragCoord.w; vec4 comparisons = step(shadowCascadeDistances, vec4(depth)); int cascadeIndex = int(dot(comparisons, vec4(1.0))); return min(cascadeIndex, shadowCascadeCount - 1); } int ditherShadowCascadeIndex(int cascadeIndex, vec4 shadowCascadeDistances, int shadowCascadeCount, float blendFactor) { if (cascadeIndex < shadowCascadeCount - 1) { float currentRangeEnd = shadowCascadeDistances[cascadeIndex]; float transitionStart = blendFactor * currentRangeEnd; float depth = 1.0 / gl_FragCoord.w; if (depth > transitionStart) { float transitionFactor = smoothstep(transitionStart, currentRangeEnd, depth); float dither = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453); if (dither < transitionFactor) { cascadeIndex += 1; } } } return cascadeIndex; } vec3 fadeShadow(vec3 shadowCoord, vec4 shadowCascadeDistances) { float depth = 1.0 / gl_FragCoord.w; if (depth > shadowCascadeDistances.w) { shadowCoord.z = -9999999.0; } return shadowCoord; } `; var shadowEVSMPS$1 = ` float linstep(float a, float b, float v) { return saturate((v - a) / (b - a)); } float reduceLightBleeding(float pMax, float amount) { return linstep(amount, 1.0, pMax); } float chebyshevUpperBound(vec2 moments, float mean, float minVariance, float lightBleedingReduction) { float variance = moments.y - (moments.x * moments.x); variance = max(variance, minVariance); float d = mean - moments.x; float pMax = variance / (variance + (d * d)); pMax = reduceLightBleeding(pMax, lightBleedingReduction); return (mean <= moments.x ? 1.0 : pMax); } float calculateEVSM(vec3 moments, float Z, float vsmBias, float exponent) { Z = 2.0 * Z - 1.0; float warpedDepth = exp(exponent * Z); moments.xy += vec2(warpedDepth, warpedDepth*warpedDepth) * (1.0 - moments.z); float VSMBias = vsmBias; float depthScale = VSMBias * exponent * warpedDepth; float minVariance1 = depthScale * depthScale; return chebyshevUpperBound(moments.xy, warpedDepth, minVariance1, 0.1); } float VSM16(TEXTURE_ACCEPT(tex), vec2 texCoords, float resolution, float Z, float vsmBias, float exponent) { vec3 moments = texture2DLod(tex, texCoords, 0.0).xyz; return calculateEVSM(moments, Z, vsmBias, exponent); } float getShadowVSM16(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent) { return VSM16(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent); } float getShadowSpotVSM16(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) { return VSM16(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, length(lightDir) * shadowParams.w + shadowParams.z, shadowParams.y, exponent); } float VSM32(TEXTURE_ACCEPT(tex), vec2 texCoords, float resolution, float Z, float vsmBias, float exponent) { #ifdef CAPS_TEXTURE_FLOAT_FILTERABLE vec3 moments = texture2DLod(tex, texCoords, 0.0).xyz; #else float pixelSize = 1.0 / resolution; texCoords -= vec2(pixelSize); vec3 s00 = texture2DLod(tex, texCoords, 0.0).xyz; vec3 s10 = texture2DLod(tex, texCoords + vec2(pixelSize, 0), 0.0).xyz; vec3 s01 = texture2DLod(tex, texCoords + vec2(0, pixelSize), 0.0).xyz; vec3 s11 = texture2DLod(tex, texCoords + vec2(pixelSize), 0.0).xyz; vec2 fr = fract(texCoords * resolution); vec3 h0 = mix(s00, s10, fr.x); vec3 h1 = mix(s01, s11, fr.x); vec3 moments = mix(h0, h1, fr.y); #endif return calculateEVSM(moments, Z, vsmBias, exponent); } float getShadowVSM32(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent) { return VSM32(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent); } float getShadowSpotVSM32(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, float exponent, vec3 lightDir) { float Z = length(lightDir) * shadowParams.w + shadowParams.z; return VSM32(TEXTURE_PASS(shadowMap), shadowCoord.xy, shadowParams.x, Z, shadowParams.y, exponent); } `; var shadowPCF1PS$1 = ` float getShadowPCF1x1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return textureShadow(shadowMap, shadowCoord); } float getShadowSpotPCF1x1(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return textureShadow(shadowMap, shadowCoord); } #ifndef WEBGPU float getShadowOmniPCF1x1(samplerCubeShadow shadowMap, vec3 shadowCoord, vec4 shadowParams, vec3 lightDir) { float shadowZ = length(lightDir) * shadowParams.w + shadowParams.z; return texture(shadowMap, vec4(lightDir, shadowZ)); } #endif `; var shadowPCF3PS$1 = ` float _getShadowPCF3x3(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec3 shadowParams) { float z = shadowCoord.z; vec2 uv = shadowCoord.xy * shadowParams.x; float shadowMapSizeInv = 1.0 / shadowParams.x; vec2 base_uv = floor(uv + 0.5); float s = (uv.x + 0.5 - base_uv.x); float t = (uv.y + 0.5 - base_uv.y); base_uv -= vec2(0.5); base_uv *= shadowMapSizeInv; float sum = 0.0; float uw0 = (3.0 - 2.0 * s); float uw1 = (1.0 + 2.0 * s); float u0 = (2.0 - s) / uw0 - 1.0; float u1 = s / uw1 + 1.0; float vw0 = (3.0 - 2.0 * t); float vw1 = (1.0 + 2.0 * t); float v0 = (2.0 - t) / vw0 - 1.0; float v1 = t / vw1 + 1.0; u0 = u0 * shadowMapSizeInv + base_uv.x; v0 = v0 * shadowMapSizeInv + base_uv.y; u1 = u1 * shadowMapSizeInv + base_uv.x; v1 = v1 * shadowMapSizeInv + base_uv.y; sum += uw0 * vw0 * textureShadow(shadowMap, vec3(u0, v0, z)); sum += uw1 * vw0 * textureShadow(shadowMap, vec3(u1, v0, z)); sum += uw0 * vw1 * textureShadow(shadowMap, vec3(u0, v1, z)); sum += uw1 * vw1 * textureShadow(shadowMap, vec3(u1, v1, z)); sum *= 1.0f / 16.0; return sum; } float getShadowPCF3x3(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return _getShadowPCF3x3(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams.xyz); } float getShadowSpotPCF3x3(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return _getShadowPCF3x3(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams.xyz); } #ifndef WEBGPU float getShadowOmniPCF3x3(samplerCubeShadow shadowMap, vec4 shadowParams, vec3 dir) { float shadowZ = length(dir) * shadowParams.w + shadowParams.z; float z = 1.0 / float(textureSize(shadowMap, 0)); vec3 tc = normalize(dir); mediump vec4 shadows; shadows.x = texture(shadowMap, vec4(tc + vec3( z, z, z), shadowZ)); shadows.y = texture(shadowMap, vec4(tc + vec3(-z,-z, z), shadowZ)); shadows.z = texture(shadowMap, vec4(tc + vec3(-z, z,-z), shadowZ)); shadows.w = texture(shadowMap, vec4(tc + vec3( z,-z,-z), shadowZ)); return dot(shadows, vec4(0.25)); } float getShadowOmniPCF3x3(samplerCubeShadow shadowMap, vec3 shadowCoord, vec4 shadowParams, vec3 lightDir) { return getShadowOmniPCF3x3(shadowMap, shadowParams, lightDir); } #endif `; var shadowPCF5PS$1 = ` float _getShadowPCF5x5(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec3 shadowParams) { float z = shadowCoord.z; vec2 uv = shadowCoord.xy * shadowParams.x; float shadowMapSizeInv = 1.0 / shadowParams.x; vec2 base_uv = floor(uv + 0.5); float s = (uv.x + 0.5 - base_uv.x); float t = (uv.y + 0.5 - base_uv.y); base_uv -= vec2(0.5); base_uv *= shadowMapSizeInv; float uw0 = (4.0 - 3.0 * s); float uw1 = 7.0; float uw2 = (1.0 + 3.0 * s); float u0 = (3.0 - 2.0 * s) / uw0 - 2.0; float u1 = (3.0 + s) / uw1; float u2 = s / uw2 + 2.0; float vw0 = (4.0 - 3.0 * t); float vw1 = 7.0; float vw2 = (1.0 + 3.0 * t); float v0 = (3.0 - 2.0 * t) / vw0 - 2.0; float v1 = (3.0 + t) / vw1; float v2 = t / vw2 + 2.0; float sum = 0.0; u0 = u0 * shadowMapSizeInv + base_uv.x; v0 = v0 * shadowMapSizeInv + base_uv.y; u1 = u1 * shadowMapSizeInv + base_uv.x; v1 = v1 * shadowMapSizeInv + base_uv.y; u2 = u2 * shadowMapSizeInv + base_uv.x; v2 = v2 * shadowMapSizeInv + base_uv.y; sum += uw0 * vw0 * textureShadow(shadowMap, vec3(u0, v0, z)); sum += uw1 * vw0 * textureShadow(shadowMap, vec3(u1, v0, z)); sum += uw2 * vw0 * textureShadow(shadowMap, vec3(u2, v0, z)); sum += uw0 * vw1 * textureShadow(shadowMap, vec3(u0, v1, z)); sum += uw1 * vw1 * textureShadow(shadowMap, vec3(u1, v1, z)); sum += uw2 * vw1 * textureShadow(shadowMap, vec3(u2, v1, z)); sum += uw0 * vw2 * textureShadow(shadowMap, vec3(u0, v2, z)); sum += uw1 * vw2 * textureShadow(shadowMap, vec3(u1, v2, z)); sum += uw2 * vw2 * textureShadow(shadowMap, vec3(u2, v2, z)); sum *= 1.0f / 144.0; sum = saturate(sum); return sum; } float getShadowPCF5x5(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return _getShadowPCF5x5(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams.xyz); } float getShadowSpotPCF5x5(SHADOWMAP_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams) { return _getShadowPCF5x5(SHADOWMAP_PASS(shadowMap), shadowCoord, shadowParams.xyz); } `; var shadowPCSSPS = ` #define PCSS_SAMPLE_COUNT 16 uniform float pcssDiskSamples[PCSS_SAMPLE_COUNT]; uniform float pcssSphereSamples[PCSS_SAMPLE_COUNT]; vec2 vogelDisk(int sampleIndex, float count, float phi, float r) { const float GoldenAngle = 2.4; float theta = float(sampleIndex) * GoldenAngle + phi; float sine = sin(theta); float cosine = cos(theta); return vec2(r * cosine, r * sine); } vec3 vogelSphere(int sampleIndex, float count, float phi, float r) { const float GoldenAngle = 2.4; float theta = float(sampleIndex) * GoldenAngle + phi; float weight = float(sampleIndex) / count; return vec3(cos(theta) * r, weight, sin(theta) * r); } float noise(vec2 screenPos) { const float PHI = 1.61803398874989484820459; return fract(sin(dot(screenPos * PHI, screenPos)) * screenPos.x); } float viewSpaceDepth(float depth, mat4 invProjection) { float z = depth * 2.0 - 1.0; vec4 clipSpace = vec4(0.0, 0.0, z, 1.0); vec4 viewSpace = invProjection * clipSpace; return viewSpace.z; } float PCSSBlockerDistance(TEXTURE_ACCEPT(shadowMap), vec2 sampleCoords[PCSS_SAMPLE_COUNT], vec2 shadowCoords, vec2 searchSize, float z, vec4 cameraParams) { float blockers = 0.0; float averageBlocker = 0.0; for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) { vec2 offset = sampleCoords[i] * searchSize; vec2 sampleUV = shadowCoords + offset; float blocker = texture2DLod(shadowMap, sampleUV, 0.0).r; float isBlocking = step(blocker, z); blockers += isBlocking; averageBlocker += blocker * isBlocking; } if (blockers > 0.0) return averageBlocker / blockers; return -1.0; } float PCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, vec2 shadowSearchArea) { float receiverDepth = linearizeDepthWithParams(shadowCoords.z, cameraParams); vec2 samplePoints[PCSS_SAMPLE_COUNT]; const float PI = 3.141592653589793; float noise = noise( gl_FragCoord.xy ) * 2.0 * PI; for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) { float pcssPresample = pcssDiskSamples[i]; samplePoints[i] = vogelDisk(i, float(PCSS_SAMPLE_COUNT), noise, pcssPresample); } float averageBlocker = PCSSBlockerDistance(TEXTURE_PASS(shadowMap), samplePoints, shadowCoords.xy, shadowSearchArea, receiverDepth, cameraParams); if (averageBlocker == -1.0) { return 1.0; } else { float depthDifference = (receiverDepth - averageBlocker) / 3.0; vec2 filterRadius = depthDifference * shadowSearchArea; float shadow = 0.0; for (int i = 0; i < PCSS_SAMPLE_COUNT; i ++) { vec2 sampleUV = samplePoints[i] * filterRadius; sampleUV = shadowCoords.xy + sampleUV; float depth = texture2DLod(shadowMap, sampleUV, 0.0).r; shadow += step(receiverDepth, depth); } return shadow / float(PCSS_SAMPLE_COUNT); } } #ifndef WEBGPU float PCSSCubeBlockerDistance(samplerCube shadowMap, vec3 lightDirNorm, vec3 samplePoints[PCSS_SAMPLE_COUNT], float z, float shadowSearchArea) { float blockers = 0.0; float averageBlocker = 0.0; for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) { vec3 sampleDir = lightDirNorm + samplePoints[i] * shadowSearchArea; sampleDir = normalize(sampleDir); float blocker = textureCubeLod(shadowMap, sampleDir, 0.0).r; float isBlocking = step(blocker, z); blockers += isBlocking; averageBlocker += blocker * isBlocking; } if (blockers > 0.0) return averageBlocker / blockers; return -1.0; } float PCSSCube(samplerCube shadowMap, vec4 shadowParams, vec3 shadowCoords, vec4 cameraParams, float shadowSearchArea, vec3 lightDir) { vec3 samplePoints[PCSS_SAMPLE_COUNT]; const float PI = 3.141592653589793; float noise = noise( gl_FragCoord.xy ) * 2.0 * PI; for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) { float r = pcssSphereSamples[i]; samplePoints[i] = vogelSphere(i, float(PCSS_SAMPLE_COUNT), noise, r); } float receiverDepth = length(lightDir) * shadowParams.w + shadowParams.z; vec3 lightDirNorm = normalize(lightDir); float averageBlocker = PCSSCubeBlockerDistance(shadowMap, lightDirNorm, samplePoints, receiverDepth, shadowSearchArea); if (averageBlocker == -1.0) { return 1.0; } else { float filterRadius = ((receiverDepth - averageBlocker) / averageBlocker) * shadowSearchArea; float shadow = 0.0; for (int i = 0; i < PCSS_SAMPLE_COUNT; i++) { vec3 offset = samplePoints[i] * filterRadius; vec3 sampleDir = lightDirNorm + offset; sampleDir = normalize(sampleDir); float depth = textureCubeLod(shadowMap, sampleDir, 0.0).r; shadow += step(receiverDepth, depth); } return shadow / float(PCSS_SAMPLE_COUNT); } } float getShadowOmniPCSS(samplerCube shadowMap, vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 shadowSearchArea, vec3 lightDir) { return PCSSCube(shadowMap, shadowParams, shadowCoord, cameraParams, shadowSearchArea.x, lightDir); } #endif float getShadowSpotPCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec2 shadowSearchArea, vec3 lightDir) { return PCSS(TEXTURE_PASS(shadowMap), shadowCoord, cameraParams, shadowSearchArea); } `; var shadowSoftPS$1 = ` highp float fractSinRand(const in vec2 uv) { const float PI = 3.141592653589793; 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); } struct VogelDiskData { float invNumSamples; float initialAngle; float currentPointId; }; void prepareDiskConstants(out VogelDiskData data, int sampleCount, float randomSeed) { const float pi2 = 6.28318530718; data.invNumSamples = 1.0 / float(sampleCount); data.initialAngle = randomSeed * pi2; data.currentPointId = 0.0; } vec2 generateDiskSample(inout VogelDiskData data) { const float GOLDEN_ANGLE = 2.399963; float r = sqrt((data.currentPointId + 0.5) * data.invNumSamples); float theta = data.currentPointId * GOLDEN_ANGLE + data.initialAngle; vec2 offset = vec2(cos(theta), sin(theta)) * pow(r, 1.33); data.currentPointId += 1.0; return offset; } void PCSSFindBlocker(TEXTURE_ACCEPT(shadowMap), out float avgBlockerDepth, out int numBlockers, vec2 shadowCoords, float z, int shadowBlockerSamples, float penumbraSize, float invShadowMapSize, float randomSeed) { VogelDiskData diskData; prepareDiskConstants(diskData, shadowBlockerSamples, randomSeed); float searchWidth = penumbraSize * invShadowMapSize; float blockerSum = 0.0; numBlockers = 0; for( int i = 0; i < shadowBlockerSamples; ++i ) { vec2 diskUV = generateDiskSample(diskData); vec2 sampleUV = shadowCoords + diskUV * searchWidth; float shadowMapDepth = texture2DLod(shadowMap, sampleUV, 0.0).r; if ( shadowMapDepth < z ) { blockerSum += shadowMapDepth; numBlockers++; } } avgBlockerDepth = blockerSum / float(numBlockers); } float PCSSFilter(TEXTURE_ACCEPT(shadowMap), vec2 uv, float receiverDepth, int shadowSamples, float filterRadius, float randomSeed) { VogelDiskData diskData; prepareDiskConstants(diskData, shadowSamples, randomSeed); float sum = 0.0; for (int i = 0; i < shadowSamples; i++) { vec2 offsetUV = generateDiskSample(diskData) * filterRadius; float depth = texture2DLod(shadowMap, uv + offsetUV, 0.0).r; sum += step(receiverDepth, depth); } return sum / float(shadowSamples); } float getPenumbra(float dblocker, float dreceiver, float penumbraSize, float penumbraFalloff) { float dist = dreceiver - dblocker; float penumbra = 1.0 - pow(1.0 - dist, penumbraFalloff); return penumbra * penumbraSize; } float PCSSDirectional(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoords, vec4 cameraParams, vec4 softShadowParams) { float receiverDepth = shadowCoords.z; float randomSeed = fractSinRand(gl_FragCoord.xy); int shadowSamples = int(softShadowParams.x); int shadowBlockerSamples = int(softShadowParams.y); float penumbraSize = softShadowParams.z; float penumbraFalloff = softShadowParams.w; int shadowMapSize = textureSize(shadowMap, 0).x; float invShadowMapSize = 1.0 / float(shadowMapSize); invShadowMapSize *= float(shadowMapSize) / 2048.0; float penumbra; if (shadowBlockerSamples > 0) { float avgBlockerDepth = 0.0; int numBlockers = 0; PCSSFindBlocker(TEXTURE_PASS(shadowMap), avgBlockerDepth, numBlockers, shadowCoords.xy, receiverDepth, shadowBlockerSamples, penumbraSize, invShadowMapSize, randomSeed); if (numBlockers < 1) return 1.0f; penumbra = getPenumbra(avgBlockerDepth, shadowCoords.z, penumbraSize, penumbraFalloff); } else { penumbra = penumbraSize; } float filterRadius = penumbra * invShadowMapSize; return PCSSFilter(TEXTURE_PASS(shadowMap), shadowCoords.xy, receiverDepth, shadowSamples, filterRadius, randomSeed); } float getShadowPCSS(TEXTURE_ACCEPT(shadowMap), vec3 shadowCoord, vec4 shadowParams, vec4 cameraParams, vec4 softShadowParams, vec3 lightDir) { return PCSSDirectional(TEXTURE_PASS(shadowMap), shadowCoord, cameraParams, softShadowParams); } `; var skinBatchVS$1 = ` attribute float vertex_boneIndices; uniform highp sampler2D texture_poseMap; mat4 getBoneMatrix(const in float indexFloat) { int width = textureSize(texture_poseMap, 0).x; int index = int(indexFloat + 0.5) * 3; int iy = index / width; int ix = index % width; vec4 v1 = texelFetch(texture_poseMap, ivec2(ix + 0, iy), 0); vec4 v2 = texelFetch(texture_poseMap, ivec2(ix + 1, iy), 0); vec4 v3 = texelFetch(texture_poseMap, ivec2(ix + 2, iy), 0); return mat4( v1.x, v2.x, v3.x, 0, v1.y, v2.y, v3.y, 0, v1.z, v2.z, v3.z, 0, v1.w, v2.w, v3.w, 1 ); } `; var skinVS$1 = ` attribute vec4 vertex_boneWeights; attribute vec4 vertex_boneIndices; uniform highp sampler2D texture_poseMap; void getBoneMatrix(const in int width, const in int index, out vec4 v1, out vec4 v2, out vec4 v3) { int v = index / width; int u = index % width; v1 = texelFetch(texture_poseMap, ivec2(u + 0, v), 0); v2 = texelFetch(texture_poseMap, ivec2(u + 1, v), 0); v3 = texelFetch(texture_poseMap, ivec2(u + 2, v), 0); } mat4 getSkinMatrix(const in vec4 indicesFloat, const in vec4 weights) { int width = textureSize(texture_poseMap, 0).x; ivec4 indices = ivec4(indicesFloat + 0.5) * 3; vec4 a1, a2, a3; getBoneMatrix(width, indices.x, a1, a2, a3); vec4 b1, b2, b3; getBoneMatrix(width, indices.y, b1, b2, b3); vec4 c1, c2, c3; getBoneMatrix(width, indices.z, c1, c2, c3); vec4 d1, d2, d3; getBoneMatrix(width, indices.w, d1, d2, d3); vec4 v1 = a1 * weights.x + b1 * weights.y + c1 * weights.z + d1 * weights.w; vec4 v2 = a2 * weights.x + b2 * weights.y + c2 * weights.z + d2 * weights.w; vec4 v3 = a3 * weights.x + b3 * weights.y + c3 * weights.z + d3 * weights.w; float one = dot(weights, vec4(1.0)); return mat4( v1.x, v2.x, v3.x, 0, v1.y, v2.y, v3.y, 0, v1.z, v2.z, v3.z, 0, v1.w, v2.w, v3.w, one ); } `; var skyboxPS$1 = ` #define LIT_SKYBOX_INTENSITY #include "envProcPS" #include "gammaPS" #include "tonemappingPS" #ifdef PREPASS_PASS varying float vLinearDepth; #include "floatAsUintPS" #endif varying vec3 vViewDir; uniform float skyboxHighlightMultiplier; #ifdef SKY_CUBEMAP uniform samplerCube texture_cubeMap; #ifdef SKYMESH varying vec3 vWorldPos; uniform mat3 cubeMapRotationMatrix; uniform vec3 projectedSkydomeCenter; #endif #else #include "sphericalPS" #include "envAtlasPS" uniform sampler2D texture_envAtlas; uniform float mipLevel; #endif void main(void) { #ifdef PREPASS_PASS gl_FragColor = float2vec4(vLinearDepth); #else #ifdef SKY_CUBEMAP #ifdef SKYMESH vec3 envDir = normalize(vWorldPos - projectedSkydomeCenter); vec3 dir = envDir * cubeMapRotationMatrix; #else vec3 dir = vViewDir; #endif dir.x *= -1.0; vec3 linear = {SKYBOX_DECODE_FNC}(textureCube(texture_cubeMap, dir)); #else vec3 dir = vViewDir * vec3(-1.0, 1.0, 1.0); vec2 uv = toSphericalUv(normalize(dir)); vec3 linear = {SKYBOX_DECODE_FNC}(texture2D(texture_envAtlas, mapRoughnessUv(uv, mipLevel))); #endif if (any(greaterThanEqual(linear, vec3(64.0)))) { linear *= skyboxHighlightMultiplier; } gl_FragColor = vec4(gammaCorrectOutput(toneMap(processEnvironment(linear))), 1.0); #endif } `; var skyboxVS$1 = ` attribute vec4 aPosition; uniform mat4 matrix_view; uniform mat4 matrix_projectionSkybox; uniform mat3 cubeMapRotationMatrix; varying vec3 vViewDir; #ifdef PREPASS_PASS varying float vLinearDepth; #endif #ifdef SKYMESH uniform mat4 matrix_model; varying vec3 vWorldPos; #endif void main(void) { mat4 view = matrix_view; #ifdef SKYMESH vec4 worldPos = matrix_model * aPosition; vWorldPos = worldPos.xyz; gl_Position = matrix_projectionSkybox * (view * worldPos); #ifdef PREPASS_PASS vLinearDepth = -(matrix_view * vec4(vWorldPos, 1.0)).z; #endif #else view[3][0] = view[3][1] = view[3][2] = 0.0; gl_Position = matrix_projectionSkybox * (view * aPosition); vViewDir = aPosition.xyz * cubeMapRotationMatrix; #ifdef PREPASS_PASS vLinearDepth = -gl_Position.w; #endif #endif gl_Position.z = gl_Position.w - 1.0e-7; } `; var specularPS$1 = ` #ifdef STD_SPECULAR_CONSTANT uniform vec3 material_specular; #endif void getSpecularity() { vec3 specularColor = vec3(1,1,1); #ifdef STD_SPECULAR_CONSTANT specularColor *= material_specular; #endif #ifdef STD_SPECULAR_TEXTURE specularColor *= {STD_SPECULAR_TEXTURE_DECODE}(texture2DBias({STD_SPECULAR_TEXTURE_NAME}, {STD_SPECULAR_TEXTURE_UV}, textureBias)).{STD_SPECULAR_TEXTURE_CHANNEL}; #endif #ifdef STD_SPECULAR_VERTEX specularColor *= saturate(vVertexColor.{STD_SPECULAR_VERTEX_CHANNEL}); #endif dSpecularity = specularColor; } `; var sphericalPS$1 = ` vec2 toSpherical(vec3 dir) { return vec2(dir.xz == vec2(0.0) ? 0.0 : atan(dir.x, dir.z), asin(dir.y)); } vec2 toSphericalUv(vec3 dir) { const float PI = 3.141592653589793; vec2 uv = toSpherical(dir) / vec2(PI * 2.0, PI) + 0.5; return vec2(uv.x, 1.0 - uv.y); } `; var specularityFactorPS$1 = ` #ifdef STD_SPECULARITYFACTOR_CONSTANT uniform float material_specularityFactor; #endif void getSpecularityFactor() { float specularityFactor = 1.0; #ifdef STD_SPECULARITYFACTOR_CONSTANT specularityFactor *= material_specularityFactor; #endif #ifdef STD_SPECULARITYFACTOR_TEXTURE specularityFactor *= texture2DBias({STD_SPECULARITYFACTOR_TEXTURE_NAME}, {STD_SPECULARITYFACTOR_TEXTURE_UV}, textureBias).{STD_SPECULARITYFACTOR_TEXTURE_CHANNEL}; #endif #ifdef STD_SPECULARITYFACTOR_VERTEX specularityFactor *= saturate(vVertexColor.{STD_SPECULARITYFACTOR_VERTEX_CHANNEL}); #endif dSpecularityFactor = specularityFactor; } `; var spotPS$1 = ` float getSpotEffect(vec3 lightSpotDir, float lightInnerConeAngle, float lightOuterConeAngle, vec3 lightDirNorm) { float cosAngle = dot(lightDirNorm, lightSpotDir); return smoothstep(lightOuterConeAngle, lightInnerConeAngle, cosAngle); } `; var startNineSlicedPS$1 = ` nineSlicedUv = vec2(vUv0.x, 1.0 - vUv0.y); `; var startNineSlicedTiledPS$1 = ` vec2 tileMask = step(vMask, vec2(0.99999)); vec2 tileSize = 0.5 * (innerOffset.xy + innerOffset.zw); vec2 tileScale = vec2(1.0) / (vec2(1.0) - tileSize); vec2 clampedUv = mix(innerOffset.xy * 0.5, vec2(1.0) - innerOffset.zw * 0.5, fract((vTiledUv - tileSize) * tileScale)); clampedUv = clampedUv * atlasRect.zw + atlasRect.xy; nineSlicedUv = vUv0 * tileMask + clampedUv * (vec2(1.0) - tileMask); nineSlicedUv.y = 1.0 - nineSlicedUv.y; `; var stdDeclarationPS$1 = ` float dAlpha = 1.0; #if LIT_BLEND_TYPE != NONE || defined(LIT_ALPHA_TEST) || defined(LIT_ALPHA_TO_COVERAGE) || STD_OPACITY_DITHER != NONE #ifdef STD_OPACITY_TEXTURE_ALLOCATE uniform sampler2D texture_opacityMap; #endif #endif #ifdef FORWARD_PASS vec3 dAlbedo; vec3 dNormalW; vec3 dSpecularity = vec3(0.0); float dGlossiness = 0.0; #ifdef LIT_REFRACTION float dTransmission; float dThickness; #endif #ifdef LIT_SCENE_COLOR uniform sampler2D uSceneColorMap; #endif #ifdef LIT_SCREEN_SIZE uniform vec4 uScreenSize; #endif #ifdef LIT_TRANSFORMS uniform mat4 matrix_viewProjection; uniform mat4 matrix_model; #endif #ifdef STD_HEIGHT_MAP vec2 dUvOffset; #ifdef STD_HEIGHT_TEXTURE_ALLOCATE uniform sampler2D texture_heightMap; #endif #endif #ifdef STD_DIFFUSE_TEXTURE_ALLOCATE uniform sampler2D texture_diffuseMap; #endif #ifdef STD_DIFFUSEDETAIL_TEXTURE_ALLOCATE uniform sampler2D texture_diffuseDetailMap; #endif #ifdef STD_NORMAL_TEXTURE_ALLOCATE uniform sampler2D texture_normalMap; #endif #ifdef STD_NORMALDETAIL_TEXTURE_ALLOCATE uniform sampler2D texture_normalDetailMap; #endif #ifdef STD_THICKNESS_TEXTURE_ALLOCATE uniform sampler2D texture_thicknessMap; #endif #ifdef STD_REFRACTION_TEXTURE_ALLOCATE uniform sampler2D texture_refractionMap; #endif #ifdef LIT_IRIDESCENCE float dIridescence; float dIridescenceThickness; #ifdef STD_IRIDESCENCE_THICKNESS_TEXTURE_ALLOCATE uniform sampler2D texture_iridescenceThicknessMap; #endif #ifdef STD_IRIDESCENCE_TEXTURE_ALLOCATE uniform sampler2D texture_iridescenceMap; #endif #endif #ifdef LIT_CLEARCOAT float ccSpecularity; float ccGlossiness; vec3 ccNormalW; #endif #ifdef LIT_GGX_SPECULAR float dAnisotropy; vec2 dAnisotropyRotation; #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_SHEEN vec3 sSpecularity; float sGlossiness; #ifdef STD_SHEEN_TEXTURE_ALLOCATE uniform sampler2D texture_sheenMap; #endif #ifdef STD_SHEENGLOSS_TEXTURE_ALLOCATE uniform sampler2D texture_sheenGlossMap; #endif #endif #ifdef LIT_METALNESS float dMetalness; float dIor; #ifdef STD_METALNESS_TEXTURE_ALLOCATE uniform sampler2D texture_metalnessMap; #endif #endif #ifdef LIT_SPECULARITY_FACTOR float dSpecularityFactor; #ifdef STD_SPECULARITYFACTOR_TEXTURE_ALLOCATE uniform sampler2D texture_specularityFactorMap; #endif #endif #ifdef STD_SPECULAR_COLOR #ifdef STD_SPECULAR_TEXTURE_ALLOCATE uniform sampler2D texture_specularMap; #endif #endif #ifdef STD_GLOSS_TEXTURE_ALLOCATE uniform sampler2D texture_glossMap; #endif #endif #ifdef STD_AO float dAo; #ifdef STD_AO_TEXTURE_ALLOCATE uniform sampler2D texture_aoMap; #endif #ifdef STD_AODETAIL_TEXTURE_ALLOCATE uniform sampler2D texture_aoDetailMap; #endif #endif vec3 dEmission; #ifdef STD_EMISSIVE_TEXTURE_ALLOCATE uniform sampler2D texture_emissiveMap; #endif #ifdef LIT_CLEARCOAT #ifdef STD_CLEARCOAT_TEXTURE_ALLOCATE uniform sampler2D texture_clearCoatMap; #endif #ifdef STD_CLEARCOATGLOSS_TEXTURE_ALLOCATE uniform sampler2D texture_clearCoatGlossMap; #endif #ifdef STD_CLEARCOATNORMAL_TEXTURE_ALLOCATE uniform sampler2D texture_clearCoatNormalMap; #endif #endif #ifdef LIT_GGX_SPECULAR #ifdef STD_ANISOTROPY_TEXTURE_ALLOCATE uniform sampler2D texture_anisotropyMap; #endif #endif #if defined(STD_LIGHTMAP) || defined(STD_LIGHT_VERTEX_COLOR) vec3 dLightmap; #ifdef STD_LIGHT_TEXTURE_ALLOCATE uniform sampler2D texture_lightMap; #endif #endif #endif #include "litShaderCorePS" `; var stdFrontEndPS$1 = ` #if LIT_BLEND_TYPE != NONE || defined(LIT_ALPHA_TEST) || defined(LIT_ALPHA_TO_COVERAGE) || STD_OPACITY_DITHER != NONE #include "opacityPS" #if defined(LIT_ALPHA_TEST) #include "alphaTestPS" #endif #if STD_OPACITY_DITHER != NONE #include "opacityDitherPS" #endif #endif #ifdef FORWARD_PASS #ifdef STD_HEIGHT_MAP #include "parallaxPS" #endif #include "diffusePS" #ifdef LIT_NEEDS_NORMAL #include "normalMapPS" #endif #ifdef LIT_REFRACTION #include "transmissionPS" #include "thicknessPS" #endif #ifdef LIT_IRIDESCENCE #include "iridescencePS" #include "iridescenceThicknessPS" #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_SHEEN #include "sheenPS" #include "sheenGlossPS" #endif #ifdef LIT_METALNESS #include "metalnessPS" #include "iorPS" #endif #ifdef LIT_SPECULARITY_FACTOR #include "specularityFactorPS" #endif #ifdef STD_SPECULAR_COLOR #include "specularPS" #else void getSpecularity() { dSpecularity = vec3(1); } #endif #include "glossPS" #endif #ifdef STD_AO #include "aoPS" #endif #include "emissivePS" #ifdef LIT_CLEARCOAT #include "clearCoatPS" #include "clearCoatGlossPS" #include "clearCoatNormalPS" #endif #if defined(LIT_SPECULAR) && defined(LIT_LIGHTING) && defined(LIT_GGX_SPECULAR) #include "anisotropyPS" #endif #if defined(STD_LIGHTMAP) || defined(STD_LIGHT_VERTEX_COLOR) #include "lightmapPS" #endif #endif void evaluateFrontend() { #if LIT_BLEND_TYPE != NONE || defined(LIT_ALPHA_TEST) || defined(LIT_ALPHA_TO_COVERAGE) || STD_OPACITY_DITHER != NONE getOpacity(); #if defined(LIT_ALPHA_TEST) alphaTest(dAlpha); #endif #if STD_OPACITY_DITHER != NONE opacityDither(dAlpha, 0.0); #endif litArgs_opacity = dAlpha; #endif #ifdef FORWARD_PASS #ifdef STD_HEIGHT_MAP getParallax(); #endif getAlbedo(); litArgs_albedo = dAlbedo; #ifdef LIT_NEEDS_NORMAL getNormal(); litArgs_worldNormal = dNormalW; #endif #ifdef LIT_REFRACTION getRefraction(); litArgs_transmission = dTransmission; getThickness(); litArgs_thickness = dThickness; #ifdef LIT_DISPERSION litArgs_dispersion = material_dispersion; #endif #endif #ifdef LIT_IRIDESCENCE getIridescence(); getIridescenceThickness(); litArgs_iridescence_intensity = dIridescence; litArgs_iridescence_thickness = dIridescenceThickness; #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_SHEEN getSheen(); litArgs_sheen_specularity = sSpecularity; getSheenGlossiness(); litArgs_sheen_gloss = sGlossiness; #endif #ifdef LIT_METALNESS getMetalness(); litArgs_metalness = dMetalness; getIor(); litArgs_ior = dIor; #endif #ifdef LIT_SPECULARITY_FACTOR getSpecularityFactor(); litArgs_specularityFactor = dSpecularityFactor; #endif getGlossiness(); getSpecularity(); litArgs_specularity = dSpecularity; litArgs_gloss = dGlossiness; #endif #ifdef STD_AO getAO(); litArgs_ao = dAo; #endif getEmission(); litArgs_emission = dEmission; #ifdef LIT_CLEARCOAT getClearCoat(); getClearCoatGlossiness(); getClearCoatNormal(); litArgs_clearcoat_specularity = ccSpecularity; litArgs_clearcoat_gloss = ccGlossiness; litArgs_clearcoat_worldNormal = ccNormalW; #endif #if defined(LIT_SPECULAR) && defined(LIT_LIGHTING) && defined(LIT_GGX_SPECULAR) getAnisotropy(); #endif #if defined(STD_LIGHTMAP) || defined(STD_LIGHT_VERTEX_COLOR) getLightMap(); litArgs_lightmap = dLightmap; #ifdef STD_LIGHTMAP_DIR litArgs_lightmapDir = dLightmapDir; #endif #endif #endif } `; var TBNPS$1 = ` #ifdef LIT_TANGENTS #define TBN_TANGENTS #else #if defined(LIT_USE_NORMALS) || defined(LIT_USE_CLEARCOAT_NORMALS) #define TBN_DERIVATIVES #endif #endif #if defined(TBN_DERIVATIVES) uniform float tbnBasis; #endif void getTBN(vec3 tangent, vec3 binormal, vec3 normal) { #ifdef TBN_TANGENTS dTBN = mat3(normalize(tangent), normalize(binormal), normalize(normal)); #elif defined(TBN_DERIVATIVES) vec2 uv = {lightingUv}; vec3 dp1 = dFdx( vPositionW ); vec3 dp2 = dFdy( vPositionW ); vec2 duv1 = dFdx( uv ); vec2 duv2 = dFdy( uv ); vec3 dp2perp = cross( dp2, normal ); vec3 dp1perp = cross( normal, dp1 ); vec3 T = dp2perp * duv1.x + dp1perp * duv2.x; vec3 B = dp2perp * duv1.y + dp1perp * duv2.y; float denom = max( dot(T,T), dot(B,B) ); float invmax = (denom == 0.0) ? 0.0 : tbnBasis / sqrt( denom ); dTBN = mat3(T * invmax, -B * invmax, normal ); #else vec3 B = cross(normal, vObjectSpaceUpW); vec3 T = cross(normal, B); if (dot(B,B)==0.0) { float major=max(max(normal.x, normal.y), normal.z); if (normal.x == major) { B = cross(normal, vec3(0,1,0)); T = cross(normal, B); } else if (normal.y == major) { B = cross(normal, vec3(0,0,1)); T = cross(normal, B); } else if (normal.z == major) { B = cross(normal, vec3(1,0,0)); T = cross(normal, B); } } dTBN = mat3(normalize(T), normalize(B), normalize(normal)); #endif } `; var thicknessPS$1 = ` #ifdef STD_THICKNESS_CONSTANT uniform float material_thickness; #endif void getThickness() { dThickness = 1.0; #ifdef STD_THICKNESS_CONSTANT dThickness *= material_thickness; #endif #ifdef STD_THICKNESS_TEXTURE dThickness *= texture2DBias({STD_THICKNESS_TEXTURE_NAME}, {STD_THICKNESS_TEXTURE_UV}, textureBias).{STD_THICKNESS_TEXTURE_CHANNEL}; #endif #ifdef STD_THICKNESS_VERTEX dThickness *= saturate(vVertexColor.{STD_THICKNESS_VERTEX_CHANNEL}); #endif } `; var tonemappingPS$1 = ` #if (TONEMAP == NONE) #include "tonemappingNonePS" #elif TONEMAP == FILMIC #include "tonemappingFilmicPS" #elif TONEMAP == LINEAR #include "tonemappingLinearPS" #elif TONEMAP == HEJL #include "tonemappingHejlPS" #elif TONEMAP == ACES #include "tonemappingAcesPS" #elif TONEMAP == ACES2 #include "tonemappingAces2PS" #elif TONEMAP == NEUTRAL #include "tonemappingNeutralPS" #endif `; var tonemappingAcesPS$1 = ` uniform float exposure; vec3 toneMap(vec3 color) { float tA = 2.51; float tB = 0.03; float tC = 2.43; float tD = 0.59; float tE = 0.14; vec3 x = color * exposure; return (x*(tA*x+tB))/(x*(tC*x+tD)+tE); } `; var tonemappingAces2PS$1 = ` uniform float exposure; const mat3 ACESInputMat = mat3( 0.59719, 0.35458, 0.04823, 0.07600, 0.90834, 0.01566, 0.02840, 0.13383, 0.83777 ); const mat3 ACESOutputMat = mat3( 1.60475, -0.53108, -0.07367, -0.10208, 1.10813, -0.00605, -0.00327, -0.07276, 1.07602 ); vec3 RRTAndODTFit(vec3 v) { vec3 a = v * (v + 0.0245786) - 0.000090537; vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; return a / b; } vec3 toneMap(vec3 color) { color *= exposure / 0.6; color = color * ACESInputMat; color = RRTAndODTFit(color); color = color * ACESOutputMat; color = clamp(color, 0.0, 1.0); return color; } `; var tonemappingFilmicPS$1 = ` const float A = 0.15; const float B = 0.50; const float C = 0.10; const float D = 0.20; const float E = 0.02; const float F = 0.30; const float W = 11.2; uniform float exposure; vec3 uncharted2Tonemap(vec3 x) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F; } vec3 toneMap(vec3 color) { color = uncharted2Tonemap(color * exposure); vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W,W,W)); color = color * whiteScale; return color; } `; var tonemappingHejlPS$1 = ` uniform float exposure; vec3 toneMap(vec3 color) { color *= exposure; const float A = 0.22, B = 0.3, C = .1, D = 0.2, E = .01, F = 0.3; const float Scl = 1.25; vec3 h = max( vec3(0.0), color - vec3(0.004) ); return (h*((Scl*A)*h+Scl*vec3(C*B,C*B,C*B))+Scl*vec3(D*E,D*E,D*E)) / (h*(A*h+vec3(B,B,B))+vec3(D*F,D*F,D*F)) - Scl*vec3(E/F,E/F,E/F); } `; var tonemappingLinearPS$1 = ` uniform float exposure; vec3 toneMap(vec3 color) { return color * exposure; } `; var tonemappingNeutralPS$1 = ` uniform float exposure; vec3 toneMap(vec3 color) { color *= exposure; float startCompression = 0.8 - 0.04; float desaturation = 0.15; float x = min(color.r, min(color.g, color.b)); float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; color -= offset; float peak = max(color.r, max(color.g, color.b)); if (peak < startCompression) return color; float d = 1. - startCompression; float newPeak = 1. - d * d / (peak + d - startCompression); color *= newPeak / peak; float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.); return mix(color, newPeak * vec3(1, 1, 1), g); } `; var tonemappingNonePS$1 = ` vec3 toneMap(vec3 color) { return color; } `; var transformVS$1 = ` #ifdef PIXELSNAP uniform vec4 uScreenSize; #endif #ifdef SCREENSPACE uniform float projectionFlipY; #endif vec4 evalWorldPosition(vec3 vertexPosition, mat4 modelMatrix) { vec3 localPos = getLocalPosition(vertexPosition); #ifdef NINESLICED localPos.xz *= outerScale; vec2 positiveUnitOffset = clamp(vertexPosition.xz, vec2(0.0), vec2(1.0)); vec2 negativeUnitOffset = clamp(-vertexPosition.xz, vec2(0.0), vec2(1.0)); localPos.xz += (-positiveUnitOffset * innerOffset.xy + negativeUnitOffset * innerOffset.zw) * vertex_texCoord0.xy; vTiledUv = (localPos.xz - outerScale + innerOffset.xy) * -0.5 + 1.0; localPos.xz *= -0.5; localPos = localPos.xzy; #endif vec4 posW = modelMatrix * vec4(localPos, 1.0); #ifdef SCREENSPACE posW.zw = vec2(0.0, 1.0); #endif return posW; } vec4 getPosition() { dModelMatrix = getModelMatrix(); vec4 posW = evalWorldPosition(vertex_position.xyz, dModelMatrix); dPositionW = posW.xyz; vec4 screenPos; #ifdef UV1LAYOUT screenPos = vec4(vertex_texCoord1.xy * 2.0 - 1.0, 0.5, 1); #ifdef WEBGPU screenPos.y *= -1.0; #endif #else #ifdef SCREENSPACE screenPos = posW; screenPos.y *= projectionFlipY; #else screenPos = matrix_viewProjection * posW; #endif #ifdef PIXELSNAP screenPos.xy = (screenPos.xy * 0.5) + 0.5; screenPos.xy *= uScreenSize.xy; screenPos.xy = floor(screenPos.xy); screenPos.xy *= uScreenSize.zw; screenPos.xy = (screenPos.xy * 2.0) - 1.0; #endif #endif return screenPos; } vec3 getWorldPosition() { return dPositionW; } `; var transformCoreVS$1 = ` attribute vec4 vertex_position; uniform mat4 matrix_viewProjection; uniform mat4 matrix_model; #ifdef MORPHING uniform vec2 morph_tex_params; attribute uint morph_vertex_id; ivec2 getTextureMorphCoords() { ivec2 textureSize = ivec2(morph_tex_params); int morphGridV = int(morph_vertex_id) / textureSize.x; int morphGridU = int(morph_vertex_id) - (morphGridV * textureSize.x); #ifdef WEBGPU morphGridV = textureSize.y - morphGridV - 1; #endif return ivec2(morphGridU, morphGridV); } #ifdef MORPHING_POSITION #ifdef MORPHING_INT uniform vec3 aabbSize; uniform vec3 aabbMin; uniform usampler2D morphPositionTex; #else uniform highp sampler2D morphPositionTex; #endif #endif #endif #ifdef defined(BATCH) #include "skinBatchVS" mat4 getModelMatrix() { return getBoneMatrix(vertex_boneIndices); } #elif defined(SKIN) #include "skinVS" mat4 getModelMatrix() { return matrix_model * getSkinMatrix(vertex_boneIndices, vertex_boneWeights); } #elif defined(INSTANCING) #include "transformInstancingVS" #else mat4 getModelMatrix() { return matrix_model; } #endif vec3 getLocalPosition(vec3 vertexPosition) { vec3 localPos = vertexPosition; #ifdef MORPHING_POSITION ivec2 morphUV = getTextureMorphCoords(); #ifdef MORPHING_INT vec3 morphPos = vec3(texelFetch(morphPositionTex, ivec2(morphUV), 0).xyz) / 65535.0 * aabbSize + aabbMin; #else vec3 morphPos = texelFetch(morphPositionTex, ivec2(morphUV), 0).xyz; #endif localPos += morphPos; #endif return localPos; } `; var transformInstancingVS$1 = ` attribute vec4 instance_line1; attribute vec4 instance_line2; attribute vec4 instance_line3; attribute vec4 instance_line4; mat4 getModelMatrix() { return matrix_model * mat4(instance_line1, instance_line2, instance_line3, instance_line4); } `; var transmissionPS$1 = ` #ifdef STD_REFRACTION_CONSTANT uniform float material_refraction; #endif void getRefraction() { float refraction = 1.0; #ifdef STD_REFRACTION_CONSTANT refraction = material_refraction; #endif #ifdef STD_REFRACTION_TEXTURE refraction *= texture2DBias({STD_REFRACTION_TEXTURE_NAME}, {STD_REFRACTION_TEXTURE_UV}, textureBias).{STD_REFRACTION_TEXTURE_CHANNEL}; #endif #ifdef STD_REFRACTION_VERTEX refraction *= saturate(vVertexColor.{STD_REFRACTION_VERTEX_CHANNEL}); #endif dTransmission = refraction; } `; var twoSidedLightingPS$1 = ` uniform float twoSidedLightingNegScaleFactor; void handleTwoSidedLighting() { dTBN[2] *= gl_FrontFacing ? twoSidedLightingNegScaleFactor : -twoSidedLightingNegScaleFactor; } `; var uv0VS$1 = ` #ifdef NINESLICED vec2 getUv0() { vec2 uv = vertex_position.xz; vec2 positiveUnitOffset = clamp(vertex_position.xz, vec2(0.0), vec2(1.0)); vec2 negativeUnitOffset = clamp(-vertex_position.xz, vec2(0.0), vec2(1.0)); uv += (-positiveUnitOffset * innerOffset.xy + negativeUnitOffset * innerOffset.zw) * vertex_texCoord0.xy; uv = uv * -0.5 + 0.5; uv = uv * atlasRect.zw + atlasRect.xy; vMask = vertex_texCoord0.xy; return uv; } #else vec2 getUv0() { return vertex_texCoord0; } #endif `; var uv1VS$1 = ` vec2 getUv1() { return vertex_texCoord1; } `; var uvTransformVS$1 = ` vUV{TRANSFORM_UV_{i}}_{TRANSFORM_ID_{i}} = vec2( dot(vec3(uv{TRANSFORM_UV_{i}}, 1), {TRANSFORM_NAME_{i}}0), dot(vec3(uv{TRANSFORM_UV_{i}}, 1), {TRANSFORM_NAME_{i}}1) ); `; var uvTransformUniformsPS$1 = ` uniform vec3 {TRANSFORM_NAME_{i}}0; uniform vec3 {TRANSFORM_NAME_{i}}1; `; var viewDirPS$1 = ` void getViewDir() { dViewDirW = normalize(view_position - vPositionW); } `; const shaderChunksGLSL = { alphaTestPS: alphaTestPS$1, ambientPS: ambientPS$1, anisotropyPS: anisotropyPS$1, aoPS: aoPS$1, aoDiffuseOccPS: aoDiffuseOccPS$1, aoSpecOccPS: aoSpecOccPS$1, bakeDirLmEndPS: bakeDirLmEndPS$1, bakeLmEndPS: bakeLmEndPS$1, basePS: basePS$1, baseNineSlicedPS: baseNineSlicedPS$1, baseNineSlicedTiledPS: baseNineSlicedTiledPS$1, bayerPS: bayerPS$1, blurVSMPS: blurVSMPS$1, clearCoatPS: clearCoatPS$1, clearCoatGlossPS: clearCoatGlossPS$1, clearCoatNormalPS: clearCoatNormalPS$1, clusteredLightCookiesPS: clusteredLightCookiesPS$1, clusteredLightShadowsPS: clusteredLightShadowsPS$1, clusteredLightUtilsPS: clusteredLightUtilsPS$1, clusteredLightPS: clusteredLightPS$1, combinePS: combinePS$1, cookieBlit2DPS: cookieBlit2DPS$1, cookieBlitCubePS: cookieBlitCubePS$1, cookieBlitVS: cookieBlitVS$1, cookiePS, cubeMapProjectPS: cubeMapProjectPS$1, cubeMapRotatePS: cubeMapRotatePS$1, debugOutputPS: debugOutputPS$1, debugProcessFrontendPS: debugProcessFrontendPS$1, detailModesPS: detailModesPS$1, diffusePS: diffusePS$1, decodePS: decodePS$1, emissivePS: emissivePS$1, encodePS: encodePS$1, endPS: endPS$1, envAtlasPS: envAtlasPS$1, envProcPS: envProcPS$1, falloffInvSquaredPS: falloffInvSquaredPS$1, falloffLinearPS: falloffLinearPS$1, floatAsUintPS: floatAsUintPS$1, fogPS: fogPS$1, fresnelSchlickPS: fresnelSchlickPS$1, frontendCodePS: '', frontendDeclPS: '', fullscreenQuadVS: fullscreenQuadVS$1, gammaPS: gammaPS$1, gles3PS, gles3VS, glossPS: glossPS$1, quadVS: quadVS$1, immediateLinePS: immediateLinePS$1, immediateLineVS: immediateLineVS$1, iridescenceDiffractionPS: iridescenceDiffractionPS$1, iridescencePS: iridescencePS$1, iridescenceThicknessPS: iridescenceThicknessPS$1, iorPS: iorPS$1, lightDeclarationPS: lightDeclarationPS$1, lightDiffuseLambertPS: lightDiffuseLambertPS$1, lightDirPointPS: lightDirPointPS$1, lightEvaluationPS: lightEvaluationPS$1, lightFunctionLightPS: lightFunctionLightPS$1, lightFunctionShadowPS: lightFunctionShadowPS$1, lightingPS: lightingPS$1, lightmapAddPS: lightmapAddPS$1, lightmapPS: lightmapPS$1, lightSpecularAnisoGGXPS: lightSpecularAnisoGGXPS$1, lightSpecularGGXPS: lightSpecularGGXPS$1, lightSpecularBlinnPS: lightSpecularBlinnPS$1, lightSheenPS: lightSheenPS$1, linearizeDepthPS: linearizeDepthPS$1, litForwardBackendPS: litForwardBackendPS$1, litForwardDeclarationPS: litForwardDeclarationPS$1, litForwardMainPS: litForwardMainPS$1, litForwardPostCodePS: litForwardPostCodePS$1, litForwardPreCodePS: litForwardPreCodePS$1, litMainPS: litMainPS$1, litMainVS: litMainVS$1, litOtherMainPS: litOtherMainPS$1, litShaderArgsPS: litShaderArgsPS$1, litShaderCorePS: litShaderCorePS$1, litShadowMainPS: litShadowMainPS$1, litUserDeclarationPS: '', litUserDeclarationVS: '', litUserCodePS: '', litUserCodeVS: '', litUserMainStartPS: '', litUserMainStartVS: '', litUserMainEndPS: '', litUserMainEndVS: '', ltcPS: ltcPS$1, metalnessPS: metalnessPS$1, metalnessModulatePS: metalnessModulatePS$1, morphPS: morphPS$1, morphVS: morphVS$1, msdfPS: msdfPS$1, msdfVS: msdfVS$1, normalVS: normalVS$1, normalCoreVS: normalCoreVS$1, normalMapPS: normalMapPS$1, opacityPS: opacityPS$1, opacityDitherPS: opacityDitherPS$1, outputPS: outputPS$1, outputAlphaPS: outputAlphaPS$1, outputTex2DPS: outputTex2DPS$1, sheenPS: sheenPS$1, sheenGlossPS: sheenGlossPS$1, parallaxPS: parallaxPS$1, pickPS: pickPS$1, reflDirPS: reflDirPS$1, reflDirAnisoPS: reflDirAnisoPS$1, reflectionCCPS: reflectionCCPS$1, reflectionCubePS: reflectionCubePS$1, reflectionEnvHQPS: reflectionEnvHQPS$1, reflectionEnvPS: reflectionEnvPS$1, reflectionSpherePS: reflectionSpherePS$1, reflectionSheenPS: reflectionSheenPS$1, refractionCubePS: refractionCubePS$1, refractionDynamicPS: refractionDynamicPS$1, reprojectPS: reprojectPS$1, reprojectVS: reprojectVS$1, screenDepthPS: screenDepthPS$1, shadowCascadesPS: shadowCascadesPS$1, shadowEVSMPS: shadowEVSMPS$1, shadowPCF1PS: shadowPCF1PS$1, shadowPCF3PS: shadowPCF3PS$1, shadowPCF5PS: shadowPCF5PS$1, shadowPCSSPS, shadowSoftPS: shadowSoftPS$1, skinBatchVS: skinBatchVS$1, skinVS: skinVS$1, skyboxPS: skyboxPS$1, skyboxVS: skyboxVS$1, specularPS: specularPS$1, sphericalPS: sphericalPS$1, specularityFactorPS: specularityFactorPS$1, spotPS: spotPS$1, startNineSlicedPS: startNineSlicedPS$1, startNineSlicedTiledPS: startNineSlicedTiledPS$1, stdDeclarationPS: stdDeclarationPS$1, stdFrontEndPS: stdFrontEndPS$1, TBNPS: TBNPS$1, thicknessPS: thicknessPS$1, tonemappingPS: tonemappingPS$1, tonemappingAcesPS: tonemappingAcesPS$1, tonemappingAces2PS: tonemappingAces2PS$1, tonemappingFilmicPS: tonemappingFilmicPS$1, tonemappingHejlPS: tonemappingHejlPS$1, tonemappingLinearPS: tonemappingLinearPS$1, tonemappingNeutralPS: tonemappingNeutralPS$1, tonemappingNonePS: tonemappingNonePS$1, transformVS: transformVS$1, transformCoreVS: transformCoreVS$1, transformInstancingVS: transformInstancingVS$1, transmissionPS: transmissionPS$1, twoSidedLightingPS: twoSidedLightingPS$1, uv0VS: uv0VS$1, uv1VS: uv1VS$1, uvTransformVS: uvTransformVS$1, uvTransformUniformsPS: uvTransformUniformsPS$1, viewDirPS: viewDirPS$1, webgpuPS: webgpuPS$1, webgpuVS: webgpuVS$1 }; var alphaTestPS = ` uniform alpha_ref: f32; fn alphaTest(a: f32) { if (a < uniform.alpha_ref) { discard; } } `; var ambientPS = ` #if LIT_AMBIENT_SOURCE == AMBIENTSH uniform ambientSH: array; #endif #if LIT_AMBIENT_SOURCE == ENVALATLAS #include "envAtlasPS" #ifndef ENV_ATLAS #define ENV_ATLAS var texture_envAtlas: texture_2d; var texture_envAtlasSampler: sampler; #endif #endif fn addAmbient(worldNormal: vec3f) { #ifdef LIT_AMBIENT_SOURCE == AMBIENTSH let n: vec3f = cubeMapRotate(worldNormal); let color: vec3f = uniform.ambientSH[0] + uniform.ambientSH[1] * n.x + uniform.ambientSH[2] * n.y + uniform.ambientSH[3] * n.z + uniform.ambientSH[4] * n.x * n.z + uniform.ambientSH[5] * n.z * n.y + uniform.ambientSH[6] * n.y * n.x + uniform.ambientSH[7] * (3.0 * n.z * n.z - 1.0) + uniform.ambientSH[8] * (n.x * n.x - n.y * n.y); dDiffuseLight += processEnvironment(max(color, vec3f(0.0))); #endif #if LIT_AMBIENT_SOURCE == ENVALATLAS let dir: vec3f = normalize(cubeMapRotate(worldNormal) * vec3f(-1.0, 1.0, 1.0)); let uv: vec2f = mapUv(toSphericalUv(dir), vec4f(128.0, 256.0 + 128.0, 64.0, 32.0) / atlasSize); let raw: vec4f = textureSample(texture_envAtlas, texture_envAtlasSampler, uv); let linear: vec3f = {ambientDecode}(raw); dDiffuseLight += processEnvironment(linear); #endif #if LIT_AMBIENT_SOURCE == CONSTANT dDiffuseLight += uniform.light_globalAmbient; #endif } `; var anisotropyPS = ` #ifdef LIT_GGX_SPECULAR uniform material_anisotropyIntensity: f32; uniform material_anisotropyRotation: vec2f; #endif fn getAnisotropy() { dAnisotropy = 0.0; dAnisotropyRotation = vec2f(1.0, 0.0); #ifdef LIT_GGX_SPECULAR dAnisotropy = uniform.material_anisotropyIntensity; dAnisotropyRotation = uniform.material_anisotropyRotation; #endif #ifdef STD_ANISOTROPY_TEXTURE let anisotropyTex: vec3f = textureSampleBias({STD_ANISOTROPY_TEXTURE_NAME}, {STD_ANISOTROPY_TEXTURE_NAME}Sampler, {STD_ANISOTROPY_TEXTURE_UV}, uniform.textureBias).rgb; dAnisotropy *= anisotropyTex.b; let anisotropyRotationFromTex: vec2f = anisotropyTex.rg * 2.0 - vec2f(1.0); let rotationMatrix: mat2x2f = mat2x2f(dAnisotropyRotation.x, dAnisotropyRotation.y, -dAnisotropyRotation.y, dAnisotropyRotation.x); dAnisotropyRotation = rotationMatrix * anisotropyRotationFromTex; #endif dAnisotropy = clamp(dAnisotropy, 0.0, 1.0); } `; var aoPS = ` #if defined(STD_AO_TEXTURE) || defined(STD_AO_VERTEX) uniform material_aoIntensity: f32; #endif #ifdef STD_AODETAIL_TEXTURE #include "detailModesPS" #endif fn getAO() { dAo = 1.0; #ifdef STD_AO_TEXTURE var aoBase: f32 = textureSampleBias({STD_AO_TEXTURE_NAME}, {STD_AO_TEXTURE_NAME}Sampler, {STD_AO_TEXTURE_UV}, uniform.textureBias).{STD_AO_TEXTURE_CHANNEL}; #ifdef STD_AODETAIL_TEXTURE var aoDetail: f32 = textureSampleBias({STD_AODETAIL_TEXTURE_NAME}, {STD_AODETAIL_TEXTURE_NAME}Sampler, {STD_AODETAIL_TEXTURE_UV}, uniform.textureBias).{STD_AODETAIL_TEXTURE_CHANNEL}; aoBase = detailMode_{STD_AODETAIL_DETAILMODE}(vec3f(aoBase), vec3f(aoDetail)).r; #endif dAo = dAo * aoBase; #endif #ifdef STD_AO_VERTEX dAo = dAo * saturate(vVertexColor.{STD_AO_VERTEX_CHANNEL}); #endif #if defined(STD_AO_TEXTURE) || defined(STD_AO_VERTEX) dAo = mix(1.0, dAo, uniform.material_aoIntensity); #endif } `; var aoDiffuseOccPS = ` fn occludeDiffuse(ao: f32) { dDiffuseLight = dDiffuseLight * ao; } `; var aoSpecOccPS = ` #if LIT_OCCLUDE_SPECULAR != NONE #ifdef LIT_OCCLUDE_SPECULAR_FLOAT uniform material_occludeSpecularIntensity: f32; #endif #endif fn occludeSpecular(gloss: f32, ao: f32, worldNormal: vec3f, viewDir: vec3f) { #if LIT_OCCLUDE_SPECULAR == AO #ifdef LIT_OCCLUDE_SPECULAR_FLOAT var specOcc: f32 = mix(1.0, ao, uniform.material_occludeSpecularIntensity); #else var specOcc: f32 = ao; #endif #endif #if LIT_OCCLUDE_SPECULAR == GLOSSDEPENDENT var specPow: f32 = exp2(gloss * 11.0); var specOcc: f32 = saturate(pow(dot(worldNormal, viewDir) + ao, 0.01 * specPow) - 1.0 + ao); #ifdef LIT_OCCLUDE_SPECULAR_FLOAT specOcc = mix(1.0, specOcc, uniform.material_occludeSpecularIntensity); #endif #endif #if LIT_OCCLUDE_SPECULAR != NONE dSpecularLight = dSpecularLight * specOcc; dReflection = dReflection * specOcc; #ifdef LIT_SHEEN sSpecularLight = sSpecularLight * specOcc; sReflection = sReflection * specOcc; #endif #endif } `; var bakeDirLmEndPS = ` let dirLm = textureSample(texture_dirLightMap, texture_dirLightMapSampler, vUv1); if (uniform.bakeDir > 0.5) { if (dAtten > 0.00001) { let unpacked_dir = dirLm.xyz * 2.0 - vec3f(1.0); dAtten = clamp(dAtten, 0.0, 1.0); let combined_dir = dLightDirNormW.xyz * dAtten + unpacked_dir * dirLm.w; let finalRgb = normalize(combined_dir) * 0.5 + vec3f(0.5); let finalA = max(dirLm.w + dAtten, 1.0 / 255.0); output.color = vec4f(finalRgb, finalA); } else { output.color = dirLm; } } else { let alpha_min = select(0.0, 1.0 / 255.0, dAtten > 0.00001); let finalA = max(dirLm.w, alpha_min); output.color = vec4f(dirLm.rgb, finalA); } `; var bakeLmEndPS = ` #ifdef LIT_LIGHTMAP_BAKING_ADD_AMBIENT dDiffuseLight = ((dDiffuseLight - 0.5) * max(uniform.ambientBakeOcclusionContrast + 1.0, 0.0)) + 0.5; dDiffuseLight = dDiffuseLight + vec3f(uniform.ambientBakeOcclusionBrightness); dDiffuseLight = saturate3(dDiffuseLight); dDiffuseLight = dDiffuseLight * dAmbientLight; #endif #ifdef LIGHTMAP_RGBM var temp_color_rgbm = vec4f(dDiffuseLight, 1.0); temp_color_rgbm = vec4f(pow(temp_color_rgbm.rgb, vec3f(0.5)), temp_color_rgbm.a); temp_color_rgbm = vec4f(temp_color_rgbm.rgb / 8.0, temp_color_rgbm.a); let max_g_b = max(temp_color_rgbm.g, max(temp_color_rgbm.b, 1.0 / 255.0)); let max_rgb = max(temp_color_rgbm.r, max_g_b); temp_color_rgbm.a = clamp(max_rgb, 0.0, 1.0); temp_color_rgbm.a = ceil(temp_color_rgbm.a * 255.0) / 255.0; temp_color_rgbm = vec4f(temp_color_rgbm.rgb / temp_color_rgbm.a, temp_color_rgbm.a); output.color = temp_color_rgbm; #else output.color = vec4f(dDiffuseLight, 1.0); #endif `; var basePS = ` uniform view_position: vec3f; uniform light_globalAmbient: vec3f; fn square(x: f32) -> f32 { return x*x; } fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn saturate3(x: vec3f) -> vec3f { return clamp(x, vec3f(0.0), vec3f(1.0)); } `; var baseNineSlicedPS = ` #define NINESLICED varying vMask: vec2f; varying vTiledUv: vec2f; uniform innerOffset: vec4f; uniform outerScale: vec2f; uniform atlasRect: vec4f; var nineSlicedUv: vec2f; `; var baseNineSlicedTiledPS = ` #define NINESLICED #define NINESLICETILED varying vMask: vec2f; varying vTiledUv: vec2f; uniform innerOffset: vec4f; uniform outerScale: vec2f; uniform atlasRect: vec4f; var nineSlicedUv: vec2f; `; var bayerPS = ` fn bayer2(p: vec2f) -> f32 { return (2.0 * p.y + p.x + 1.0) % 4.0; } fn bayer4(p: vec2f) -> f32 { let p1: vec2f = p % vec2f(2.0); let p2: vec2f = floor(0.5 * (p % vec2f(4.0))); return 4.0 * bayer2(p1) + bayer2(p2); } fn bayer8(p: vec2f) -> f32 { let p1: vec2f = p % vec2f(2.0); let p2: vec2f = floor(0.5 * (p % vec2f(4.0))); let p4: vec2f = floor(0.25 * (p % vec2f(8.0))); return 4.0 * (4.0 * bayer2(p1) + bayer2(p2)) + bayer2(p4); } `; var blurVSMPS = ` varying vUv0: vec2f; var source: texture_2d; var sourceSampler: sampler; #ifdef GAUSS uniform weight: array; #endif uniform pixelOffset: vec2f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; var moments: vec3f = vec3f(0.0); let uv: vec2f = input.vUv0 - uniform.pixelOffset * (f32({SAMPLES}) * 0.5); for (var i: i32 = 0; i < {SAMPLES}; i = i + 1) { let c: vec4f = textureSample(source, sourceSampler, uv + uniform.pixelOffset * f32(i)); #ifdef GAUSS moments = moments + c.xyz * uniform.weight[i].element; #else moments = moments + c.xyz; #endif } #ifndef GAUSS moments = moments * (1.0 / f32({SAMPLES})); #endif output.color = vec4f(moments, 1.0); return output; } `; var clearCoatPS = ` uniform material_clearCoat: f32; fn getClearCoat() { ccSpecularity = uniform.material_clearCoat; #ifdef STD_CLEARCOAT_TEXTURE ccSpecularity = ccSpecularity * textureSampleBias({STD_CLEARCOAT_TEXTURE_NAME}, {STD_CLEARCOAT_TEXTURE_NAME}Sampler, {STD_CLEARCOAT_TEXTURE_UV}, uniform.textureBias).{STD_CLEARCOAT_TEXTURE_CHANNEL}; #endif #ifdef STD_CLEARCOAT_VERTEX ccSpecularity = ccSpecularity * saturate(vVertexColor.{STD_CLEARCOAT_VERTEX_CHANNEL}); #endif } `; var clearCoatGlossPS = ` uniform material_clearCoatGloss: f32; fn getClearCoatGlossiness() { ccGlossiness = uniform.material_clearCoatGloss; #ifdef STD_CLEARCOATGLOSS_TEXTURE ccGlossiness = ccGlossiness * textureSampleBias({STD_CLEARCOATGLOSS_TEXTURE_NAME}, {STD_CLEARCOATGLOSS_TEXTURE_NAME}Sampler, {STD_CLEARCOATGLOSS_TEXTURE_UV}, uniform.textureBias).{STD_CLEARCOATGLOSS_TEXTURE_CHANNEL}; #endif #ifdef STD_CLEARCOATGLOSS_VERTEX ccGlossiness = ccGlossiness * saturate(vVertexColor.{STD_CLEARCOATGLOSS_VERTEX_CHANNEL}); #endif #ifdef STD_CLEARCOATGLOSS_INVERT ccGlossiness = 1.0 - ccGlossiness; #endif ccGlossiness += 0.0000001; } `; var clearCoatNormalPS = ` #ifdef STD_CLEARCOATNORMAL_TEXTURE uniform material_clearCoatBumpiness: f32; #endif fn getClearCoatNormal() { #ifdef STD_CLEARCOATNORMAL_TEXTURE var normalMap: vec3f = {STD_CLEARCOATNORMAL_TEXTURE_DECODE}(textureSampleBias({STD_CLEARCOATNORMAL_TEXTURE_NAME}, {STD_CLEARCOATNORMAL_TEXTURE_NAME}Sampler, {STD_CLEARCOATNORMAL_TEXTURE_UV}, uniform.textureBias)); normalMap = mix(vec3f(0.0, 0.0, 1.0), normalMap, uniform.material_clearCoatBumpiness); ccNormalW = normalize(dTBN * normalMap); #else ccNormalW = dVertexNormalW; #endif } `; var clusteredLightUtilsPS = ` struct FaceCoords { uv: vec2f, faceIndex: f32, tileOffset: vec2f, } fn getCubemapFaceCoordinates(dir: vec3f) -> FaceCoords { var faceIndex: f32; var tileOffset: vec2f; var uv: vec2f; let vAbs: vec3f = abs(dir); var ma: f32; if (vAbs.z >= vAbs.x && vAbs.z >= vAbs.y) { let is_neg_z = dir.z < 0.0; faceIndex = select(4.0, 5.0, is_neg_z); ma = 0.5 / vAbs.z; uv = vec2f(select(dir.x, -dir.x, is_neg_z), -dir.y); tileOffset = vec2f(2.0, select(0.0, 1.0, is_neg_z)); } else if (vAbs.y >= vAbs.x) { let is_neg_y = dir.y < 0.0; faceIndex = select(2.0, 3.0, is_neg_y); ma = 0.5 / vAbs.y; uv = vec2f(dir.x, select(dir.z, -dir.z, is_neg_y)); tileOffset = vec2f(1.0, select(0.0, 1.0, is_neg_y)); } else { let is_neg_x = dir.x < 0.0; faceIndex = select(0.0, 1.0, is_neg_x); ma = 0.5 / vAbs.x; uv = vec2f(select(-dir.z, dir.z, is_neg_x), -dir.y); tileOffset = vec2f(0.0, select(0.0, 1.0, is_neg_x)); } uv = uv * ma + 0.5; return FaceCoords(uv, faceIndex, tileOffset); } fn getCubemapAtlasCoordinates(omniAtlasViewport: vec3f, shadowEdgePixels: f32, shadowTextureResolution: f32, dir: vec3f) -> vec2f { let faceData: FaceCoords = getCubemapFaceCoordinates(dir); var uv: vec2f = faceData.uv; let tileOffset: vec2f = faceData.tileOffset; let atlasFaceSize: f32 = omniAtlasViewport.z; let tileSize: f32 = shadowTextureResolution * atlasFaceSize; var offset: f32 = shadowEdgePixels / tileSize; uv = uv * (1.0 - offset * 2.0) + offset; uv = uv * atlasFaceSize; uv = uv + tileOffset * atlasFaceSize; uv = uv + omniAtlasViewport.xy; return uv; } `; var clusteredLightCookiesPS = ` fn _getCookieClustered(tex: texture_2d, texSampler: sampler, uv: vec2f, intensity: f32, cookieChannel: vec4f) -> vec3f { let pixel: vec4f = mix(vec4f(1.0), textureSampleLevel(tex, texSampler, uv, 0.0), intensity); let isRgb: bool = dot(cookieChannel.rgb, vec3f(1.0)) == 3.0; return select(vec3f(dot(pixel, cookieChannel)), pixel.rgb, isRgb); } fn getCookie2DClustered(tex: texture_2d, texSampler: sampler, transform: mat4x4f, worldPosition: vec3f, intensity: f32, cookieChannel: vec4f) -> vec3f { let projPos: vec4f = transform * vec4f(worldPosition, 1.0); return _getCookieClustered(tex, texSampler, projPos.xy / projPos.w, intensity, cookieChannel); } fn getCookieCubeClustered(tex: texture_2d, texSampler: sampler, dir: vec3f, intensity: f32, cookieChannel: vec4f, shadowTextureResolution: f32, shadowEdgePixels: f32, omniAtlasViewport: vec3f) -> vec3f { let uv: vec2f = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, dir); return _getCookieClustered(tex, texSampler, uv, intensity, cookieChannel); } `; var clusteredLightShadowsPS = ` fn _getShadowCoordPerspZbuffer(shadowMatrix: mat4x4f, shadowParams: vec4f, wPos: vec3f) -> vec3f { var projPos = shadowMatrix * vec4f(wPos, 1.0); return projPos.xyz / projPos.w; } fn getShadowCoordPerspZbufferNormalOffset(shadowMatrix: mat4x4f, shadowParams: vec4f, normal: vec3f) -> vec3f { let wPos: vec3f = vPositionW + normal * shadowParams.y; return _getShadowCoordPerspZbuffer(shadowMatrix, shadowParams, wPos); } fn normalOffsetPointShadow(shadowParams: vec4f, lightPos: vec3f, lightDir: vec3f, lightDirNorm: vec3f, normal: vec3f) -> vec3f { let distScale: f32 = length(lightDir); let wPos: vec3f = vPositionW + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; let dir: vec3f = wPos - lightPos; return dir; } #if defined(CLUSTER_SHADOW_TYPE_PCF1) fn getShadowOmniClusteredPCF1(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowParams: vec4f, omniAtlasViewport: vec3f, shadowEdgePixels: f32, lightDir: vec3f) -> f32 { let shadowTextureResolution: f32 = shadowParams.x; let uv: vec2f = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, lightDir); let shadowZ: f32 = length(lightDir) * shadowParams.w + shadowParams.z; return textureSampleCompareLevel(shadowMap, shadowMapSampler, uv, shadowZ); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF3) fn getShadowOmniClusteredPCF3(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowParams: vec4f, omniAtlasViewport: vec3f, shadowEdgePixels: f32, lightDir: vec3f) -> f32 { let shadowTextureResolution: f32 = shadowParams.x; let uv: vec2f = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, lightDir); let shadowZ: f32 = length(lightDir) * shadowParams.w + shadowParams.z; let shadowCoord: vec3f = vec3f(uv, shadowZ); return getShadowPCF3x3(shadowMap, shadowMapSampler, shadowCoord, shadowParams); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF5) fn getShadowOmniClusteredPCF5(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowParams: vec4f, omniAtlasViewport: vec3f, shadowEdgePixels: f32, lightDir: vec3f) -> f32 { let shadowTextureResolution: f32 = shadowParams.x; let uv: vec2f = getCubemapAtlasCoordinates(omniAtlasViewport, shadowEdgePixels, shadowTextureResolution, lightDir); let shadowZ: f32 = length(lightDir) * shadowParams.w + shadowParams.z; let shadowCoord: vec3f = vec3f(uv, shadowZ); return getShadowPCF5x5(shadowMap, shadowMapSampler, shadowCoord, shadowParams); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF1) fn getShadowSpotClusteredPCF1(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return textureSampleCompareLevel(shadowMap, shadowMapSampler, shadowCoord.xy, shadowCoord.z); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF3) fn getShadowSpotClusteredPCF3(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return getShadowSpotPCF3x3(shadowMap, shadowMapSampler, shadowCoord, shadowParams); } #endif #if defined(CLUSTER_SHADOW_TYPE_PCF5) fn getShadowSpotClusteredPCF5(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return getShadowPCF5x5(shadowMap, shadowMapSampler, shadowCoord, shadowParams); } #endif `; var clusteredLightPS = ` #include "lightBufferDefinesPS" #include "clusteredLightUtilsPS" #ifdef CLUSTER_COOKIES #include "clusteredLightCookiesPS" #endif #ifdef CLUSTER_SHADOWS #include "clusteredLightShadowsPS" #endif var clusterWorldTexture: texture_2d; var lightsTexture: texture_2d; #ifdef CLUSTER_SHADOWS var shadowAtlasTexture: texture_depth_2d; var shadowAtlasTextureSampler: sampler_comparison; #endif #ifdef CLUSTER_COOKIES var cookieAtlasTexture: texture_2d; var cookieAtlasTextureSampler: sampler; #endif uniform clusterMaxCells: i32; uniform numClusteredLights: i32; uniform clusterTextureWidth: i32; uniform clusterCellsCountByBoundsSize: vec3f; uniform clusterBoundsMin: vec3f; uniform clusterBoundsDelta: vec3f; uniform clusterCellsDot: vec3i; uniform clusterCellsMax: vec3i; uniform shadowAtlasParams: vec2f; struct ClusterLightData { flags: u32, halfWidth: vec3f, isSpot: bool, halfHeight: vec3f, lightIndex: i32, position: vec3f, shape: u32, direction: vec3f, falloffModeLinear: bool, color: vec3f, shadowIntensity: f32, omniAtlasViewport: vec3f, range: f32, cookieChannelMask: vec4f, biasesData: f32, colorBFlagsData: u32, shadowBias: f32, shadowNormalBias: f32, anglesData: f32, innerConeAngleCos: f32, outerConeAngleCos: f32, cookieIntensity: f32, isDynamic: bool, isLightmapped: bool } var lightProjectionMatrix: mat4x4f; fn sampleLightTextureF(lightIndex: i32, index: i32) -> vec4f { return textureLoad(lightsTexture, vec2(index, lightIndex), 0); } fn decodeClusterLightCore(clusterLightData: ptr, lightIndex: i32) { clusterLightData.lightIndex = lightIndex; let halfData: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_COLOR_ANGLES_BIAS}); clusterLightData.anglesData = halfData.z; clusterLightData.biasesData = halfData.w; clusterLightData.colorBFlagsData = bitcast(halfData.y); let colorRG: vec2f = unpack2x16float(bitcast(halfData.x)); let colorB_flags: vec2f = unpack2x16float(clusterLightData.colorBFlagsData); clusterLightData.color = vec3f(colorRG, colorB_flags.x) * {LIGHT_COLOR_DIVIDER}; let lightPosRange: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_POSITION_RANGE}); clusterLightData.position = lightPosRange.xyz; clusterLightData.range = lightPosRange.w; let lightDir_Flags: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_DIRECTION_FLAGS}); clusterLightData.direction = lightDir_Flags.xyz; let flags_uint: u32 = bitcast(lightDir_Flags.w); clusterLightData.flags = flags_uint; clusterLightData.isSpot = (flags_uint & (1u << 30u)) != 0u; clusterLightData.shape = (flags_uint >> 28u) & 0x3u; clusterLightData.falloffModeLinear = (flags_uint & (1u << 27u)) == 0u; clusterLightData.shadowIntensity = f32((flags_uint >> 0u) & 0xFFu) / 255.0; clusterLightData.cookieIntensity = f32((flags_uint >> 8u) & 0xFFu) / 255.0; clusterLightData.isDynamic = (flags_uint & (1u << 22u)) != 0u; clusterLightData.isLightmapped = (flags_uint & (1u << 21u)) != 0u; } fn decodeClusterLightSpot(clusterLightData: ptr) { let angleFlags: u32 = (clusterLightData.colorBFlagsData >> 16u) & 0xFFFFu; let angleValues: vec2f = unpack2x16float(bitcast(clusterLightData.anglesData)); let innerVal: f32 = angleValues.x; let outerVal: f32 = angleValues.y; let innerIsVersine: bool = (angleFlags & 1u) != 0u; let outerIsVersine: bool = ((angleFlags >> 1u) & 1u) != 0u; clusterLightData.innerConeAngleCos = select(innerVal, 1.0 - innerVal, innerIsVersine); clusterLightData.outerConeAngleCos = select(outerVal, 1.0 - outerVal, outerIsVersine); } fn decodeClusterLightOmniAtlasViewport(clusterLightData: ptr) { clusterLightData.omniAtlasViewport = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_0}).xyz; } fn decodeClusterLightAreaData(clusterLightData: ptr) { clusterLightData.halfWidth = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_AREA_DATA_WIDTH}).xyz; clusterLightData.halfHeight = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_AREA_DATA_HEIGHT}).xyz; } fn decodeClusterLightProjectionMatrixData(clusterLightData: ptr) { let m0: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_0}); let m1: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_1}); let m2: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_2}); let m3: vec4f = sampleLightTextureF(clusterLightData.lightIndex, {CLUSTER_TEXTURE_PROJ_MAT_3}); lightProjectionMatrix = mat4x4f(m0, m1, m2, m3); } fn decodeClusterLightShadowData(clusterLightData: ptr) { let biases: vec2f = unpack2x16float(bitcast(clusterLightData.biasesData)); clusterLightData.shadowBias = biases.x; clusterLightData.shadowNormalBias = biases.y; } fn decodeClusterLightCookieData(clusterLightData: ptr) { let cookieFlags: u32 = (clusterLightData.flags >> 23u) & 0x0Fu; let mask_uvec: vec4 = vec4(cookieFlags) & vec4(1u, 2u, 4u, 8u); clusterLightData.cookieChannelMask = step(vec4f(1.0), vec4f(mask_uvec)); } fn evaluateLight( light: ptr, worldNormal: vec3f, viewDir: vec3f, reflectionDir: vec3f, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir: vec3f, #endif gloss: f32, specularity: vec3f, geometricNormal: vec3f, tbn: mat3x3f, #if defined(LIT_IRIDESCENCE) iridescenceFresnel: vec3f, #endif clearcoat_worldNormal: vec3f, clearcoat_gloss: f32, sheen_gloss: f32, iridescence_intensity: f32 ) { var cookieAttenuation: vec3f = vec3f(1.0); var diffuseAttenuation: f32 = 1.0; var falloffAttenuation: f32 = 1.0; let lightDirW: vec3f = evalOmniLight(light.position); let lightDirNormW: vec3f = normalize(lightDirW); #ifdef CLUSTER_AREALIGHTS if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { decodeClusterLightAreaData(light); if (light.shape == {LIGHTSHAPE_RECT}) { calcRectLightValues(light.position, light.halfWidth, light.halfHeight); } else if (light.shape == {LIGHTSHAPE_DISK}) { calcDiskLightValues(light.position, light.halfWidth, light.halfHeight); } else { calcSphereLightValues(light.position, light.halfWidth, light.halfHeight); } falloffAttenuation = getFalloffWindow(light.range, lightDirW); } else #endif { if (light.falloffModeLinear) { falloffAttenuation = getFalloffLinear(light.range, lightDirW); } else { falloffAttenuation = getFalloffInvSquared(light.range, lightDirW); } } if (falloffAttenuation > 0.00001) { #ifdef CLUSTER_AREALIGHTS if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { if (light.shape == {LIGHTSHAPE_RECT}) { diffuseAttenuation = getRectLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0; } else if (light.shape == {LIGHTSHAPE_DISK}) { diffuseAttenuation = getDiskLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0; } else { diffuseAttenuation = getSphereLightDiffuse(worldNormal, viewDir, lightDirW, lightDirNormW) * 16.0; } } else #endif { falloffAttenuation = falloffAttenuation * getLightDiffuse(worldNormal, viewDir, lightDirNormW); } if (light.isSpot) { decodeClusterLightSpot(light); falloffAttenuation = falloffAttenuation * getSpotEffect(light.direction, light.innerConeAngleCos, light.outerConeAngleCos, lightDirNormW); } #if defined(CLUSTER_COOKIES) || defined(CLUSTER_SHADOWS) if (falloffAttenuation > 0.00001) { if (light.shadowIntensity > 0.0 || light.cookieIntensity > 0.0) { if (light.isSpot) { decodeClusterLightProjectionMatrixData(light); } else { decodeClusterLightOmniAtlasViewport(light); } let shadowTextureResolution: f32 = uniform.shadowAtlasParams.x; let shadowEdgePixels: f32 = uniform.shadowAtlasParams.y; #ifdef CLUSTER_COOKIES if (light.cookieIntensity > 0.0) { decodeClusterLightCookieData(light); if (light.isSpot) { cookieAttenuation = getCookie2DClustered(cookieAtlasTexture, cookieAtlasTextureSampler, lightProjectionMatrix, vPositionW, light.cookieIntensity, light.cookieChannelMask); } else { cookieAttenuation = getCookieCubeClustered(cookieAtlasTexture, cookieAtlasTextureSampler, lightDirW, light.cookieIntensity, light.cookieChannelMask, shadowTextureResolution, shadowEdgePixels, light.omniAtlasViewport); } } #endif #ifdef CLUSTER_SHADOWS if (light.shadowIntensity > 0.0) { decodeClusterLightShadowData(light); let shadowParams: vec4f = vec4f(shadowTextureResolution, light.shadowNormalBias, light.shadowBias, 1.0 / light.range); if (light.isSpot) { let shadowCoord: vec3f = getShadowCoordPerspZbufferNormalOffset(lightProjectionMatrix, shadowParams, geometricNormal); #if defined(CLUSTER_SHADOW_TYPE_PCF1) let shadow: f32 = getShadowSpotClusteredPCF1(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams); #elif defined(CLUSTER_SHADOW_TYPE_PCF3) let shadow: f32 = getShadowSpotClusteredPCF3(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams); #elif defined(CLUSTER_SHADOW_TYPE_PCF5) let shadow: f32 = getShadowSpotClusteredPCF5(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams); #elif defined(CLUSTER_SHADOW_TYPE_PCSS) let shadow: f32 = getShadowSpotClusteredPCSS(shadowAtlasTexture, shadowAtlasTextureSampler, shadowCoord, shadowParams); #endif falloffAttenuation = falloffAttenuation * mix(1.0, shadow, light.shadowIntensity); } else { let dir: vec3f = normalOffsetPointShadow(shadowParams, light.position, lightDirW, lightDirNormW, geometricNormal); #if defined(CLUSTER_SHADOW_TYPE_PCF1) let shadow: f32 = getShadowOmniClusteredPCF1(shadowAtlasTexture, shadowAtlasTextureSampler, shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir); #elif defined(CLUSTER_SHADOW_TYPE_PCF3) let shadow: f32 = getShadowOmniClusteredPCF3(shadowAtlasTexture, shadowAtlasTextureSampler, shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir); #elif defined(CLUSTER_SHADOW_TYPE_PCF5) let shadow: f32 = getShadowOmniClusteredPCF5(shadowAtlasTexture, shadowAtlasTextureSampler, shadowParams, light.omniAtlasViewport, shadowEdgePixels, dir); #endif falloffAttenuation = falloffAttenuation * mix(1.0, shadow, light.shadowIntensity); } } #endif } } #endif #ifdef CLUSTER_AREALIGHTS if (light.shape != {LIGHTSHAPE_PUNCTUAL}) { { var areaDiffuse: vec3f = (diffuseAttenuation * falloffAttenuation) * light.color * cookieAttenuation; #if defined(LIT_SPECULAR) areaDiffuse = mix(areaDiffuse, vec3f(0.0), dLTCSpecFres); #endif dDiffuseLight = dDiffuseLight + areaDiffuse; } #ifdef LIT_SPECULAR var areaLightSpecular: f32; if (light.shape == {LIGHTSHAPE_RECT}) { areaLightSpecular = getRectLightSpecular(worldNormal, viewDir); } else if (light.shape == {LIGHTSHAPE_DISK}) { areaLightSpecular = getDiskLightSpecular(worldNormal, viewDir); } else { areaLightSpecular = getSphereLightSpecular(worldNormal, viewDir); } dSpecularLight = dSpecularLight + dLTCSpecFres * areaLightSpecular * falloffAttenuation * light.color * cookieAttenuation; #ifdef LIT_CLEARCOAT var areaLightSpecularCC: f32; if (light.shape == {LIGHTSHAPE_RECT}) { areaLightSpecularCC = getRectLightSpecular(clearcoat_worldNormal, viewDir); } else if (light.shape == {LIGHTSHAPE_DISK}) { areaLightSpecularCC = getDiskLightSpecular(clearcoat_worldNormal, viewDir); } else { areaLightSpecularCC = getSphereLightSpecular(clearcoat_worldNormal, viewDir); } ccSpecularLight = ccSpecularLight + ccLTCSpecFres * areaLightSpecularCC * falloffAttenuation * light.color * cookieAttenuation; #endif #endif } else #endif { { var punctualDiffuse: vec3f = falloffAttenuation * light.color * cookieAttenuation; #if defined(CLUSTER_AREALIGHTS) #if defined(LIT_SPECULAR) punctualDiffuse = mix(punctualDiffuse, vec3f(0.0), specularity); #endif #endif dDiffuseLight = dDiffuseLight + punctualDiffuse; } #ifdef LIT_SPECULAR let halfDir: vec3f = normalize(-lightDirNormW + viewDir); #ifdef LIT_SPECULAR_FRESNEL dSpecularLight = dSpecularLight + getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * getFresnel( dot(viewDir, halfDir), gloss, specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, iridescence_intensity #endif ); #else dSpecularLight = dSpecularLight + getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, lightDirNormW, gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * specularity; #endif #ifdef LIT_CLEARCOAT #ifdef LIT_SPECULAR_FRESNEL ccSpecularLight = ccSpecularLight + getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation * getFresnelCC(dot(viewDir, halfDir)); #else ccSpecularLight = ccSpecularLight + getLightSpecular(halfDir, clearcoatReflectionDir, clearcoat_worldNormal, viewDir, lightDirNormW, clearcoat_gloss, tbn) * falloffAttenuation * light.color * cookieAttenuation; #endif #endif #ifdef LIT_SHEEN sSpecularLight = sSpecularLight + getLightSpecularSheen(halfDir, worldNormal, viewDir, lightDirNormW, sheen_gloss) * falloffAttenuation * light.color * cookieAttenuation; #endif #endif } } dAtten = falloffAttenuation; dLightDirNormW = lightDirNormW; } fn evaluateClusterLight( lightIndex: i32, worldNormal: vec3f, viewDir: vec3f, reflectionDir: vec3f, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir: vec3f, #endif gloss: f32, specularity: vec3f, geometricNormal: vec3f, tbn: mat3x3f, #if defined(LIT_IRIDESCENCE) iridescenceFresnel: vec3f, #endif clearcoat_worldNormal: vec3f, clearcoat_gloss: f32, sheen_gloss: f32, iridescence_intensity: f32 ) { var clusterLightData: ClusterLightData; decodeClusterLightCore(&clusterLightData, lightIndex); #ifdef CLUSTER_MESH_DYNAMIC_LIGHTS let acceptLightMask: bool = clusterLightData.isDynamic; #else let acceptLightMask: bool = clusterLightData.isLightmapped; #endif if (acceptLightMask) { evaluateLight( &clusterLightData, worldNormal, viewDir, reflectionDir, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir, #endif gloss, specularity, geometricNormal, tbn, #if defined(LIT_IRIDESCENCE) iridescenceFresnel, #endif clearcoat_worldNormal, clearcoat_gloss, sheen_gloss, iridescence_intensity ); } } fn addClusteredLights( worldNormal: vec3f, viewDir: vec3f, reflectionDir: vec3f, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir: vec3f, #endif gloss: f32, specularity: vec3f, geometricNormal: vec3f, tbn: mat3x3f, #if defined(LIT_IRIDESCENCE) iridescenceFresnel: vec3f, #endif clearcoat_worldNormal: vec3f, clearcoat_gloss: f32, sheen_gloss: f32, iridescence_intensity: f32 ) { if (uniform.numClusteredLights <= 1) { return; } let cellCoords: vec3i = vec3i(floor((vPositionW - uniform.clusterBoundsMin) * uniform.clusterCellsCountByBoundsSize)); if (!(any(cellCoords < vec3i(0)) || any(cellCoords >= uniform.clusterCellsMax))) { let cellIndex: i32 = cellCoords.x * uniform.clusterCellsDot.x + cellCoords.y * uniform.clusterCellsDot.y + cellCoords.z * uniform.clusterCellsDot.z; let clusterV: i32 = cellIndex / uniform.clusterTextureWidth; let clusterU: i32 = cellIndex - clusterV * uniform.clusterTextureWidth; for (var lightCellIndex: i32 = 0; lightCellIndex < uniform.clusterMaxCells; lightCellIndex = lightCellIndex + 1) { let lightIndex: u32 = textureLoad(clusterWorldTexture, vec2(clusterU + lightCellIndex, clusterV), 0).r; if (lightIndex == 0u) { break; } evaluateClusterLight( i32(lightIndex), worldNormal, viewDir, reflectionDir, #if defined(LIT_CLEARCOAT) clearcoatReflectionDir, #endif gloss, specularity, geometricNormal, tbn, #if defined(LIT_IRIDESCENCE) iridescenceFresnel, #endif clearcoat_worldNormal, clearcoat_gloss, sheen_gloss, iridescence_intensity ); } } }`; var combinePS = ` fn combineColor(albedo: vec3f, sheenSpecularity: vec3f, clearcoatSpecularity: f32) -> vec3f { var ret: vec3f = vec3f(0.0); #ifdef LIT_OLD_AMBIENT ret = ret + ((dDiffuseLight - uniform.light_globalAmbient) * albedo + uniform.material_ambient * uniform.light_globalAmbient); #else ret = ret + (albedo * dDiffuseLight); #endif #ifdef LIT_SPECULAR ret = ret + dSpecularLight; #endif #ifdef LIT_REFLECTIONS ret = ret + (dReflection.rgb * dReflection.a); #endif #ifdef LIT_SHEEN let sheenScaling: f32 = 1.0 - max(max(sheenSpecularity.r, sheenSpecularity.g), sheenSpecularity.b) * 0.157; ret = ret * sheenScaling + (sSpecularLight + sReflection.rgb) * sheenSpecularity; #endif #ifdef LIT_CLEARCOAT let clearCoatScaling: f32 = 1.0 - ccFresnel * clearcoatSpecularity; ret = ret * clearCoatScaling + (ccSpecularLight + ccReflection) * clearcoatSpecularity; #endif return ret; } `; var cookieBlit2DPS = ` varying uv0: vec2f; var blitTexture: texture_2d; var blitTextureSampler : sampler; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; output.color = textureSample(blitTexture, blitTextureSampler, input.uv0); return output; } `; var cookieBlitCubePS = ` varying uv0: vec2f; uniform invViewProj: mat4x4; var blitTexture: texture_cube; var blitTextureSampler : sampler; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; var projPos = vec4f(input.uv0 * 2.0 - 1.0, 0.5, 1.0); var worldPos = uniform.invViewProj * projPos; output.color = textureSample(blitTexture, blitTextureSampler, worldPos.xyz); return output; } `; var cookieBlitVS = ` attribute vertex_position: vec2f; varying uv0: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(input.vertex_position, 0.5, 1.0); output.uv0 = input.vertex_position * 0.5 + vec2f(0.5, 0.5); output.uv0.y = 1.0 - output.uv0.y; return output; } `; var cubeMapProjectPS = ` #if LIT_CUBEMAP_PROJECTION == BOX uniform envBoxMin: vec3f; uniform envBoxMax: vec3f; #endif fn cubeMapProject(nrdir: vec3f) -> vec3f { #if LIT_CUBEMAP_PROJECTION == NONE return cubeMapRotate(nrdir); #endif #if LIT_CUBEMAP_PROJECTION == BOX let nrdir_rotated: vec3f = cubeMapRotate(nrdir); let rbmax: vec3f = (uniform.envBoxMax - vPositionW) / nrdir_rotated; let rbmin: vec3f = (uniform.envBoxMin - vPositionW) / nrdir_rotated; let rbminmax: vec3f = select(rbmin, rbmax, nrdir_rotated > vec3f(0.0)); let fa: f32 = min(min(rbminmax.x, rbminmax.y), rbminmax.z); let posonbox: vec3f = vPositionW + nrdir_rotated * fa; let envBoxPos: vec3f = (uniform.envBoxMin + uniform.envBoxMax) * 0.5; return normalize(posonbox - envBoxPos); #endif } `; var cubeMapRotatePS = ` #ifdef CUBEMAP_ROTATION uniform cubeMapRotationMatrix: mat3x3f; #endif fn cubeMapRotate(refDir: vec3f) -> vec3f { #ifdef CUBEMAP_ROTATION return refDir * uniform.cubeMapRotationMatrix; #else return refDir; #endif } `; var debugOutputPS = ` #ifdef DEBUG_ALBEDO_PASS output.color = vec4(gammaCorrectOutput(dAlbedo), 1.0); #endif #ifdef DEBUG_UV0_PASS output.color = vec4f(litArgs_albedo , 1.0); #endif #ifdef DEBUG_WORLD_NORMAL_PASS output.color = vec4f(litArgs_worldNormal * 0.5 + 0.5, 1.0); #endif #ifdef DEBUG_OPACITY_PASS output.color = vec4f(vec3f(litArgs_opacity) , 1.0); #endif #ifdef DEBUG_SPECULARITY_PASS output.color = vec4f(litArgs_specularity, 1.0); #endif #ifdef DEBUG_GLOSS_PASS output.color = vec4f(vec3f(litArgs_gloss) , 1.0); #endif #ifdef DEBUG_METALNESS_PASS output.color = vec4f(vec3f(litArgs_metalness) , 1.0); #endif #ifdef DEBUG_AO_PASS output.color = vec4f(vec3f(litArgs_ao) , 1.0); #endif #ifdef DEBUG_EMISSION_PASS output.color = vec4f(gammaCorrectOutput(litArgs_emission), 1.0); #endif `; var debugProcessFrontendPS = ` #ifdef DEBUG_LIGHTING_PASS litArgs_albedo = vec3f(0.5); #endif #ifdef DEBUG_UV0_PASS #ifdef VARYING_VUV0 litArgs_albedo = vec3f(vUv0, 0.0); #else litArgs_albedo = vec3f(0.0); #endif #endif `; var decodePS = ` #ifndef _DECODE_INCLUDED_ #define _DECODE_INCLUDED_ fn decodeLinear(raw: vec4f) -> vec3f { return raw.rgb; } fn decodeGammaFloat(raw: f32) -> f32 { return pow(raw, 2.2); } fn decodeGamma3(raw: vec3f) -> vec3f { return pow(raw, vec3f(2.2)); } fn decodeGamma(raw: vec4f) -> vec3f { return pow(raw.xyz, vec3f(2.2)); } fn decodeRGBM(raw: vec4f) -> vec3f { let color = (8.0 * raw.a) * raw.rgb; return color * color; } fn decodeRGBP(raw: vec4f) -> vec3f { let color = raw.rgb * (-raw.a * 7.0 + 8.0); return color * color; } fn decodeRGBE(raw: vec4f) -> vec3f { return select(vec3f(0.0), raw.xyz * pow(2.0, raw.w * 255.0 - 128.0), raw.a != 0.0); } fn passThrough(raw: vec4f) -> vec4f { return raw; } fn unpackNormalXYZ(nmap: vec4f) -> vec3f { return nmap.xyz * 2.0 - 1.0; } fn unpackNormalXY(nmap: vec4f) -> vec3f { var xy = nmap.wy * 2.0 - 1.0; return vec3f(xy, sqrt(1.0 - clamp(dot(xy, xy), 0.0, 1.0))); } #endif `; var detailModesPS = ` #ifndef _DETAILMODES_INCLUDED_ #define _DETAILMODES_INCLUDED_ fn detailMode_mul(c1: vec3f, c2: vec3f) -> vec3f { return c1 * c2; } fn detailMode_add(c1: vec3f, c2: vec3f) -> vec3f { return c1 + c2; } fn detailMode_screen(c1: vec3f, c2: vec3f) -> vec3f { return 1.0 - (1.0 - c1)*(1.0 - c2); } fn detailMode_overlay(c1: vec3f, c2: vec3f) -> vec3f { return mix(1.0 - 2.0 * (1.0 - c1)*(1.0 - c2), 2.0 * c1 * c2, step(c1, vec3f(0.5))); } fn detailMode_min(c1: vec3f, c2: vec3f) -> vec3f { return min(c1, c2); } fn detailMode_max(c1: vec3f, c2: vec3f) -> vec3f { return max(c1, c2); } #endif `; var diffusePS = ` uniform material_diffuse: vec3f; #ifdef STD_DIFFUSEDETAIL_TEXTURE #include "detailModesPS" #endif fn getAlbedo() { dAlbedo = uniform.material_diffuse.rgb; #ifdef STD_DIFFUSE_TEXTURE var albedoTexture: vec3f = {STD_DIFFUSE_TEXTURE_DECODE}(textureSampleBias({STD_DIFFUSE_TEXTURE_NAME}, {STD_DIFFUSE_TEXTURE_NAME}Sampler, {STD_DIFFUSE_TEXTURE_UV}, uniform.textureBias)).{STD_DIFFUSE_TEXTURE_CHANNEL}; #ifdef STD_DIFFUSEDETAIL_TEXTURE var albedoDetail: vec3f = {STD_DIFFUSEDETAIL_TEXTURE_DECODE}(textureSampleBias({STD_DIFFUSEDETAIL_TEXTURE_NAME}, {STD_DIFFUSEDETAIL_TEXTURE_NAME}Sampler, {STD_DIFFUSEDETAIL_TEXTURE_UV}, uniform.textureBias)).{STD_DIFFUSEDETAIL_TEXTURE_CHANNEL}; albedoTexture = detailMode_{STD_DIFFUSEDETAIL_DETAILMODE}(albedoTexture, albedoDetail); #endif dAlbedo = dAlbedo * albedoTexture; #endif #ifdef STD_DIFFUSE_VERTEX dAlbedo = dAlbedo * saturate3(vVertexColor.{STD_DIFFUSE_VERTEX_CHANNEL}); #endif } `; var emissivePS = ` uniform material_emissive: vec3f; uniform material_emissiveIntensity: f32; fn getEmission() { dEmission = uniform.material_emissive * uniform.material_emissiveIntensity; #ifdef STD_EMISSIVE_TEXTURE dEmission *= {STD_EMISSIVE_TEXTURE_DECODE}(textureSampleBias({STD_EMISSIVE_TEXTURE_NAME}, {STD_EMISSIVE_TEXTURE_NAME}Sampler, {STD_EMISSIVE_TEXTURE_UV}, uniform.textureBias)).{STD_EMISSIVE_TEXTURE_CHANNEL}; #endif #ifdef STD_EMISSIVE_VERTEX dEmission = dEmission * saturate3(vVertexColor.{STD_EMISSIVE_VERTEX_CHANNEL}); #endif } `; var encodePS = ` fn encodeLinear(source: vec3f) -> vec4f { return vec4f(source, 1.0); } fn encodeGamma(source: vec3f) -> vec4f { return vec4f(pow(source + vec3f(0.0000001), vec3f(1.0 / 2.2)), 1.0); } fn encodeRGBM(source: vec3f) -> vec4f { var color: vec3f = pow(source, vec3f(0.5)); color *= 1.0 / 8.0; var a: f32 = saturate(max(max(color.r, color.g), max(color.b, 1.0 / 255.0))); a = ceil(a * 255.0) / 255.0; color /= a; return vec4f(color, a); } fn encodeRGBP(source: vec3f) -> vec4f { var gamma: vec3f = pow(source, vec3f(0.5)); var maxVal: f32 = min(8.0, max(1.0, max(gamma.x, max(gamma.y, gamma.z)))); var v: f32 = 1.0 - ((maxVal - 1.0) / 7.0); v = ceil(v * 255.0) / 255.0; return vec4f(gamma / (-v * 7.0 + 8.0), v); } fn encodeRGBE(source: vec3f) -> vec4f { var maxVal: f32 = max(source.x, max(source.y, source.z)); if (maxVal < 1e-32) { return vec4f(0.0, 0.0, 0.0, 0.0); } else { var e: f32 = ceil(log2(maxVal)); return vec4f(source / pow(2.0, e), (e + 128.0) / 255.0); } } `; var endPS = ` var finalRgb: vec3f = combineColor(litArgs_albedo, litArgs_sheen_specularity, litArgs_clearcoat_specularity); finalRgb = finalRgb + litArgs_emission; finalRgb = addFog(finalRgb); finalRgb = toneMap(finalRgb); finalRgb = gammaCorrectOutput(finalRgb); output.color = vec4f(finalRgb, output.color.a); `; var envAtlasPS = ` #ifndef _ENVATLAS_INCLUDED_ #define _ENVATLAS_INCLUDED_ const atlasSize : f32 = 512.0; const seamSize : f32 = 1.0 / atlasSize; fn mapUv(uv : vec2f, rect : vec4f) -> vec2f { return vec2f(mix(rect.x + seamSize, rect.x + rect.z - seamSize, uv.x), mix(rect.y + seamSize, rect.y + rect.w - seamSize, uv.y)); } fn mapRoughnessUv(uv : vec2f, level : f32) -> vec2f { let t : f32 = 1.0 / exp2(level); return mapUv(uv, vec4f(0.0, 1.0 - t, t, t * 0.5)); } fn mapShinyUv(uv : vec2f, level : f32) -> vec2f { let t : f32 = 1.0 / exp2(level); return mapUv(uv, vec4f(1.0 - t, 1.0 - t, t, t * 0.5)); } #endif `; var envProcPS = ` #ifdef LIT_SKYBOX_INTENSITY uniform skyboxIntensity : f32; #endif fn processEnvironment(color : vec3f) -> vec3f { #ifdef LIT_SKYBOX_INTENSITY return color * uniform.skyboxIntensity; #else return color; #endif } `; var falloffInvSquaredPS = ` fn getFalloffWindow(lightRadius: f32, lightDir: vec3f) -> f32 { let sqrDist: f32 = dot(lightDir, lightDir); let invRadius: f32 = 1.0 / lightRadius; return square(saturate(1.0 - square(sqrDist * square(invRadius)))); } fn getFalloffInvSquared(lightRadius: f32, lightDir: vec3f) -> f32 { let sqrDist: f32 = dot(lightDir, lightDir); var falloff: f32 = 1.0 / (sqrDist + 1.0); let invRadius: f32 = 1.0 / lightRadius; falloff = falloff * 16.0; falloff = falloff * square(saturate(1.0 - square(sqrDist * square(invRadius)))); return falloff; } `; var falloffLinearPS = ` fn getFalloffLinear(lightRadius: f32, lightDir: vec3f) -> f32 { let d: f32 = length(lightDir); return max(((lightRadius - d) / lightRadius), 0.0); } `; var floatAsUintPS = ` #ifndef FLOAT_AS_UINT #define FLOAT_AS_UINT fn float2uint(value: f32) -> vec4f { let intBits = bitcast(value); return vec4f( f32((intBits >> 24u) & 0xffu), f32((intBits >> 16u) & 0xffu), f32((intBits >> 8u) & 0xffu), f32(intBits & 0xffu) ) / 255.0; } fn uint2float(value: vec4f) -> f32 { let rgba_u32 = vec4(value * 255.0); let intBits: u32 = (rgba_u32.r << 24u) | (rgba_u32.g << 16u) | (rgba_u32.b << 8u) | rgba_u32.a; return bitcast(intBits); } fn float2vec4(value: f32) -> vec4f { #if defined(CAPS_TEXTURE_FLOAT_RENDERABLE) return vec4f(value, 1.0, 1.0, 1.0); #else return float2uint(value); #endif } #endif `; var fogPS = ` var dBlendModeFogFactor : f32 = 1.0; #if (FOG != NONE) uniform fog_color : vec3f; #if (FOG == LINEAR) uniform fog_start : f32; uniform fog_end : f32; #else uniform fog_density : f32; #endif #endif fn getFogFactor() -> f32 { let depth = pcPosition.z / pcPosition.w; var fogFactor : f32 = 0.0; #if (FOG == LINEAR) fogFactor = (uniform.fog_end - depth) / (uniform.fog_end - uniform.fog_start); #elif (FOG == EXP) fogFactor = exp(-depth * uniform.fog_density); #elif (FOG == EXP2) fogFactor = exp(-depth * depth * uniform.fog_density * uniform.fog_density); #endif return clamp(fogFactor, 0.0, 1.0); } fn addFog(color : vec3f) -> vec3f { #if (FOG != NONE) return mix(uniform.fog_color * dBlendModeFogFactor, color, getFogFactor()); #else return color; #endif } `; var fresnelSchlickPS = ` fn getFresnel( cosTheta: f32, gloss: f32, specularity: vec3f #if defined(LIT_IRIDESCENCE) , iridescenceFresnel: vec3f, iridescenceIntensity: f32 #endif ) -> vec3f { let fresnel: f32 = pow(1.0 - saturate(cosTheta), 5.0); let glossSq: f32 = gloss * gloss; let specIntensity: f32 = max(specularity.r, max(specularity.g, specularity.b)); let ret: vec3f = specularity + (max(vec3f(glossSq * specIntensity), specularity) - specularity) * fresnel; #if defined(LIT_IRIDESCENCE) return mix(ret, iridescenceFresnel, iridescenceIntensity); #else return ret; #endif } fn getFresnelCC(cosTheta: f32) -> f32 { let fresnel: f32 = pow(1.0 - saturate(cosTheta), 5.0); return 0.04 + (1.0 - 0.04) * fresnel; }`; var fullscreenQuadVS = ` attribute vertex_position: vec2f; varying vUv0: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(input.vertex_position, 0.5, 1.0); output.vUv0 = input.vertex_position.xy * 0.5 + vec2f(0.5); return output; } `; var gammaPS = ` #include "decodePS" #if (GAMMA == SRGB) fn gammaCorrectInput(color: f32) -> f32 { return decodeGammaFloat(color); } fn gammaCorrectInputVec3(color: vec3f) -> vec3f { return decodeGamma3(color); } fn gammaCorrectInputVec4(color: vec4f) -> vec4f { return vec4f(decodeGamma3(color.xyz), color.w); } fn gammaCorrectOutput(color: vec3f) -> vec3f { return pow(color + 0.0000001, vec3f(1.0 / 2.2)); } #else fn gammaCorrectInput(color: f32) -> f32 { return color; } fn gammaCorrectInputVec3(color: vec3f) -> vec3f { return color; } fn gammaCorrectInputVec4(color: vec4f) -> vec4f { return color; } fn gammaCorrectOutput(color: vec3f) -> vec3f { return color; } #endif `; var glossPS = ` #ifdef STD_GLOSS_CONSTANT uniform material_gloss: f32; #endif fn getGlossiness() { dGlossiness = 1.0; #ifdef STD_GLOSS_CONSTANT dGlossiness = dGlossiness * uniform.material_gloss; #endif #ifdef STD_GLOSS_TEXTURE dGlossiness = dGlossiness * textureSampleBias({STD_GLOSS_TEXTURE_NAME}, {STD_GLOSS_TEXTURE_NAME}Sampler, {STD_GLOSS_TEXTURE_UV}, uniform.textureBias).{STD_GLOSS_TEXTURE_CHANNEL}; #endif #ifdef STD_GLOSS_VERTEX dGlossiness = dGlossiness * saturate(vVertexColor.{STD_GLOSS_VERTEX_CHANNEL}); #endif #ifdef STD_GLOSS_INVERT dGlossiness = 1.0 - dGlossiness; #endif dGlossiness = dGlossiness + 0.0000001; } `; var quadVS = ` attribute aPosition: vec2f; varying uv0: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(input.aPosition, 0.0, 1.0); output.uv0 = getImageEffectUV((input.aPosition + 1.0) * 0.5); return output; } `; var indirectCoreCS = ` struct DrawIndexedIndirectArgs { indexCount: u32, instanceCount: u32, firstIndex: u32, baseVertex: i32, firstInstance: u32 }; struct DrawIndirectArgs { vertexCount: u32, instanceCount: u32, firstVertex: u32, firstInstance: u32, _pad: u32 }; `; var immediateLinePS = ` #include "gammaPS" varying color: vec4f; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; output.color = vec4f(gammaCorrectOutput(decodeGamma3(input.color.rgb)), input.color.a); return output; } `; var immediateLineVS = ` attribute vertex_position: vec4f; attribute vertex_color: vec4f; uniform matrix_model: mat4x4f; uniform matrix_viewProjection: mat4x4f; varying color: vec4f; @vertex fn vertexMain(input : VertexInput) -> VertexOutput { var output : VertexOutput; output.color = input.vertex_color; output.position = uniform.matrix_viewProjection * uniform.matrix_model * input.vertex_position; return output; } `; var iridescenceDiffractionPS = ` uniform material_iridescenceRefractionIndex: f32; fn iridescence_iorToFresnelScalar(transmittedIor: f32, incidentIor: f32) -> f32 { return pow((transmittedIor - incidentIor) / (transmittedIor + incidentIor), 2.0); } fn iridescence_iorToFresnelVec3(transmittedIor: vec3f, incidentIor: f32) -> vec3f { return pow((transmittedIor - vec3f(incidentIor)) / (transmittedIor + vec3f(incidentIor)), vec3f(2.0)); } fn iridescence_fresnelToIor(f0: vec3f) -> vec3f { let sqrtF0: vec3f = sqrt(f0); return (vec3f(1.0) + sqrtF0) / (vec3f(1.0) - sqrtF0); } const XYZ_TO_REC709: mat3x3f = mat3x3f( vec3f(3.2404542, -1.5371385, -0.4985314), vec3f(-0.9692660, 1.8760108, 0.0415560), vec3f(0.0556434, -0.2040259, 1.0572252) ); fn iridescence_sensitivity(opd: f32, shift: vec3f) -> vec3f { let PI: f32 = 3.141592653589793; let phase: f32 = 2.0 * PI * opd * 1.0e-9; const val: vec3f = vec3f(5.4856e-13, 4.4201e-13, 5.2481e-13); const pos: vec3f = vec3f(1.6810e+06, 1.7953e+06, 2.2084e+06); const var_: vec3f = vec3f(4.3278e+09, 9.3046e+09, 6.6121e+09); var xyz: vec3f = val * sqrt(2.0 * PI * var_) * cos(pos * phase + shift) * exp(-pow(phase, 2.0) * var_); xyz.x = xyz.x + 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift[0]) * exp(-4.5282e+09 * pow(phase, 2.0)); xyz = xyz / vec3f(1.0685e-07); return XYZ_TO_REC709 * xyz; } fn iridescence_fresnelScalar(cosTheta: f32, f0: f32) -> f32 { let x: f32 = clamp(1.0 - cosTheta, 0.0, 1.0); let x2: f32 = x * x; let x5: f32 = x * x2 * x2; return f0 + (1.0 - f0) * x5; } fn iridescence_fresnelVec3(cosTheta: f32, f0: vec3f) -> vec3f { let x: f32 = clamp(1.0 - cosTheta, 0.0, 1.0); let x2: f32 = x * x; let x5: f32 = x * x2 * x2; return f0 + (vec3f(1.0) - f0) * x5; } fn calcIridescence(outsideIor: f32, cosTheta: f32, base_f0: vec3f, iridescenceThickness: f32) -> vec3f { let PI: f32 = 3.141592653589793; let iridescenceIor: f32 = mix(outsideIor, uniform.material_iridescenceRefractionIndex, smoothstep(0.0, 0.03, iridescenceThickness)); let sinTheta2Sq: f32 = pow(outsideIor / iridescenceIor, 2.0) * (1.0 - pow(cosTheta, 2.0)); let cosTheta2Sq: f32 = 1.0 - sinTheta2Sq; if (cosTheta2Sq < 0.0) { return vec3f(1.0); } let cosTheta2: f32 = sqrt(cosTheta2Sq); let r0: f32 = iridescence_iorToFresnelScalar(iridescenceIor, outsideIor); let r12: f32 = iridescence_fresnelScalar(cosTheta, r0); let r21: f32 = r12; let t121: f32 = 1.0 - r12; let phi12: f32 = select(0.0, PI, iridescenceIor < outsideIor); let phi21: f32 = PI - phi12; let baseIor: vec3f = iridescence_fresnelToIor(base_f0 + vec3f(0.0001)); let r1: vec3f = iridescence_iorToFresnelVec3(baseIor, iridescenceIor); let r23: vec3f = iridescence_fresnelVec3(cosTheta2, r1); let phi23: vec3f = select(vec3f(0.0), vec3f(PI), baseIor < vec3f(iridescenceIor)); let opd: f32 = 2.0 * iridescenceIor * iridescenceThickness * cosTheta2; let phi: vec3f = vec3f(phi21) + phi23; let r123Sq: vec3f = clamp(vec3f(r12) * r23, vec3f(1e-5), vec3f(0.9999)); let r123: vec3f = sqrt(r123Sq); let rs: vec3f = pow(vec3f(t121), vec3f(2.0)) * r23 / (vec3f(1.0) - r123Sq); let c0: vec3f = vec3f(r12) + rs; var i_irid: vec3f = c0; var cm: vec3f = rs - vec3f(t121); cm = cm * r123; let sm1: vec3f = 2.0 * iridescence_sensitivity(1.0 * opd, 1.0 * phi); i_irid = i_irid + cm * sm1; cm = cm * r123; let sm2: vec3f = 2.0 * iridescence_sensitivity(2.0 * opd, 2.0 * phi); i_irid = i_irid + cm * sm2; return max(i_irid, vec3f(0.0)); } fn getIridescenceDiffraction(cosTheta: f32, specularity: vec3f, iridescenceThickness: f32) -> vec3f { return calcIridescence(1.0, cosTheta, specularity, iridescenceThickness); } `; var iridescencePS = ` #ifdef STD_IRIDESCENCE_CONSTANT uniform material_iridescence: f32; #endif fn getIridescence() { var iridescence = 1.0; #ifdef STD_IRIDESCENCE_CONSTANT iridescence = iridescence * uniform.material_iridescence; #endif #ifdef STD_IRIDESCENCE_TEXTURE iridescence = iridescence * textureSampleBias({STD_IRIDESCENCE_TEXTURE_NAME}, {STD_IRIDESCENCE_TEXTURE_NAME}Sampler, {STD_IRIDESCENCE_TEXTURE_UV}, uniform.textureBias).{STD_IRIDESCENCE_TEXTURE_CHANNEL}; #endif dIridescence = iridescence; } `; var iridescenceThicknessPS = ` uniform material_iridescenceThicknessMax: f32; #ifdef STD_IRIDESCENCETHICKNESS_TEXTURE uniform material_iridescenceThicknessMin: f32; #endif fn getIridescenceThickness() { #ifdef STD_IRIDESCENCETHICKNESS_TEXTURE var blend: f32 = textureSampleBias({STD_IRIDESCENCETHICKNESS_TEXTURE_NAME}, {STD_IRIDESCENCETHICKNESS_TEXTURE_NAME}Sampler, {STD_IRIDESCENCETHICKNESS_TEXTURE_UV}, uniform.textureBias).{STD_IRIDESCENCETHICKNESS_TEXTURE_CHANNEL}; var iridescenceThickness: f32 = mix(uniform.material_iridescenceThicknessMin, uniform.material_iridescenceThicknessMax, blend); #else var iridescenceThickness: f32 = uniform.material_iridescenceThicknessMax; #endif dIridescenceThickness = iridescenceThickness; } `; var iorPS = ` #ifdef STD_IOR_CONSTANT uniform material_refractionIndex: f32; #endif fn getIor() { #ifdef STD_IOR_CONSTANT dIor = uniform.material_refractionIndex; #else dIor = 1.0 / 1.5; #endif } `; var lightDeclarationPS = ` #if defined(LIGHT{i}) uniform light{i}_color: vec3f; #if LIGHT{i}TYPE == DIRECTIONAL uniform light{i}_direction: vec3f; #else #define LIT_CODE_LIGHTS_POINT uniform light{i}_position: vec3f; uniform light{i}_radius: f32; #if LIGHT{i}TYPE == SPOT #define LIT_CODE_LIGHTS_SPOT uniform light{i}_direction: vec3f; uniform light{i}_innerConeAngle: f32; uniform light{i}_outerConeAngle: f32; #endif #endif #if LIGHT{i}SHAPE != PUNCTUAL #define LIT_CODE_FALLOFF_SQUARED #if LIGHT{i}TYPE == DIRECTIONAL uniform light{i}_position: vec3f; #endif uniform light{i}_halfWidth: vec3f; uniform light{i}_halfHeight: vec3f; #else #if LIGHT{i}FALLOFF == LINEAR #define LIT_CODE_FALLOFF_LINEAR #endif #if LIGHT{i}FALLOFF == INVERSESQUARED #define LIT_CODE_FALLOFF_SQUARED #endif #endif #if defined(LIGHT{i}CASTSHADOW) #if LIGHT{i}TYPE != OMNI uniform light{i}_shadowMatrix: mat4x4f; #endif uniform light{i}_shadowIntensity: f32; uniform light{i}_shadowParams: vec4f; #if LIGHT{i}SHADOWTYPE == PCSS_32F uniform light{i}_shadowSearchArea: f32; uniform light{i}_cameraParams: vec4f; #if LIGHT{i}TYPE == DIRECTIONAL uniform light{i}_softShadowParams: vec4f; #endif #endif #if LIGHT{i}TYPE == DIRECTIONAL uniform light{i}_shadowMatrixPalette: array; uniform light{i}_shadowCascadeDistances: vec4f; uniform light{i}_shadowCascadeCount: i32; uniform light{i}_shadowCascadeBlend: f32; #endif #if LIGHT{i}TYPE == OMNI NOT SUPPORTED #else #if defined(LIGHT{i}SHADOW_PCF) var light{i}_shadowMap: texture_depth_2d; var light{i}_shadowMapSampler: sampler_comparison; #else var light{i}_shadowMap: texture_2d; var light{i}_shadowMapSampler: sampler; #endif #endif #endif #if defined(LIGHT{i}COOKIE) #define LIT_CODE_COOKIE #if LIGHT{i}TYPE == OMNI NOT SUPPORTED #endif #if LIGHT{i}TYPE == SPOT NOT SUPPORTED #endif #endif #endif `; var lightDiffuseLambertPS = ` fn getLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDirNorm: vec3f) -> f32 { return max(dot(worldNormal, -lightDirNorm), 0.0); } `; var lightDirPointPS = ` fn evalOmniLight(lightPosW: vec3f) -> vec3f { return vPositionW - lightPosW; } `; var lightEvaluationPS = ` #if defined(LIGHT{i}) evaluateLight{i}( #if defined(LIT_IRIDESCENCE) iridescenceFresnel #endif ); #endif `; var lightFunctionLightPS = ` #if defined(LIGHT{i}) fn evaluateLight{i}( #if defined(LIT_IRIDESCENCE) iridescenceFresnel: vec3f #endif ) { var lightColor: vec3f = uniform.light{i}_color; #if LIGHT{i}TYPE == DIRECTIONAL && !defined(LIT_SHADOW_CATCHER) if (all(lightColor == vec3f(0.0, 0.0, 0.0))) { return; } #endif #if LIGHT{i}TYPE == DIRECTIONAL dLightDirNormW = uniform.light{i}_direction; dAtten = 1.0; #else var lightDirW: vec3f = evalOmniLight(uniform.light{i}_position); dLightDirNormW = normalize(lightDirW); #if defined(LIGHT{i}COOKIE) #if LIGHT{i}TYPE == SPOT #ifdef LIGHT{i}COOKIE_FALLOFF #ifdef LIGHT{i}COOKIE_TRANSFORM var cookieAttenuation: vec3f = getCookie2DXform(uniform.light{i}_cookie, uniform.light{i}_shadowMatrix, uniform.light{i}_cookieIntensity, uniform.light{i}_cookieMatrix, uniform.light{i}_cookieOffset).{LIGHT{i}COOKIE_CHANNEL}; #else var cookieAttenuation: vec3f = getCookie2D(uniform.light{i}_cookie, uniform.light{i}_shadowMatrix, uniform.light{i}_cookieIntensity).{LIGHT{i}COOKIE_CHANNEL}; #endif #else #ifdef LIGHT{i}COOKIE_TRANSFORM var cookieAttenuation: vec3f = getCookie2DClipXform(uniform.light{i}_cookie, uniform.light{i}_shadowMatrix, uniform.light{i}_cookieIntensity, uniform.light{i}_cookieMatrix, uniform.light{i}_cookieOffset).{LIGHT{i}COOKIE_CHANNEL}; #else var cookieAttenuation: vec3f = getCookie2DClip(uniform.light{i}_cookie, uniform.light{i}_shadowMatrix, uniform.light{i}_cookieIntensity).{LIGHT{i}COOKIE_CHANNEL}; #endif #endif #endif #if LIGHT{i}TYPE == OMNI var cookieAttenuation: vec3f = getCookieCube(uniform.light{i}_cookie, uniform.light{i}_shadowMatrix, uniform.light{i}_cookieIntensity).{LIGHT{i}COOKIE_CHANNEL}; #endif lightColor = lightColor * cookieAttenuation; #endif #if LIGHT{i}SHAPE == PUNCTUAL #if LIGHT{i}FALLOFF == LINEAR dAtten = getFalloffLinear(uniform.light{i}_radius, lightDirW); #else dAtten = getFalloffInvSquared(uniform.light{i}_radius, lightDirW); #endif #else dAtten = getFalloffWindow(uniform.light{i}_radius, lightDirW); #endif #if LIGHT{i}TYPE == SPOT #if !defined(LIGHT{i}COOKIE) || defined(LIGHT{i}COOKIE_FALLOFF) dAtten = dAtten * getSpotEffect(uniform.light{i}_direction, uniform.light{i}_innerConeAngle, uniform.light{i}_outerConeAngle, dLightDirNormW); #endif #endif #endif if (dAtten < 0.00001) { return; } #if LIGHT{i}SHAPE != PUNCTUAL #if LIGHT{i}SHAPE == RECT calcRectLightValues(uniform.light{i}_position, uniform.light{i}_halfWidth, uniform.light{i}_halfHeight); #elif LIGHT{i}SHAPE == DISK calcDiskLightValues(uniform.light{i}_position, uniform.light{i}_halfWidth, uniform.light{i}_halfHeight); #elif LIGHT{i}SHAPE == SPHERE calcSphereLightValues(uniform.light{i}_position, uniform.light{i}_halfWidth, uniform.light{i}_halfHeight); #endif #endif #if LIGHT{i}SHAPE != PUNCTUAL #if LIGHT{i}TYPE == DIRECTIONAL var attenDiffuse: f32 = getLightDiffuse(litArgs_worldNormal, dViewDirW, dLightDirNormW); #else #if LIGHT{i}SHAPE == RECT var attenDiffuse: f32 = getRectLightDiffuse(litArgs_worldNormal, dViewDirW, lightDirW, dLightDirNormW) * 16.0; #elif LIGHT{i}SHAPE == DISK var attenDiffuse: f32 = getDiskLightDiffuse(litArgs_worldNormal, dViewDirW, lightDirW, dLightDirNormW) * 16.0; #elif LIGHT{i}SHAPE == SPHERE var attenDiffuse: f32 = getSphereLightDiffuse(litArgs_worldNormal, dViewDirW, lightDirW, dLightDirNormW) * 16.0; #endif #endif #else dAtten = dAtten * getLightDiffuse(litArgs_worldNormal, vec3(0.0), dLightDirNormW); #endif #ifdef LIGHT{i}CASTSHADOW #if LIGHT{i}TYPE == DIRECTIONAL var shadow: f32 = getShadow{i}(vec3(0.0)); #else var shadow: f32 = getShadow{i}(lightDirW); #endif shadow = mix(1.0, shadow, uniform.light{i}_shadowIntensity); dAtten = dAtten * shadow; #if defined(LIT_SHADOW_CATCHER) && LIGHT{i}TYPE == DIRECTIONAL dShadowCatcher = dShadowCatcher * shadow; #endif #endif #if LIGHT{i}SHAPE != PUNCTUAL #ifdef LIT_SPECULAR dDiffuseLight = dDiffuseLight + (((attenDiffuse * dAtten) * lightColor) * (1.0 - dLTCSpecFres)); #else dDiffuseLight = dDiffuseLight + ((attenDiffuse * dAtten) * lightColor); #endif #else #if defined(AREA_LIGHTS) && defined(LIT_SPECULAR) dDiffuseLight = dDiffuseLight + ((dAtten * lightColor) * (1.0 - litArgs_specularity)); #else dDiffuseLight = dDiffuseLight + (dAtten * lightColor); #endif #endif #ifdef LIGHT{i}AFFECT_SPECULARITY #if LIGHT{i}SHAPE != PUNCTUAL #ifdef LIT_CLEARCOAT #if LIGHT{i}SHAPE == RECT ccSpecularLight = ccSpecularLight + (ccLTCSpecFres * getRectLightSpecular(litArgs_clearcoat_worldNormal, dViewDirW) * dAtten * lightColor); #elif LIGHT{i}SHAPE == DISK ccSpecularLight = ccSpecularLight + (ccLTCSpecFres * getDiskLightSpecular(litArgs_clearcoat_worldNormal, dViewDirW) * dAtten * lightColor); #elif LIGHT{i}SHAPE == SPHERE ccSpecularLight = ccSpecularLight + (ccLTCSpecFres * getSphereLightSpecular(litArgs_clearcoat_worldNormal, dViewDirW) * dAtten * lightColor); #endif #endif #ifdef LIT_SPECULAR #if LIGHT{i}SHAPE == RECT dSpecularLight = dSpecularLight + (dLTCSpecFres * getRectLightSpecular(litArgs_worldNormal, dViewDirW) * dAtten * lightColor); #elif LIGHT{i}SHAPE == DISK dSpecularLight = dSpecularLight + (dLTCSpecFres * getDiskLightSpecular(litArgs_worldNormal, dViewDirW) * dAtten * lightColor); #elif LIGHT{i}SHAPE == SPHERE dSpecularLight = dSpecularLight + (dLTCSpecFres * getSphereLightSpecular(litArgs_worldNormal, dViewDirW) * dAtten * lightColor); #endif #endif #else #if LIGHT{i}TYPE == DIRECTIONAL && LIT_FRESNEL_MODEL != NONE #define LIGHT{i}FRESNEL #endif #ifdef LIT_SPECULAR var halfDirW: vec3f = normalize(-dLightDirNormW + dViewDirW); #endif #ifdef LIT_CLEARCOAT var lightspecularCC: vec3f = getLightSpecular(halfDirW, ccReflDirW, litArgs_clearcoat_worldNormal, dViewDirW, dLightDirNormW, litArgs_clearcoat_gloss, dTBN) * dAtten * lightColor; #ifdef LIGHT{i}FRESNEL lightspecularCC = lightspecularCC * getFresnelCC(dot(dViewDirW, halfDirW)); #endif ccSpecularLight = ccSpecularLight + lightspecularCC; #endif #ifdef LIT_SHEEN sSpecularLight = sSpecularLight + (getLightSpecularSheen(halfDirW, litArgs_worldNormal, dViewDirW, dLightDirNormW, litArgs_sheen_gloss) * dAtten * lightColor); #endif #ifdef LIT_SPECULAR var lightSpecular: vec3f = getLightSpecular(halfDirW, dReflDirW, litArgs_worldNormal, dViewDirW, dLightDirNormW, litArgs_gloss, dTBN) * dAtten * lightColor; #ifdef LIGHT{i}FRESNEL #if defined(LIT_IRIDESCENCE) lightSpecular = lightSpecular * getFresnel(dot(dViewDirW, halfDirW), litArgs_gloss, litArgs_specularity, iridescenceFresnel, litArgs_iridescence_intensity); #else lightSpecular = lightSpecular * getFresnel(dot(dViewDirW, halfDirW), litArgs_gloss, litArgs_specularity); #endif #else lightSpecular = lightSpecular * litArgs_specularity; #endif dSpecularLight = dSpecularLight + lightSpecular; #endif #endif #endif } #endif `; var lightFunctionShadowPS = ` #ifdef LIGHT{i}CASTSHADOW #ifdef LIGHT{i}_SHADOW_SAMPLE_POINT fn getShadowSampleCoordOmni{i}(shadowParams: vec4f, worldPosition: vec3f, lightPos: vec3f, lightDir: ptr, lightDirNorm: vec3f, normal: vec3f) -> vec3f { #ifdef LIGHT{i}_SHADOW_SAMPLE_NORMAL_OFFSET let distScale: f32 = length(*lightDir); var surfacePosition = worldPosition + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; *lightDir = surfacePosition - lightPos; #endif return *lightDir; } #endif #ifndef LIGHT{i}_SHADOW_SAMPLE_POINT fn getShadowSampleCoord{i}(shadowTransform: mat4x4f, shadowParams: vec4f, worldPosition: vec3f, lightPos: vec3f, lightDir: ptr, lightDirNorm: vec3f, normal: vec3f) -> vec3f { var surfacePosition = worldPosition; #ifdef LIGHT{i}_SHADOW_SAMPLE_SOURCE_ZBUFFER #ifdef LIGHT{i}_SHADOW_SAMPLE_NORMAL_OFFSET surfacePosition = surfacePosition + normal * shadowParams.y; #endif #else #ifdef LIGHT{i}_SHADOW_SAMPLE_NORMAL_OFFSET #ifdef LIGHT{i}_SHADOW_SAMPLE_ORTHO var distScale: f32 = 1.0; #else var distScale: f32 = abs(dot(vPositionW - lightPos, lightDirNorm)); #endif surfacePosition = surfacePosition + normal * shadowParams.y * clamp(1.0 - dot(normal, -lightDirNorm), 0.0, 1.0) * distScale; #endif #endif var positionInShadowSpace: vec4f = shadowTransform * vec4f(surfacePosition, 1.0); #ifdef LIGHT{i}_SHADOW_SAMPLE_ORTHO positionInShadowSpace.z = saturate(positionInShadowSpace.z) - 0.0001; #else #ifdef LIGHT{i}_SHADOW_SAMPLE_SOURCE_ZBUFFER positionInShadowSpace.xyz = positionInShadowSpace.xyz / positionInShadowSpace.w; #else positionInShadowSpace.xy = positionInShadowSpace.xy / positionInShadowSpace.w; positionInShadowSpace.z = length(*lightDir) * shadowParams.w; #endif #endif return positionInShadowSpace.xyz; } #endif fn getShadow{i}(lightDirW_in: vec3f) -> f32 { var lightDirArg = lightDirW_in; #if LIGHT{i}TYPE == OMNI var shadowCoord: vec3f = getShadowSampleCoordOmni{i}(uniform.light{i}_shadowParams, vPositionW, uniform.light{i}_position, &lightDirArg, dLightDirNormW, dVertexNormalW); #else #ifdef LIGHT{i}_SHADOW_CASCADES var cascadeIndex: i32 = getShadowCascadeIndex(uniform.light{i}_shadowCascadeDistances, uniform.light{i}_shadowCascadeCount); #ifdef LIGHT{i}_SHADOW_CASCADE_BLEND cascadeIndex = ditherShadowCascadeIndex(cascadeIndex, uniform.light{i}_shadowCascadeDistances, uniform.light{i}_shadowCascadeCount, uniform.light{i}_shadowCascadeBlend); #endif var shadowMatrix: mat4x4f = uniform.light{i}_shadowMatrixPalette[cascadeIndex]; #else var shadowMatrix: mat4x4f = uniform.light{i}_shadowMatrix; #endif #if LIGHT{i}TYPE == DIRECTIONAL var shadowCoord: vec3f = getShadowSampleCoord{i}(shadowMatrix, uniform.light{i}_shadowParams, vPositionW, vec3f(0.0), &lightDirArg, dLightDirNormW, dVertexNormalW); #else var shadowCoord: vec3f = getShadowSampleCoord{i}(shadowMatrix, uniform.light{i}_shadowParams, vPositionW, uniform.light{i}_position, &lightDirArg, dLightDirNormW, dVertexNormalW); #endif #endif #if LIGHT{i}TYPE == DIRECTIONAL shadowCoord = fadeShadow(shadowCoord, uniform.light{i}_shadowCascadeDistances); #endif #if LIGHT{i}TYPE == DIRECTIONAL #if LIGHT{i}SHADOWTYPE == VSM_16F return getShadowVSM16(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, 5.54); #endif #if LIGHT{i}SHADOWTYPE == VSM_32F return getShadowVSM32(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, 15.0); #endif #if LIGHT{i}SHADOWTYPE == PCSS_32F #if LIGHT{i}SHAPE != PUNCTUAL let shadowSearchArea = vec2f(length(uniform.light{i}_halfWidth), length(uniform.light{i}_halfHeight)) * uniform.light{i}_shadowSearchArea; return getShadowPCSS(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, uniform.light{i}_cameraParams, shadowSearchArea, lightDirW_in); #else return getShadowPCSS(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, uniform.light{i}_cameraParams, uniform.light{i}_softShadowParams, lightDirW_in); #endif #endif #if LIGHT{i}SHADOWTYPE == PCF1_16F || LIGHT{i}SHADOWTYPE == PCF1_32F return getShadowPCF1x1(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF3_16F || LIGHT{i}SHADOWTYPE == PCF3_32F return getShadowPCF3x3(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF5_16F || LIGHT{i}SHADOWTYPE == PCF5_32F return getShadowPCF5x5(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams); #endif #endif #if LIGHT{i}TYPE == SPOT #if LIGHT{i}SHADOWTYPE == VSM_16F return getShadowSpotVSM16(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, 5.54, lightDirW_in); #endif #if LIGHT{i}SHADOWTYPE == VSM_32F return getShadowSpotVSM32(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, 15.0, lightDirW_in); #endif #if LIGHT{i}SHADOWTYPE == PCSS_32F #if LIGHT{i}SHAPE != PUNCTUAL var shadowSearchArea: vec2f = vec2f(length(uniform.light{i}_halfWidth), length(uniform.light{i}_halfHeight)) * uniform.light{i}_shadowSearchArea; #else var shadowSearchArea: vec2f = vec2f(uniform.light{i}_shadowSearchArea); #endif return getShadowSpotPCSS(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, uniform.light{i}_cameraParams, shadowSearchArea, lightDirW_in); #endif #if LIGHT{i}SHADOWTYPE == PCF1_16F || LIGHT{i}SHADOWTYPE == PCF1_32F return getShadowSpotPCF1x1(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF3_16F || LIGHT{i}SHADOWTYPE == PCF3_32F return getShadowSpotPCF3x3(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams); #endif #if LIGHT{i}SHADOWTYPE == PCF5_16F || LIGHT{i}SHADOWTYPE == PCF5_32F return getShadowSpotPCF5x5(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams); #endif #endif #if LIGHT{i}TYPE == OMNI #if LIGHT{i}SHADOWTYPE == PCSS_32F var shadowSearchArea: vec2f; #if LIGHT{i}SHAPE != PUNCTUAL var shadowSearchArea: vec2f = vec2f(length(uniform.light{i}_halfWidth), length(uniform.light{i}_halfHeight)) * uniform.light{i}_shadowSearchArea; #else var shadowSearchArea: vec2f = vec2f(uniform.light{i}_shadowSearchArea); #endif return getShadowOmniPCSS(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, uniform.light{i}_cameraParams, shadowSearchArea, lightDirW_in); #endif #if LIGHT{i}SHADOWTYPE == PCF1_16F || LIGHT{i}SHADOWTYPE == PCF1_32F return getShadowOmniPCF1x1(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, lightDirW_in); #endif #if LIGHT{i}SHADOWTYPE == PCF3_16F || LIGHT{i}SHADOWTYPE == PCF3_32F return getShadowOmniPCF3x3(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, lightDirW_in); #endif #endif } #endif `; var lightingPS = ` #ifdef LIT_CLUSTERED_LIGHTS #define LIT_CODE_FALLOFF_LINEAR #define LIT_CODE_FALLOFF_SQUARED #define LIT_CODE_LIGHTS_POINT #define LIT_CODE_LIGHTS_SPOT #endif #ifdef AREA_LIGHTS var areaLightsLutTex1: texture_2d; var areaLightsLutTex1Sampler: sampler; var areaLightsLutTex2: texture_2d; var areaLightsLutTex2Sampler: sampler; #endif #ifdef LIT_LIGHTING #include "lightDiffuseLambertPS" #if defined(AREA_LIGHTS) || defined(LIT_CLUSTERED_AREA_LIGHTS) #include "ltcPS" #endif #endif #ifdef SHADOW_DIRECTIONAL #include "shadowCascadesPS" #endif #if defined(SHADOW_KIND_PCF1) #include "shadowPCF1PS" #endif #if defined(SHADOW_KIND_PCF3) #include "shadowPCF3PS" #endif #if defined(SHADOW_KIND_PCF5) #include "shadowPCF5PS" #endif #if defined(SHADOW_KIND_PCSS) #include "linearizeDepthPS" #include "shadowSoftPS" #endif #if defined(SHADOW_KIND_VSM) #include "shadowEVSMPS" #endif #ifdef LIT_CODE_FALLOFF_LINEAR #include "falloffLinearPS" #endif #ifdef LIT_CODE_FALLOFF_SQUARED #include "falloffInvSquaredPS" #endif #ifdef LIT_CODE_LIGHTS_POINT #include "lightDirPointPS" #endif #ifdef LIT_CODE_LIGHTS_SPOT #include "spotPS" #endif #ifdef LIT_CODE_COOKIE #include "cookiePS" #endif #ifdef LIT_CLUSTERED_LIGHTS #include "clusteredLightPS" #endif #ifdef LIGHT_COUNT > 0 #include "lightFunctionShadowPS, LIGHT_COUNT" #include "lightFunctionLightPS, LIGHT_COUNT" #endif `; var lightmapAddPS = ` fn addLightMap( lightmap: vec3f, dir: vec3f, worldNormal: vec3f, viewDir: vec3f, reflectionDir: vec3f, gloss: f32, specularity: vec3f, vertexNormal: vec3f, tbn: mat3x3f #if defined(LIT_IRIDESCENCE) , iridescenceFresnel: vec3f, iridescenceIntensity: f32 #endif ) { #if defined(LIT_SPECULAR) && defined(LIT_DIR_LIGHTMAP) if (dot(dir, dir) < 0.0001) { dDiffuseLight = dDiffuseLight + lightmap; } else { let vlight: f32 = saturate(dot(dir, -vertexNormal)); let flight: f32 = saturate(dot(dir, -worldNormal)); let nlight: f32 = (flight / max(vlight, 0.01)) * 0.5; dDiffuseLight = dDiffuseLight + lightmap * nlight * 2.0; let halfDir: vec3f = normalize(-dir + viewDir); var specularLight: vec3f = lightmap * getLightSpecular(halfDir, reflectionDir, worldNormal, viewDir, dir, gloss, tbn); #ifdef LIT_SPECULAR_FRESNEL specularLight = specularLight * getFresnel(dot(viewDir, halfDir), gloss, specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, iridescenceIntensity #endif ); #endif dSpecularLight = dSpecularLight + specularLight; } #else dDiffuseLight = dDiffuseLight + lightmap; #endif } `; var lightmapPS = ` #ifdef STD_LIGHTMAP_DIR var dLightmapDir: vec3f; var texture_dirLightMap: texture_2d; var texture_dirLightMapSampler: sampler; #endif fn getLightMap() { dLightmap = vec3f(1.0); #ifdef STD_LIGHT_TEXTURE dLightmap = dLightmap * {STD_LIGHT_TEXTURE_DECODE}(textureSampleBias({STD_LIGHT_TEXTURE_NAME}, {STD_LIGHT_TEXTURE_NAME}Sampler, {STD_LIGHT_TEXTURE_UV}, uniform.textureBias)).{STD_LIGHT_TEXTURE_CHANNEL}; #ifdef STD_LIGHTMAP_DIR var dir: vec3f = textureSampleBias(texture_dirLightMap, texture_dirLightMapSampler, {STD_LIGHT_TEXTURE_UV}, uniform.textureBias).xyz * 2.0 - 1.0; var dirDot = dot(dir, dir); dLightmapDir = select(vec3(0.0), dir / sqrt(dirDot), dirDot > 0.001); #endif #endif #ifdef STD_LIGHT_VERTEX dLightmap = dLightmap * saturate(vVertexColor.{STD_LIGHT_VERTEX_CHANNEL}); #endif } `; var lightSpecularAnisoGGXPS = ` fn calcLightSpecular(gloss: f32, worldNormal: vec3f, viewDir: vec3f, h: vec3f, lightDirNorm: vec3f, tbn: mat3x3f) -> f32 { let PI: f32 = 3.141592653589793; let roughness: f32 = max((1.0 - gloss) * (1.0 - gloss), 0.001); let alphaRoughness: f32 = roughness * roughness; let anisotropy: f32 = dAnisotropy; let direction: vec2f = dAnisotropyRotation; let at: f32 = mix(alphaRoughness, 1.0, anisotropy * anisotropy); let ab: f32 = clamp(alphaRoughness, 0.001, 1.0); let anisotropicT: vec3f = normalize(tbn * vec3f(direction, 0.0)); let anisotropicB: vec3f = normalize(cross(tbn[2], anisotropicT)); let NoH: f32 = dot(worldNormal, h); let ToH: f32 = dot(anisotropicT, h); let BoH: f32 = dot(anisotropicB, h); let a2: f32 = at * ab; let v: vec3f = vec3f(ab * ToH, at * BoH, a2 * NoH); let v2: f32 = dot(v, v); let w2: f32 = a2 / v2; let D: f32 = a2 * w2 * w2 * (1.0 / PI); let ToV: f32 = dot(anisotropicT, viewDir); let BoV: f32 = dot(anisotropicB, viewDir); let ToL: f32 = dot(anisotropicT, -lightDirNorm); let BoL: f32 = dot(anisotropicB, -lightDirNorm); let NoV: f32 = dot(worldNormal, viewDir); let NoL: f32 = dot(worldNormal, -lightDirNorm); let lambdaV: f32 = NoL * length(vec3f(at * ToV, ab * BoV, NoV)); let lambdaL: f32 = NoV * length(vec3f(at * ToL, ab * BoL, NoL)); let G: f32 = 0.5 / (lambdaV + lambdaL); return D * G; } fn getLightSpecular(h: vec3f, reflDir: vec3f, worldNormal: vec3f, viewDir: vec3f, lightDirNorm: vec3f, gloss: f32, tbn: mat3x3f) -> f32 { return calcLightSpecular(gloss, worldNormal, viewDir, h, lightDirNorm, tbn); } `; var lightSpecularGGXPS = ` fn calcLightSpecular(gloss: f32, worldNormal: vec3f, viewDir: vec3f, h: vec3f, lightDirNorm: vec3f) -> f32 { const PI: f32 = 3.141592653589793; let roughness: f32 = max((1.0 - gloss) * (1.0 - gloss), 0.001); let alpha: f32 = roughness * roughness; let NoH: f32 = max(dot(worldNormal, h), 0.0); let NoV: f32 = max(dot(worldNormal, viewDir), 0.0); let NoL: f32 = max(dot(worldNormal, -lightDirNorm), 0.0); let NoH2: f32 = NoH * NoH; let denom: f32 = NoH2 * (alpha - 1.0) + 1.0; let D: f32 = alpha / (PI * denom * denom); let alpha2: f32 = alpha * alpha; let lambdaV: f32 = NoL * sqrt(NoV * NoV * (1.0 - alpha2) + alpha2); let lambdaL: f32 = NoV * sqrt(NoL * NoL * (1.0 - alpha2) + alpha2); let G: f32 = 0.5 / max(lambdaV + lambdaL, 0.00001); return D * G; } fn getLightSpecular(h: vec3f, reflDir: vec3f, worldNormal: vec3f, viewDir: vec3f, lightDirNorm: vec3f, gloss: f32, tbn: mat3x3f) -> f32 { return calcLightSpecular(gloss, worldNormal, viewDir, h, lightDirNorm); } `; var lightSpecularBlinnPS = ` fn calcLightSpecular(gloss: f32, worldNormal: vec3f, h: vec3f) -> f32 { let nh: f32 = max( dot( h, worldNormal ), 0.0 ); var specPow: f32 = exp2(gloss * 11.0); specPow = max(specPow, 0.0001); return pow(nh, specPow) * (specPow + 2.0) / 8.0; } fn getLightSpecular(h: vec3f, reflDir: vec3f, worldNormal: vec3f, viewDir: vec3f, lightDirNorm: vec3f, gloss: f32, tbn: mat3x3f) -> f32 { return calcLightSpecular(gloss, worldNormal, h); } `; var lightSheenPS = ` fn sheenD(normal: vec3f, h: vec3f, roughness: f32) -> f32 { let PI: f32 = 3.141592653589793; let invR: f32 = 1.0 / (roughness * roughness); var cos2h: f32 = max(dot(normal, h), 0.0); cos2h = cos2h * cos2h; let sin2h: f32 = max(1.0 - cos2h, 0.0078125); return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * PI); } fn sheenV(normal: vec3f, viewDir: vec3f, light: vec3f) -> f32 { let NoV: f32 = max(dot(normal, viewDir), 0.000001); let NoL: f32 = max(dot(normal, light), 0.000001); return 1.0 / (4.0 * (NoL + NoV - NoL * NoV)); } fn getLightSpecularSheen(h: vec3f, worldNormal: vec3f, viewDir: vec3f, lightDirNorm: vec3f, sheenGloss: f32) -> f32 { let D: f32 = sheenD(worldNormal, h, sheenGloss); let V: f32 = sheenV(worldNormal, viewDir, -lightDirNorm); return D * V; }`; var linearizeDepthPS = ` #ifndef LINEARIZE_DEPTH #define LINEARIZE_DEPTH fn linearizeDepthWithParams(z: f32, cameraParams: vec4f) -> f32 { if (cameraParams.w == 0.0) { return (cameraParams.z * cameraParams.y) / (cameraParams.y + z * (cameraParams.z - cameraParams.y)); } else { return cameraParams.z + z * (cameraParams.y - cameraParams.z); } } #ifndef CAMERAPLANES #define CAMERAPLANES uniform camera_params: vec4f; #endif fn linearizeDepth(z: f32) -> f32 { return linearizeDepthWithParams(z, uniform.camera_params); } #endif `; var litForwardBackendPS = ` fn evaluateBackend() -> FragmentOutput { var output: FragmentOutput; #ifdef LIT_SSAO litArgs_ao = litArgs_ao * textureSampleLevel(ssaoTexture, ssaoTextureSampler, pcPosition.xy * uniform.ssaoTextureSizeInv, 0.0).r; #endif #ifdef LIT_NEEDS_NORMAL #ifdef LIT_SPECULAR getReflDir(litArgs_worldNormal, dViewDirW, litArgs_gloss, dTBN); #endif #ifdef LIT_CLEARCOAT ccReflDirW = normalize(-reflect(dViewDirW, litArgs_clearcoat_worldNormal)); #endif #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_METALNESS var f0: f32 = 1.0 / litArgs_ior; f0 = (f0 - 1.0) / (f0 + 1.0); f0 = f0 * f0; #ifdef LIT_SPECULARITY_FACTOR litArgs_specularity = getSpecularModulate(litArgs_specularity, litArgs_albedo, litArgs_metalness, f0, litArgs_specularityFactor); #else litArgs_specularity = getSpecularModulate(litArgs_specularity, litArgs_albedo, litArgs_metalness, f0, 1.0); #endif litArgs_albedo = getAlbedoModulate(litArgs_albedo, litArgs_metalness); #endif #ifdef LIT_IRIDESCENCE var iridescenceFresnel: vec3f = getIridescenceDiffraction(saturate(dot(dViewDirW, litArgs_worldNormal)), litArgs_specularity, litArgs_iridescence_thickness); #endif #endif #ifdef LIT_ADD_AMBIENT addAmbient(litArgs_worldNormal); #ifdef LIT_SPECULAR dDiffuseLight = dDiffuseLight * (1.0 - litArgs_specularity); #endif #ifdef LIT_SEPARATE_AMBIENT var dAmbientLight: vec3f = dDiffuseLight; dDiffuseLight = vec3(0.0); #endif #endif #ifndef LIT_OLD_AMBIENT dDiffuseLight = dDiffuseLight * uniform.material_ambient; #endif #ifdef LIT_AO #ifndef LIT_OCCLUDE_DIRECT occludeDiffuse(litArgs_ao); #endif #endif #ifdef LIT_LIGHTMAP addLightMap( litArgs_lightmap, litArgs_lightmapDir, litArgs_worldNormal, dViewDirW, dReflDirW, litArgs_gloss, litArgs_specularity, dVertexNormalW, dTBN #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, litArgs_iridescence_intensity #endif ); #endif #ifdef LIT_LIGHTING || LIT_REFLECTIONS #ifdef LIT_REFLECTIONS #ifdef LIT_CLEARCOAT addReflectionCC(ccReflDirW, litArgs_clearcoat_gloss); #ifdef LIT_SPECULAR_FRESNEL ccFresnel = getFresnelCC(dot(dViewDirW, litArgs_clearcoat_worldNormal)); ccReflection = ccReflection * ccFresnel; #else ccFresnel = 0.0; #endif #endif #ifdef LIT_SPECULARITY_FACTOR ccReflection = ccReflection * litArgs_specularityFactor; #endif #ifdef LIT_SHEEN addReflectionSheen(litArgs_worldNormal, dViewDirW, litArgs_sheen_gloss); #endif addReflection(dReflDirW, litArgs_gloss); #ifdef LIT_FRESNEL_MODEL dReflection = vec4f( dReflection.rgb * getFresnel( dot(dViewDirW, litArgs_worldNormal), litArgs_gloss, litArgs_specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, litArgs_iridescence_intensity #endif ), dReflection.a ); #else dReflection = vec4f(dReflection.rgb * litArgs_specularity, dReflection.a); #endif #endif #ifdef AREA_LIGHTS dSpecularLight = dSpecularLight * litArgs_specularity; #ifdef LIT_SPECULAR calcLTCLightValues(litArgs_gloss, litArgs_worldNormal, dViewDirW, litArgs_specularity, litArgs_clearcoat_gloss, litArgs_clearcoat_worldNormal, litArgs_clearcoat_specularity); #endif #endif #ifdef LIGHT_COUNT > 0 #include "lightEvaluationPS, LIGHT_COUNT" #endif #ifdef LIT_CLUSTERED_LIGHTS addClusteredLights(litArgs_worldNormal, dViewDirW, dReflDirW, #if defined(LIT_CLEARCOAT) ccReflDirW, #endif litArgs_gloss, litArgs_specularity, dVertexNormalW, dTBN, #if defined(LIT_IRIDESCENCE) iridescenceFresnel, #endif litArgs_clearcoat_worldNormal, litArgs_clearcoat_gloss, litArgs_sheen_gloss, litArgs_iridescence_intensity ); #endif #ifdef AREA_LIGHTS #ifdef LIT_CLEARCOAT litArgs_clearcoat_specularity = 1.0; #endif #ifdef LIT_SPECULAR litArgs_specularity = vec3(1.0); #endif #endif #ifdef LIT_REFRACTION addRefraction( litArgs_worldNormal, dViewDirW, litArgs_thickness, litArgs_gloss, litArgs_specularity, litArgs_albedo, litArgs_transmission, litArgs_ior, litArgs_dispersion #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, litArgs_iridescence_intensity #endif ); #endif #endif #ifdef LIT_AO #ifdef LIT_OCCLUDE_DIRECT occludeDiffuse(litArgs_ao); #endif #if LIT_OCCLUDE_SPECULAR != NONE occludeSpecular(litArgs_gloss, litArgs_ao, litArgs_worldNormal, dViewDirW); #endif #endif #if !defined(LIT_OPACITY_FADES_SPECULAR) #if LIT_BLEND_TYPE == NORMAL || LIT_BLEND_TYPE == PREMULTIPLIED var specLum: f32 = dot((dSpecularLight + dReflection.rgb * dReflection.a), vec3f( 0.2126, 0.7152, 0.0722 )); #ifdef LIT_CLEARCOAT specLum = specLum + dot(ccSpecularLight * litArgs_clearcoat_specularity + ccReflection * litArgs_clearcoat_specularity, vec3f( 0.2126, 0.7152, 0.0722 )); #endif litArgs_opacity = clamp(litArgs_opacity + gammaCorrectInput(specLum), 0.0, 1.0); #endif litArgs_opacity = litArgs_opacity * uniform.material_alphaFade; #endif #ifdef LIT_LIGHTMAP_BAKING #ifdef LIT_LIGHTMAP_BAKING_COLOR #include "bakeLmEndPS" #endif #ifdef LIT_LIGHTMAP_BAKING_DIR #include "bakeDirLmEndPS" #endif #else #include "endPS" #include "outputAlphaPS" #endif #ifdef LIT_MSDF output.color = applyMsdf(output.color); #endif #include "outputPS" #include "debugOutputPS" #ifdef LIT_SHADOW_CATCHER output.color = vec4f(vec3f(dShadowCatcher), output.color.a); #endif return output; } `; var litForwardDeclarationPS = ` var sReflection: vec3f; var dVertexNormalW: vec3f; var dTangentW: vec3f; var dBinormalW: vec3f; var dViewDirW: vec3f; var dReflDirW: vec3f; var ccReflDirW: vec3f; var dLightDirNormW: vec3f; var dAtten: f32; var dTBN: mat3x3f; var dReflection: vec4f; var dDiffuseLight: vec3f; var dSpecularLight: vec3f; var ccFresnel: f32; var ccReflection: vec3f; var ccSpecularLight: vec3f; var ccSpecularityNoFres: f32; var sSpecularLight: vec3f; #ifdef LIT_DISPERSION uniform material_dispersion: f32; #endif #ifndef LIT_OPACITY_FADES_SPECULAR uniform material_alphaFade: f32; #endif #ifdef LIT_SSAO var ssaoTexture : texture_2d; var ssaoTextureSampler : sampler; uniform ssaoTextureSizeInv: vec2f; #endif #ifdef LIT_SHADOW_CATCHER var dShadowCatcher: f32 = 1.0; #endif #if LIGHT_COUNT > 0 #include "lightDeclarationPS, LIGHT_COUNT" #endif #ifdef LIT_SPECULAR #if LIT_FRESNEL_MODEL == NONE && !defined(LIT_REFLECTIONS) && !defined(LIT_DIFFUSE_MAP) #define LIT_OLD_AMBIENT #endif #endif #ifdef STD_LIGHTMAP_DIR uniform bakeDir: f32; #endif #ifdef LIT_LIGHTMAP_BAKING_ADD_AMBIENT uniform ambientBakeOcclusionContrast: f32; uniform ambientBakeOcclusionBrightness: f32; #endif `; var litForwardMainPS = ` @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { #include "litUserMainStartPS" dReflection = vec4f(0.0); #ifdef LIT_CLEARCOAT ccSpecularLight = vec3f(0.0); ccReflection = vec3f(0.0); #endif #if LIT_NONE_SLICE_MODE == SLICED #include "startNineSlicedPS" #elif LIT_NONE_SLICE_MODE == TILED #include "startNineSlicedTiledPS" #endif #ifdef LIT_NEEDS_NORMAL dVertexNormalW = normalize(vNormalW); #ifdef LIT_TANGENTS #if defined(LIT_HEIGHTS) || defined(LIT_USE_NORMALS) || defined(LIT_USE_CLEARCOAT_NORMALS) || defined(LIT_GGX_SPECULAR) dTangentW = vTangentW; dBinormalW = vBinormalW; #endif #endif getViewDir(); #ifdef LIT_TBN getTBN(dTangentW, dBinormalW, dVertexNormalW); #ifdef LIT_TWO_SIDED_LIGHTING handleTwoSidedLighting(); #endif #endif #endif evaluateFrontend(); #include "debugProcessFrontendPS" var output: FragmentOutput = evaluateBackend(); #include "litUserMainEndPS" return output; } `; var litForwardPostCodePS = ` #ifdef LIT_NEEDS_NORMAL #include "cubeMapRotatePS" #include "cubeMapProjectPS" #include "envProcPS" #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_METALNESS #include "metalnessModulatePS" #endif #if LIT_FRESNEL_MODEL == SCHLICK #include "fresnelSchlickPS" #endif #ifdef LIT_IRIDESCENCE #include "iridescenceDiffractionPS" #endif #endif #ifdef LIT_AO #include "aoDiffuseOccPS" #include "aoSpecOccPS" #endif #if LIT_REFLECTION_SOURCE == ENVATLASHQ #include "envAtlasPS" #include "reflectionEnvHQPS" #elif LIT_REFLECTION_SOURCE == ENVATLAS #include "envAtlasPS" #include "reflectionEnvPS" #elif LIT_REFLECTION_SOURCE == CUBEMAP #include "reflectionCubePS" #elif LIT_REFLECTION_SOURCE == SPHEREMAP #include "reflectionSpherePS" #endif #ifdef LIT_REFLECTIONS #ifdef LIT_CLEARCOAT #include "reflectionCCPS" #endif #ifdef LIT_SHEEN #include "reflectionSheenPS" #endif #endif #ifdef LIT_REFRACTION #if defined(LIT_DYNAMIC_REFRACTION) #include "refractionDynamicPS" #elif defined(LIT_REFLECTIONS) #include "refractionCubePS" #endif #endif #ifdef LIT_SHEEN #include "lightSheenPS" #endif uniform material_ambient: vec3f; #ifdef LIT_SPECULAR #ifdef LIT_LIGHTING #ifdef LIT_GGX_SPECULAR #ifdef LIT_ANISOTROPY #include "lightSpecularAnisoGGXPS" #else #include "lightSpecularGGXPS" #endif #else #include "lightSpecularBlinnPS" #endif #endif #endif #include "combinePS" #ifdef LIT_LIGHTMAP #include "lightmapAddPS" #endif #ifdef LIT_ADD_AMBIENT #include "ambientPS" #endif #ifdef LIT_MSDF #include "msdfPS" #endif #ifdef LIT_NEEDS_NORMAL #include "viewDirPS" #ifdef LIT_SPECULAR #ifdef LIT_ANISOTROPY #include "reflDirAnisoPS" #else #include "reflDirPS" #endif #endif #endif #include "lightingPS" `; var litForwardPreCodePS = ` #include "basePS" #include "sphericalPS" #include "decodePS" #include "gammaPS" #include "tonemappingPS" #include "fogPS" #if LIT_NONE_SLICE_MODE == SLICED #include "baseNineSlicedPS" #elif LIT_NONE_SLICE_MODE == TILED #include "baseNineSlicedTiledPS" #endif #ifdef LIT_TBN #include "TBNPS" #ifdef LIT_TWO_SIDED_LIGHTING #include "twoSidedLightingPS" #endif #endif `; var litMainPS = ` #include "varyingsPS" #include "litUserDeclarationPS" #include "frontendDeclPS" #if defined(PICK_PASS) || defined(PREPASS_PASS) #include "frontendCodePS" #include "litUserCodePS" #include "litOtherMainPS" #elif defined(SHADOW_PASS) #include "frontendCodePS" #include "litUserCodePS" #include "litShadowMainPS" #else #include "litForwardDeclarationPS" #include "litForwardPreCodePS" #include "frontendCodePS" #include "litForwardPostCodePS" #include "litForwardBackendPS" #include "litUserCodePS" #include "litForwardMainPS" #endif `; var litMainVS = ` #include "varyingsVS" #include "litUserDeclarationVS" #ifdef VERTEX_COLOR attribute vertex_color: vec4f; #endif #ifdef NINESLICED varying vMask: vec2f; varying vTiledUv: vec2f; var dMaskGlobal: vec2f; var dTiledUvGlobal: vec2f; uniform innerOffset: vec4f; uniform outerScale: vec2f; uniform atlasRect: vec4f; #endif var dPositionW: vec3f; var dModelMatrix: mat4x4f; #include "transformCoreVS" #ifdef UV0 attribute vertex_texCoord0: vec2f; #include "uv0VS" #endif #ifdef UV1 attribute vertex_texCoord1: vec2f; #include "uv1VS" #endif #ifdef LINEAR_DEPTH #ifndef VIEWMATRIX #define VIEWMATRIX uniform matrix_view: mat4x4f; #endif #endif #include "transformVS" #ifdef NORMALS #include "normalCoreVS" #include "normalVS" #endif #ifdef TANGENTS attribute vertex_tangent: vec4f; #endif #include "uvTransformUniformsPS, UV_TRANSFORMS_COUNT" #ifdef MSDF #include "msdfVS" #endif #include "litUserCodeVS" #ifdef VERTEX_COLOR fn decodeGamma3(raw: vec3f) -> vec3f { return pow(raw, vec3f(2.2)); } fn gammaCorrectInputVec4(color: vec4f) -> vec4f { return vec4f(decodeGamma3(color.xyz), color.w); } #endif @vertex fn vertexMain(input : VertexInput) -> VertexOutput { #include "litUserMainStartVS" var output : VertexOutput; output.position = getPosition(); output.vPositionW = getWorldPosition(); #ifdef NORMALS output.vNormalW = getNormal(); #endif #ifdef TANGENTS output.vTangentW = normalize(dNormalMatrix * vertex_tangent.xyz); output.vBinormalW = cross(output.vNormalW, output.vTangentW) * vertex_tangent.w; #elif defined(GGX_SPECULAR) output.vObjectSpaceUpW = normalize(dNormalMatrix * vec3f(0.0, 1.0, 0.0)); #endif #ifdef UV0 var uv0: vec2f = getUv0(); #ifdef UV0_UNMODIFIED output.vUv0 = uv0; #endif #endif #ifdef UV1 var uv1: vec2f = getUv1(); #ifdef UV1_UNMODIFIED output.vUv1 = uv1; #endif #endif #include "uvTransformVS, UV_TRANSFORMS_COUNT" #ifdef VERTEX_COLOR #ifdef STD_VERTEX_COLOR_GAMMA output.vVertexColor = gammaCorrectInputVec4(vertex_color); #else output.vVertexColor = vertex_color; #endif #endif #ifdef LINEAR_DEPTH output.vLinearDepth = -(uniform.matrix_view * vec4f(output.vPositionW, 1.0)).z; #endif #ifdef MSDF unpackMsdfParams(); output.outline_color = dOutlineColor; output.outline_thickness = dOutlineThickness; output.shadow_color = dShadowColor; output.shadow_offset = dShadowOffset; #endif #ifdef NINESLICED output.vMask = dMaskGlobal; output.vTiledUv = dTiledUvGlobal; #endif #include "litUserMainEndVS" return output; } `; var litOtherMainPS = ` #ifdef PICK_PASS #include "pickPS" #endif #ifdef PREPASS_PASS #include "floatAsUintPS" #endif @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { #include "litUserMainStartPS" var output: FragmentOutput; evaluateFrontend(); #ifdef PICK_PASS output.color = getPickOutput(); #ifdef DEPTH_PICK_PASS output.color1 = getPickDepth(); #endif #endif #ifdef PREPASS_PASS output.color = float2vec4(vLinearDepth); #endif #include "litUserMainEndPS" return output; } `; var litShaderArgsPS = ` var litArgs_albedo: vec3f; var litArgs_opacity: f32; var litArgs_emission: vec3f; var litArgs_worldNormal: vec3f; var litArgs_ao: f32; var litArgs_lightmap: vec3f; var litArgs_lightmapDir: vec3f; var litArgs_metalness: f32; var litArgs_specularity: vec3f; var litArgs_specularityFactor: f32; var litArgs_gloss: f32; var litArgs_sheen_gloss: f32; var litArgs_sheen_specularity: vec3f; var litArgs_transmission: f32; var litArgs_thickness: f32; var litArgs_ior: f32; var litArgs_dispersion: f32; var litArgs_iridescence_intensity: f32; var litArgs_iridescence_thickness: f32; var litArgs_clearcoat_worldNormal: vec3f; var litArgs_clearcoat_specularity: f32; var litArgs_clearcoat_gloss: f32; `; var litShaderCorePS = ` #if LIT_NONE_SLICE_MODE == TILED var textureBias: f32 = -1000.0; #else uniform textureBias: f32; #endif #include "litShaderArgsPS" `; var litShadowMainPS = ` #if LIGHT_TYPE != DIRECTIONAL uniform view_position: vec3f; uniform light_radius: f32; #endif #if SHADOW_TYPE == PCSS_32F #include "linearizeDepthPS" #endif @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { #include "litUserMainStartPS" var output: FragmentOutput; evaluateFrontend(); #ifdef PERSPECTIVE_DEPTH var depth: f32 = input.position.z; #if SHADOW_TYPE == PCSS_32F #if LIGHT_TYPE != DIRECTIONAL depth = linearizeDepthWithParams(depth, camera_params); #endif #endif #else var depth: f32 = min(distance(uniform.view_position, input.vPositionW) / uniform.light_radius, 0.99999); #define MODIFIED_DEPTH #endif #if SHADOW_TYPE == VSM_16F || SHADOW_TYPE == VSM_32F #if SHADOW_TYPE == VSM_32F var exponent: f32 = 15.0; #else var exponent: f32 = 5.54; #endif var depth_vsm = 2.0 * depth - 1.0; depth_vsm = exp(exponent * depth_vsm); output.color = vec4f(depth_vsm, depth_vsm * depth_vsm, 1.0, 1.0); #else #if SHADOW_TYPE == PCSS_32F output.color = vec4f(depth, 0.0, 0.0, 1.0); #else #ifdef MODIFIED_DEPTH output.fragDepth = depth; #endif output.color = vec4f(1.0); #endif #endif #include "litUserMainEndPS" return output; } `; var ltcPS = ` fn LTC_Uv(N: vec3f, V: vec3f, roughness: f32) -> vec2f { const LUT_SIZE: f32 = 64.0; const LUT_SCALE: f32 = (LUT_SIZE - 1.0) / LUT_SIZE; const LUT_BIAS: f32 = 0.5 / LUT_SIZE; let dotNV: f32 = saturate(dot( N, V )); let uv: vec2f = vec2f( roughness, sqrt( 1.0 - dotNV ) ); return uv * LUT_SCALE + LUT_BIAS; } fn LTC_ClippedSphereFormFactor( f: vec3f ) -> f32 { let l: f32 = length( f ); return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); } fn LTC_EdgeVectorFormFactor( v1: vec3f, v2: vec3f ) -> vec3f { let x: f32 = dot( v1, v2 ); let y: f32 = abs( x ); let a: f32 = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; let b: f32 = 3.4175940 + ( 4.1616724 + y ) * y; let v: f32 = a / b; let inv_sqrt_term = inverseSqrt( max( 1.0 - x * x, 1e-7f ) ); let theta_sintheta: f32 = select( (0.5 * inv_sqrt_term - v), v, x > 0.0 ); return cross( v1, v2 ) * theta_sintheta; } struct Coords { coord0: vec3f, coord1: vec3f, coord2: vec3f, coord3: vec3f, } fn LTC_EvaluateRect( N: vec3f, V: vec3f, P: vec3f, mInv: mat3x3f, rectCoords: Coords) -> f32 { let v1: vec3f = rectCoords.coord1 - rectCoords.coord0; let v2: vec3f = rectCoords.coord3 - rectCoords.coord0; let lightNormal: vec3f = cross( v1, v2 ); let factor: f32 = sign(-dot( lightNormal, P - rectCoords.coord0 )); let T1: vec3f = normalize( V - N * dot( V, N ) ); let T2: vec3f = factor * cross( N, T1 ); let mat: mat3x3f = mInv * transpose( mat3x3f( T1, T2, N ) ); var coords: array; coords[0] = mat * ( rectCoords.coord0 - P ); coords[1] = mat * ( rectCoords.coord1 - P ); coords[2] = mat * ( rectCoords.coord2 - P ); coords[3] = mat * ( rectCoords.coord3 - P ); coords[0] = normalize( coords[0] ); coords[1] = normalize( coords[1] ); coords[2] = normalize( coords[2] ); coords[3] = normalize( coords[3] ); var vectorFormFactor: vec3f = vec3f( 0.0 ); vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[0], coords[1] ); vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[1], coords[2] ); vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[2], coords[3] ); vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[3], coords[0] ); let result: f32 = LTC_ClippedSphereFormFactor( vectorFormFactor ); return result; } var dLTCCoords: Coords; fn getLTCLightCoords(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) -> Coords { var coords: Coords; coords.coord0 = lightPos + halfWidth - halfHeight; coords.coord1 = lightPos - halfWidth - halfHeight; coords.coord2 = lightPos - halfWidth + halfHeight; coords.coord3 = lightPos + halfWidth + halfHeight; return coords; } var dSphereRadius: f32; fn getSphereLightCoords(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) -> Coords { dSphereRadius = max(length(halfWidth), length(halfHeight)); let f: vec3f = reflect(normalize(lightPos - uniform.view_position), vNormalW); let w: vec3f = normalize(cross(f, halfHeight)); let h: vec3f = normalize(cross(f, w)); return getLTCLightCoords(lightPos, w * dSphereRadius, h * dSphereRadius); } var dLTCUV: vec2f; #ifdef LIT_CLEARCOAT var ccLTCUV: vec2f; #endif fn getLTCLightUV(gloss: f32, worldNormal: vec3f, viewDir: vec3f) -> vec2f { let roughness: f32 = max((1.0 - gloss) * (1.0 - gloss), 0.001); return LTC_Uv( worldNormal, viewDir, roughness ); } var dLTCSpecFres: vec3f; #ifdef LIT_CLEARCOAT var ccLTCSpecFres: vec3f; #endif fn getLTCLightSpecFres(uv: vec2f, specularity: vec3f) -> vec3f { let t2: vec4f = textureSampleLevel(areaLightsLutTex2, areaLightsLutTex2Sampler, uv, 0.0); return specularity * t2.x + ( vec3f( 1.0 ) - specularity) * t2.y; } fn calcLTCLightValues(gloss: f32, worldNormal: vec3f, viewDir: vec3f, specularity: vec3f, clearcoatGloss: f32, clearcoatWorldNormal: vec3f, clearcoatSpecularity: f32) { dLTCUV = getLTCLightUV(gloss, worldNormal, viewDir); dLTCSpecFres = getLTCLightSpecFres(dLTCUV, specularity); #ifdef LIT_CLEARCOAT ccLTCUV = getLTCLightUV(clearcoatGloss, clearcoatWorldNormal, viewDir); ccLTCSpecFres = getLTCLightSpecFres(ccLTCUV, vec3f(clearcoatSpecularity)); #endif } fn calcRectLightValues(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) { dLTCCoords = getLTCLightCoords(lightPos, halfWidth, halfHeight); } fn calcDiskLightValues(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) { calcRectLightValues(lightPos, halfWidth, halfHeight); } fn calcSphereLightValues(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) { dLTCCoords = getSphereLightCoords(lightPos, halfWidth, halfHeight); } fn SolveCubic(Coefficient_in: vec4f) -> vec3f { let pi: f32 = 3.14159; var Coefficient = Coefficient_in; Coefficient = vec4f(Coefficient.xyz / Coefficient.w, Coefficient.w); let new_yz: vec2f = Coefficient.yz / 3.0; Coefficient = vec4f(Coefficient.x, new_yz.x, new_yz.y, Coefficient.w); let A: f32 = Coefficient.w; let B: f32 = Coefficient.z; let C: f32 = Coefficient.y; let D: f32 = Coefficient.x; let Delta: vec3f = vec3f( -Coefficient.z * Coefficient.z + Coefficient.y, -Coefficient.y * Coefficient.z + Coefficient.x, dot(vec2f(Coefficient.z, -Coefficient.y), Coefficient.xy) ); let Discriminant: f32 = dot(vec2f(4.0 * Delta.x, -Delta.y), Delta.zy); var xlc: vec2f; var xsc: vec2f; { let A_a: f32 = 1.0; let C_a: f32 = Delta.x; let D_a: f32 = -2.0 * B * Delta.x + Delta.y; let Theta: f32 = atan2(sqrt(Discriminant), -D_a) / 3.0; let sqrt_neg_Ca = sqrt(-C_a); let x_1a: f32 = 2.0 * sqrt_neg_Ca * cos(Theta); let x_3a: f32 = 2.0 * sqrt_neg_Ca * cos(Theta + (2.0 / 3.0) * pi); let xl: f32 = select(x_3a, x_1a, (x_1a + x_3a) > 2.0 * B); xlc = vec2f(xl - B, A); } { let A_d: f32 = D; let C_d: f32 = Delta.z; let D_d: f32 = -D * Delta.y + 2.0 * C * Delta.z; let Theta: f32 = atan2(D * sqrt(Discriminant), -D_d) / 3.0; let sqrt_neg_Cd = sqrt(-C_d); let x_1d: f32 = 2.0 * sqrt_neg_Cd * cos(Theta); let x_3d: f32 = 2.0 * sqrt_neg_Cd * cos(Theta + (2.0 / 3.0) * pi); let xs: f32 = select(x_3d, x_1d, x_1d + x_3d < 2.0 * C); xsc = vec2f(-D, xs + C); } let E: f32 = xlc.y * xsc.y; let F: f32 = -xlc.x * xsc.y - xlc.y * xsc.x; let G: f32 = xlc.x * xsc.x; let xmc: vec2f = vec2f(C * F - B * G, -B * F + C * E); var Root: vec3f = vec3f(xsc.x / xsc.y, xmc.x / xmc.y, xlc.x / xlc.y); if (Root.x < Root.y && Root.x < Root.z) { Root = Root.yxz; } else if (Root.z < Root.x && Root.z < Root.y) { Root = Root.xzy; } return Root; } fn LTC_EvaluateDisk(N: vec3f, V: vec3f, P: vec3f, Minv: mat3x3f, points: Coords) -> f32 { let T1: vec3f = normalize(V - N * dot(V, N)); let T2: vec3f = cross(N, T1); let R: mat3x3f = transpose( mat3x3f( T1, T2, N ) ); var L_: array; L_[0] = R * ( points.coord0 - P ); L_[1] = R * ( points.coord1 - P ); L_[2] = R * ( points.coord2 - P ); let C: vec3f = 0.5 * (L_[0] + L_[2]); var V1: vec3f = 0.5 * (L_[1] - L_[2]); var V2: vec3f = 0.5 * (L_[1] - L_[0]); let C_Minv: vec3f = Minv * C; let V1_Minv: vec3f = Minv * V1; let V2_Minv: vec3f = Minv * V2; var a: f32; var b: f32; let d11: f32 = dot(V1_Minv, V1_Minv); let d22: f32 = dot(V2_Minv, V2_Minv); let d12: f32 = dot(V1_Minv, V2_Minv); if (abs(d12) / sqrt(d11 * d22) > 0.0001) { let tr: f32 = d11 + d22; let det_inner: f32 = -d12 * d12 + d11 * d22; let det: f32 = sqrt(det_inner); let u: f32 = 0.5 * sqrt(tr - 2.0 * det); let v: f32 = 0.5 * sqrt(tr + 2.0 * det); let e_max: f32 = (u + v) * (u + v); let e_min: f32 = (u - v) * (u - v); var V1_: vec3f; var V2_: vec3f; if (d11 > d22) { V1_ = d12 * V1_Minv + (e_max - d11) * V2_Minv; V2_ = d12 * V1_Minv + (e_min - d11) * V2_Minv; } else { V1_ = d12*V2_Minv + (e_max - d22)*V1_Minv; V2_ = d12*V2_Minv + (e_min - d22)*V1_Minv; } a = 1.0 / e_max; b = 1.0 / e_min; V1 = normalize(V1_); V2 = normalize(V2_); } else { a = 1.0 / dot(V1_Minv, V1_Minv); b = 1.0 / dot(V2_Minv, V2_Minv); V1 = V1_Minv * sqrt(a); V2 = V2_Minv * sqrt(b); } var V3: vec3f = normalize(cross(V1, V2)); if (dot(C_Minv, V3) < 0.0) { V3 = V3 * -1.0; } let L: f32 = dot(V3, C_Minv); let x0: f32 = dot(V1, C_Minv) / L; let y0: f32 = dot(V2, C_Minv) / L; let E1: f32 = inverseSqrt(a); let E2: f32 = inverseSqrt(b); let a_scaled = a * L * L; let b_scaled = b * L * L; let c0: f32 = a_scaled * b_scaled; let c1: f32 = a_scaled * b_scaled * (1.0 + x0 * x0 + y0 * y0) - a_scaled - b_scaled; let c2: f32 = 1.0 - a_scaled * (1.0 + x0 * x0) - b_scaled * (1.0 + y0 * y0); let c3: f32 = 1.0; let roots: vec3f = SolveCubic(vec4f(c0, c1, c2, c3)); let e1: f32 = roots.x; let e2: f32 = roots.y; let e3: f32 = roots.z; var avgDir: vec3f = vec3f(a_scaled * x0 / (a_scaled - e2), b_scaled * y0 / (b_scaled - e2), 1.0); let rotate: mat3x3f = mat3x3f(V1, V2, V3); avgDir = rotate * avgDir; avgDir = normalize(avgDir); let L1: f32 = sqrt(-e2 / e3); let L2: f32 = sqrt(-e2 / e1); let formFactor: f32 = max(0.0, L1 * L2 * inverseSqrt((1.0 + L1 * L1) * (1.0 + L2 * L2))); const LUT_SIZE_disk: f32 = 64.0; const LUT_SCALE_disk: f32 = ( LUT_SIZE_disk - 1.0 ) / LUT_SIZE_disk; const LUT_BIAS_disk: f32 = 0.5 / LUT_SIZE_disk; var uv: vec2f = vec2f(avgDir.z * 0.5 + 0.5, formFactor); uv = uv * LUT_SCALE_disk + LUT_BIAS_disk; let scale: f32 = textureSampleLevel(areaLightsLutTex2, areaLightsLutTex2Sampler, uv, 0.0).w; return formFactor * scale; } fn FixNan(value: f32) -> f32 { return select(value, 0.0, value != value); } fn getRectLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDir: vec3f, lightDirNorm: vec3f) -> f32 { let identityMat = mat3x3f(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0)); return LTC_EvaluateRect( worldNormal, viewDir, vPositionW, identityMat, dLTCCoords ); } fn getDiskLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDir: vec3f, lightDirNorm: vec3f) -> f32 { let identityMat = mat3x3f(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0)); return FixNan(LTC_EvaluateDisk( worldNormal, viewDir, vPositionW, identityMat, dLTCCoords )); } fn getSphereLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDir: vec3f, lightDirNorm: vec3f) -> f32 { let falloff: f32 = dSphereRadius / (dot(lightDir, lightDir) + dSphereRadius); return FixNan(getLightDiffuse(worldNormal, viewDir, lightDirNorm) * falloff); } fn getLTCLightInvMat(uv: vec2f) -> mat3x3f { let t1: vec4f = textureSampleLevel(areaLightsLutTex1, areaLightsLutTex1Sampler, uv, 0.0); return mat3x3f( vec3f( t1.x, 0.0, t1.y ), vec3f( 0.0, 1.0, 0.0 ), vec3f( t1.z, 0.0, t1.w ) ); } fn calcRectLightSpecular(worldNormal: vec3f, viewDir: vec3f, uv: vec2f) -> f32 { let mInv: mat3x3f = getLTCLightInvMat(uv); return LTC_EvaluateRect( worldNormal, viewDir, vPositionW, mInv, dLTCCoords ); } fn getRectLightSpecular(worldNormal: vec3f, viewDir: vec3f) -> f32 { return calcRectLightSpecular(worldNormal, viewDir, dLTCUV); } fn calcDiskLightSpecular(worldNormal: vec3f, viewDir: vec3f, uv: vec2f) -> f32 { let mInv: mat3x3f = getLTCLightInvMat(uv); return LTC_EvaluateDisk( worldNormal, viewDir, vPositionW, mInv, dLTCCoords ); } fn getDiskLightSpecular(worldNormal: vec3f, viewDir: vec3f) -> f32 { return calcDiskLightSpecular(worldNormal, viewDir, dLTCUV); } fn getSphereLightSpecular(worldNormal: vec3f, viewDir: vec3f) -> f32 { return calcDiskLightSpecular(worldNormal, viewDir, dLTCUV); } `; var metalnessPS = ` #ifdef STD_METALNESS_CONSTANT uniform material_metalness: f32; #endif fn getMetalness() { var metalness: f32 = 1.0; #ifdef STD_METALNESS_CONSTANT metalness = metalness * uniform.material_metalness; #endif #ifdef STD_METALNESS_TEXTURE metalness = metalness * textureSampleBias({STD_METALNESS_TEXTURE_NAME}, {STD_METALNESS_TEXTURE_NAME}Sampler, {STD_METALNESS_TEXTURE_UV}, uniform.textureBias).{STD_METALNESS_TEXTURE_CHANNEL}; #endif #ifdef STD_METALNESS_VERTEX metalness = metalness * saturate(vVertexColor.{STD_METALNESS_VERTEX_CHANNEL}); #endif dMetalness = metalness; } `; var msdfPS = ` var texture_msdfMap: texture_2d; var texture_msdfMapSampler: sampler; fn median(r: f32, g: f32, b: f32) -> f32 { return max(min(r, g), min(max(r, g), b)); } fn map(min: f32, max: f32, v: f32) -> f32 { return (v - min) / (max - min); } uniform font_sdfIntensity: f32; uniform font_pxrange: f32; uniform font_textureWidth: f32; #ifndef LIT_MSDF_TEXT_ATTRIBUTE uniform outline_color: vec4f; uniform outline_thickness: f32; uniform shadow_color: vec4f; uniform shadow_offset: vec2f; #else varying outline_color: vec4f; varying outline_thickness: f32; varying shadow_color: vec4f; varying shadow_offset: vec2f; #endif fn applyMsdf(color_in: vec4f) -> vec4f { #ifndef LIT_MSDF_TEXT_ATTRIBUTE var outline_colorValue = uniform.outline_color; var outline_thicknessValue = uniform.outline_thickness; var shadow_colorValue = uniform.shadow_color; var shadow_offsetValue = uniform.shadow_offset; #else var outline_colorValue = outline_color; var outline_thicknessValue = outline_thickness; var shadow_colorValue = shadow_color; var shadow_offsetValue = shadow_offset; #endif var color = vec4f(gammaCorrectInputVec3(color_in.rgb), color_in.a); let tsample: vec3f = textureSample(texture_msdfMap, texture_msdfMapSampler, vUv0).rgb; let uvShdw: vec2f = vUv0 - shadow_offsetValue; let ssample: vec3f = textureSample(texture_msdfMap, texture_msdfMapSampler, uvShdw).rgb; let sigDist: f32 = median(tsample.r, tsample.g, tsample.b); var sigDistShdw: f32 = median(ssample.r, ssample.g, ssample.b); let smoothingMax: f32 = 0.2; let w: vec2f = abs(dpdx(vUv0)) + abs(dpdy(vUv0)); let smoothing: f32 = clamp(w.x * uniform.font_textureWidth / uniform.font_pxrange, 0.0, smoothingMax); let mapMin: f32 = 0.05; let mapMax: f32 = clamp(1.0 - uniform.font_sdfIntensity, mapMin, 1.0); let sigDistInner: f32 = map(mapMin, mapMax, sigDist); let sigDistOutline: f32 = map(mapMin, mapMax, sigDist + outline_thicknessValue); sigDistShdw = map(mapMin, mapMax, sigDistShdw + outline_thicknessValue); let center: f32 = 0.5; let inside: f32 = smoothstep(center - smoothing, center + smoothing, sigDistInner); let outline: f32 = smoothstep(center - smoothing, center + smoothing, sigDistOutline); let shadow: f32 = smoothstep(center - smoothing, center + smoothing, sigDistShdw); let tcolor_outline: vec4f = outline * vec4f(outline_colorValue.a * outline_colorValue.rgb, outline_colorValue.a); var tcolor: vec4f = select(vec4f(0.0), tcolor_outline, outline > inside); tcolor = mix(tcolor, color, inside); let scolor_shadow: vec4f = shadow * vec4f(shadow_colorValue.a * shadow_colorValue.rgb, shadow_colorValue.a); let scolor: vec4f = select(tcolor, scolor_shadow, shadow > outline); tcolor = mix(scolor, tcolor, outline); tcolor = vec4f(gammaCorrectOutput(tcolor.rgb), tcolor.a); return tcolor; } `; var metalnessModulatePS = ` fn getSpecularModulate(specularity: vec3f, albedo: vec3f, metalness: f32, f0: f32, specularityFactor: f32) -> vec3f { let dielectricF0: vec3f = f0 * specularity * specularityFactor; return mix(dielectricF0, albedo, metalness); } fn getAlbedoModulate(albedo: vec3f, metalness: f32) -> vec3f { return albedo * (1.0 - metalness); } `; var morphPS = ` varying uv0: vec2f; var morphTexture: texture_2d_array; uniform morphFactor: array; uniform morphIndex: array; uniform count: u32; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var color = vec3f(0, 0, 0); let textureDims = textureDimensions(morphTexture); let pixelCoords = vec2i(input.uv0 * vec2f(textureDims)); for (var i: u32 = 0; i < uniform.count; i = i + 1) { var textureIndex: u32 = uniform.morphIndex[i].element; var delta = textureLoad(morphTexture, pixelCoords, textureIndex, 0).xyz; color += uniform.morphFactor[i].element * delta; } var output: FragmentOutput; output.color = vec4f(color, 1.0); return output; } `; var morphVS = ` attribute vertex_position: vec2f; varying uv0: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(input.vertex_position, 0.5, 1.0); output.uv0 = input.vertex_position * 0.5 + vec2f(0.5, 0.5); return output; } `; var msdfVS = ` attribute vertex_outlineParameters: vec3f; attribute vertex_shadowParameters: vec3f; varying outline_color: vec4f; varying outline_thickness: f32; varying shadow_color: vec4f; varying shadow_offset: vec2f; var dOutlineColor: vec4f; var dOutlineThickness: f32; var dShadowColor: vec4f; var dShadowOffset: vec2f; fn unpackMsdfParams() { let little: vec3f = vertex_outlineParameters % vec3f(256.0); let big: vec3f = (vertex_outlineParameters - little) / 256.0; dOutlineColor = vec4f(little.x, big.x, little.y, big.y) / 255.0; dOutlineThickness = little.z / 255.0 * 0.2; let little_shadow = vertex_shadowParameters % vec3f(256.0); let big_shadow = (vertex_shadowParameters - little_shadow) / 256.0; dShadowColor = vec4f(little_shadow.x, big_shadow.x, little_shadow.y, big_shadow.y) / 255.0; dShadowOffset = (vec2f(little_shadow.z, big_shadow.z) / 127.0 - 1.0) * 0.005; } `; var normalVS = ` var dNormalMatrix: mat3x3f; fn getNormal() -> vec3f { dNormalMatrix = getNormalMatrix(dModelMatrix); let localNormal: vec3f = getLocalNormal(vertex_normal); return normalize(dNormalMatrix * localNormal); }`; var normalCoreVS = ` attribute vertex_normal: vec3f; uniform matrix_normal: mat3x3f; #ifdef MORPHING_NORMAL #ifdef MORPHING_INT var morphNormalTex: texture_2d; var morphNormalTexSampler: sampler; #else var morphNormalTex: texture_2d; var morphNormalTexSampler: sampler; #endif #endif fn getLocalNormal(vertexNormal: vec3f) -> vec3f { var localNormal: vec3f = vertexNormal; #ifdef MORPHING_NORMAL let morphUV: vec2i = getTextureMorphCoords(); #ifdef MORPHING_INT let morphNormalInt: vec4u = textureLoad(morphNormalTex, morphUV, 0); let morphNormalF: vec3f = vec3f(morphNormalInt.xyz) / 65535.0 * 2.0 - 1.0; localNormal = localNormal + morphNormalF; #else let morphNormal: vec3f = textureLoad(morphNormalTex, morphUV, 0).xyz; localNormal = localNormal + morphNormal; #endif #endif return localNormal; } #if defined(SKIN) || defined(BATCH) fn getNormalMatrix(modelMatrix: mat4x4f) -> mat3x3f { return mat3x3f(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz); } #elif defined(INSTANCING) fn getNormalMatrix(modelMatrix: mat4x4f) -> mat3x3f { return mat3x3f(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz); } #else fn getNormalMatrix(modelMatrix: mat4x4f) -> mat3x3f { return uniform.matrix_normal; } #endif `; var normalMapPS = ` #ifdef STD_NORMAL_TEXTURE uniform material_bumpiness: f32; #endif #ifdef STD_NORMALDETAIL_TEXTURE uniform material_normalDetailMapBumpiness: f32; fn blendNormals(inN1: vec3f, inN2: vec3f) -> vec3f { let n1: vec3f = inN1 + vec3f(0.0, 0.0, 1.0); let n2: vec3f = inN2 * vec3f(-1.0, -1.0, 1.0); return n1 * dot(n1, n2) / n1.z - n2; } #endif fn getNormal() { #ifdef STD_NORMAL_TEXTURE var normalMap: vec3f = {STD_NORMAL_TEXTURE_DECODE}(textureSampleBias({STD_NORMAL_TEXTURE_NAME}, {STD_NORMAL_TEXTURE_NAME}Sampler, {STD_NORMAL_TEXTURE_UV}, uniform.textureBias)); normalMap = mix(vec3f(0.0, 0.0, 1.0), normalMap, uniform.material_bumpiness); #ifdef STD_NORMALDETAIL_TEXTURE var normalDetailMap: vec3f = {STD_NORMALDETAIL_TEXTURE_DECODE}(textureSampleBias({STD_NORMALDETAIL_TEXTURE_NAME}, {STD_NORMALDETAIL_TEXTURE_NAME}Sampler, {STD_NORMALDETAIL_TEXTURE_UV}, uniform.textureBias)); normalDetailMap = mix(vec3f(0.0, 0.0, 1.0), normalDetailMap, uniform.material_normalDetailMapBumpiness); normalMap = blendNormals(normalMap, normalDetailMap); #endif dNormalW = normalize(dTBN * normalMap); #else dNormalW = dVertexNormalW; #endif } `; var opacityPS = ` uniform material_opacity: f32; fn getOpacity() { dAlpha = uniform.material_opacity; #ifdef STD_OPACITY_TEXTURE dAlpha = dAlpha * textureSampleBias({STD_OPACITY_TEXTURE_NAME}, {STD_OPACITY_TEXTURE_NAME}Sampler, {STD_OPACITY_TEXTURE_UV}, uniform.textureBias).{STD_OPACITY_TEXTURE_CHANNEL}; #endif #ifdef STD_OPACITY_VERTEX dAlpha = dAlpha * clamp(vVertexColor.{STD_OPACITY_VERTEX_CHANNEL}, 0.0, 1.0); #endif } `; var opacityDitherPS = ` #if STD_OPACITY_DITHER == BAYER8 #include "bayerPS" #endif uniform blueNoiseJitter: vec4f; #if STD_OPACITY_DITHER == BLUENOISE var blueNoiseTex32 : texture_2d; var blueNoiseTex32Sampler : sampler; #endif fn opacityDither(alpha: f32, id: f32) { #if STD_OPACITY_DITHER == BAYER8 var noise: f32 = bayer8(floor((pcPosition.xy + uniform.blueNoiseJitter.xy + id) % vec2f(8.0))) / 64.0; #else #if STD_OPACITY_DITHER == BLUENOISE var uv = fract(pcPosition.xy / 32.0 + uniform.blueNoiseJitter.xy + id); var noise: f32 = textureSampleLevel(blueNoiseTex32, blueNoiseTex32Sampler, uv, 0.0).y; #endif #if STD_OPACITY_DITHER == IGNNOISE var magic = vec3f(0.06711056, 0.00583715, 52.9829189); var noise: f32 = fract(magic.z * fract(dot(pcPosition.xy + uniform.blueNoiseJitter.xy + id, magic.xy))); #endif #endif noise = pow(noise, 2.2); if (alpha < noise) { discard; } } `; var outputPS = ` `; var outputAlphaPS = ` #if LIT_BLEND_TYPE == NORMAL || LIT_BLEND_TYPE == ADDITIVEALPHA || defined(LIT_ALPHA_TO_COVERAGE) output.color = vec4f(output.color.rgb, litArgs_opacity); #elif LIT_BLEND_TYPE == PREMULTIPLIED output.color = vec4f(output.color.rgb * litArgs_opacity, litArgs_opacity); #else output.color = vec4f(output.color.rgb, 1.0); #endif `; var outputTex2DPS = ` varying vUv0: vec2f; var source: texture_2d; var sourceSampler: sampler; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; output.color = textureSample(source, sourceSampler, input.vUv0); return output; } `; var sheenPS = ` uniform material_sheen: vec3f; fn getSheen() { var sheenColor = uniform.material_sheen; #ifdef STD_SHEEN_TEXTURE sheenColor = sheenColor * {STD_SHEEN_TEXTURE_DECODE}(textureSampleBias({STD_SHEEN_TEXTURE_NAME}, {STD_SHEEN_TEXTURE_NAME}Sampler, {STD_SHEEN_TEXTURE_UV}, uniform.textureBias)).{STD_SHEEN_TEXTURE_CHANNEL}; #endif #ifdef STD_SHEEN_VERTEX sheenColor = sheenColor * saturate3(vVertexColor.{STD_SHEEN_VERTEX_CHANNEL}); #endif sSpecularity = sheenColor; } `; var sheenGlossPS = ` uniform material_sheenGloss: f32; fn getSheenGlossiness() { var sheenGlossiness = uniform.material_sheenGloss; #ifdef STD_SHEENGLOSS_TEXTURE sheenGlossiness = sheenGlossiness * textureSampleBias({STD_SHEENGLOSS_TEXTURE_NAME}, {STD_SHEENGLOSS_TEXTURE_NAME}Sampler, {STD_SHEENGLOSS_TEXTURE_UV}, uniform.textureBias).{STD_SHEENGLOSS_TEXTURE_CHANNEL}; #endif #ifdef STD_SHEENGLOSS_VERTEX sheenGlossiness = sheenGlossiness * saturate(vVertexColor.{STD_SHEENGLOSS_VERTEX_CHANNEL}); #endif #ifdef STD_SHEENGLOSS_INVERT sheenGlossiness = 1.0 - sheenGlossiness; #endif sGlossiness = sheenGlossiness + 0.0000001; } `; var parallaxPS = ` uniform material_heightMapFactor: f32; fn getParallax() { var parallaxScale = uniform.material_heightMapFactor; var height: f32 = textureSampleBias({STD_HEIGHT_TEXTURE_NAME}, {STD_HEIGHT_TEXTURE_NAME}Sampler, {STD_HEIGHT_TEXTURE_UV}, uniform.textureBias).{STD_HEIGHT_TEXTURE_CHANNEL}; height = height * parallaxScale - parallaxScale * 0.5; var viewDirT: vec3f = dViewDirW * dTBN; viewDirT.z = viewDirT.z + 0.42; dUvOffset = height * (viewDirT.xy / viewDirT.z); } `; var pickPS = ` uniform meshInstanceId: u32; fn getPickOutput() -> vec4f { let inv: vec4f = vec4f(1.0 / 255.0); let shifts: vec4u = vec4u(16u, 8u, 0u, 24u); let col: vec4u = (vec4u(uniform.meshInstanceId) >> shifts) & vec4u(0xffu); return vec4f(col) * inv; } #ifdef DEPTH_PICK_PASS #include "floatAsUintPS" fn getPickDepth() -> vec4f { return float2uint(pcPosition.z); } #endif `; var reflDirPS = ` fn getReflDir(worldNormal: vec3f, viewDir: vec3f, gloss: f32, tbn: mat3x3f) { dReflDirW = normalize(-reflect(viewDir, worldNormal)); } `; var reflDirAnisoPS = ` fn getReflDir(worldNormal: vec3f, viewDir: vec3f, gloss: f32, tbn: mat3x3f) { let roughness: f32 = sqrt(1.0 - min(gloss, 1.0)); let direction: vec2f = dAnisotropyRotation; let anisotropicT: vec3f = normalize(tbn * vec3f(direction, 0.0)); let anisotropicB: vec3f = normalize(cross(tbn[2], anisotropicT)); let anisotropy: f32 = dAnisotropy; let anisotropicDirection: vec3f = anisotropicB; let anisotropicTangent: vec3f = cross(anisotropicDirection, viewDir); let anisotropicNormal: vec3f = cross(anisotropicTangent, anisotropicDirection); let bendFactor: f32 = 1.0 - anisotropy * (1.0 - roughness); let bendFactor4: f32 = bendFactor * bendFactor * bendFactor * bendFactor; let bentNormal: vec3f = normalize(mix(normalize(anisotropicNormal), normalize(worldNormal), bendFactor4)); dReflDirW = reflect(-viewDir, bentNormal); }`; var reflectionCCPS = ` #ifdef LIT_CLEARCOAT fn addReflectionCC(reflDir: vec3f, gloss: f32) { ccReflection = ccReflection + calcReflection(reflDir, gloss); } #endif `; var reflectionCubePS = ` var texture_cubeMap: texture_cube; var texture_cubeMapSampler: sampler; uniform material_reflectivity: f32; fn calcReflection(reflDir: vec3f, gloss: f32) -> vec3f { var lookupVec: vec3f = cubeMapProject(reflDir); lookupVec.x = lookupVec.x * -1.0; return {reflectionDecode}(textureSample(texture_cubeMap, texture_cubeMapSampler, lookupVec)); } fn addReflection(reflDir: vec3f, gloss: f32) { dReflection = dReflection + vec4f(calcReflection(reflDir, gloss), uniform.material_reflectivity); } `; var reflectionEnvHQPS = ` #ifndef ENV_ATLAS #define ENV_ATLAS var texture_envAtlas: texture_2d; var texture_envAtlasSampler: sampler; #endif var texture_cubeMap: texture_cube; var texture_cubeMapSampler: sampler; uniform material_reflectivity: f32; fn calcReflection(reflDir: vec3f, gloss: f32) -> vec3f { let dir: vec3f = cubeMapProject(reflDir) * vec3f(-1.0, 1.0, 1.0); let uv: vec2f = toSphericalUv(dir); let level: f32 = saturate(1.0 - gloss) * 5.0; let ilevel: f32 = floor(level); let flevel: f32 = level - ilevel; let sharp: vec3f = {reflectionCubemapDecode}(textureSample(texture_cubeMap, texture_cubeMapSampler, dir)); let roughA: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, mapRoughnessUv(uv, ilevel))); let roughB: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, mapRoughnessUv(uv, ilevel + 1.0))); return processEnvironment(mix(sharp, mix(roughA, roughB, flevel), min(level, 1.0))); } fn addReflection(reflDir: vec3f, gloss: f32) { dReflection = dReflection + vec4f(calcReflection(reflDir, gloss), uniform.material_reflectivity); } `; var reflectionEnvPS = ` #ifndef ENV_ATLAS #define ENV_ATLAS var texture_envAtlas: texture_2d; var texture_envAtlasSampler: sampler; #endif uniform material_reflectivity: f32; fn shinyMipLevel(uv: vec2f) -> f32 { let dx: vec2f = dpdx(uv); let dy: vec2f = dpdy(uv); let uv2: vec2f = vec2f(fract(uv.x + 0.5), uv.y); let dx2: vec2f = dpdx(uv2); let dy2: vec2f = dpdy(uv2); let maxd: f32 = min(max(dot(dx, dx), dot(dy, dy)), max(dot(dx2, dx2), dot(dy2, dy2))); return clamp(0.5 * log2(maxd) - 1.0 + uniform.textureBias, 0.0, 5.0); } fn calcReflection(reflDir: vec3f, gloss: f32) -> vec3f { let dir: vec3f = cubeMapProject(reflDir) * vec3f(-1.0, 1.0, 1.0); let uv: vec2f = toSphericalUv(dir); let level: f32 = saturate(1.0 - gloss) * 5.0; let ilevel: f32 = floor(level); let level2: f32 = shinyMipLevel(uv * atlasSize); let ilevel2: f32 = floor(level2); var uv0: vec2f; var uv1: vec2f; var weight: f32; if (ilevel == 0.0) { uv0 = mapShinyUv(uv, ilevel2); uv1 = mapShinyUv(uv, ilevel2 + 1.0); weight = level2 - ilevel2; } else { uv0 = mapRoughnessUv(uv, ilevel); uv1 = uv0; weight = 0.0; } let linearA: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, uv0)); let linearB: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, uv1)); let linear0: vec3f = mix(linearA, linearB, weight); let linear1: vec3f = {reflectionDecode}(textureSample(texture_envAtlas, texture_envAtlasSampler, mapRoughnessUv(uv, ilevel + 1.0))); return processEnvironment(mix(linear0, linear1, level - ilevel)); } fn addReflection(reflDir: vec3f, gloss: f32) { dReflection = dReflection + vec4f(calcReflection(reflDir, gloss), uniform.material_reflectivity); } `; var reflectionSpherePS = ` #ifndef VIEWMATRIX #define VIEWMATRIX uniform matrix_view: mat4x4f; #endif var texture_sphereMap: texture_2d; var texture_sphereMapSampler: sampler; uniform material_reflectivity: f32; fn calcReflection(reflDir: vec3f, gloss: f32) -> vec3f { let viewRotationMatrix = mat3x3f(uniform.matrix_view[0].xyz, uniform.matrix_view[1].xyz, uniform.matrix_view[2].xyz); let reflDirV: vec3f = viewRotationMatrix * reflDir; let m: f32 = 2.0 * sqrt(dot(reflDirV.xy, reflDirV.xy) + (reflDirV.z + 1.0) * (reflDirV.z + 1.0)); let sphereMapUv: vec2f = reflDirV.xy / m + 0.5; return {reflectionDecode}(textureSample(texture_sphereMap, texture_sphereMapSampler, sphereMapUv)); } fn addReflection(reflDir: vec3f, gloss: f32) { dReflection = dReflection + vec4f(calcReflection(reflDir, gloss), uniform.material_reflectivity); } `; var reflectionSheenPS = ` fn addReflectionSheen(worldNormal: vec3f, viewDir: vec3f, gloss: f32) { let NoV: f32 = dot(worldNormal, viewDir); let alphaG: f32 = gloss * gloss; let a: f32 = select( -8.48 * alphaG + 14.3 * gloss - 9.95, -339.2 * alphaG + 161.4 * gloss - 25.9, gloss < 0.25 ); let b: f32 = select( 1.97 * alphaG - 3.27 * gloss + 0.72, 44.0 * alphaG - 23.7 * gloss + 3.26, gloss < 0.25 ); let dg_add: f32 = select( 0.1 * ( gloss - 0.25 ), 0.0, gloss < 0.25 ); let dg: f32 = exp( a * NoV + b ) + dg_add; sReflection = sReflection + (calcReflection(worldNormal, 0.0) * saturate(dg)); }`; var refractionCubePS = ` fn refract2(viewVec: vec3f, normal: vec3f, IOR: f32) -> vec3f { let vn: f32 = dot(viewVec, normal); let k: f32 = 1.0 - IOR * IOR * (1.0 - vn * vn); let refrVec: vec3f = IOR * viewVec - (IOR * vn + sqrt(k)) * normal; return refrVec; } fn addRefraction( worldNormal: vec3f, viewDir: vec3f, thickness: f32, gloss: f32, specularity: vec3f, albedo: vec3f, transmission: f32, refractionIndex: f32, dispersion: f32 #if defined(LIT_IRIDESCENCE) , iridescenceFresnel: vec3f, iridescenceIntensity: f32 #endif ) { let tmpRefl: vec4f = dReflection; let reflectionDir: vec3f = refract2(-viewDir, worldNormal, refractionIndex); dReflection = vec4f(0.0); addReflection(reflectionDir, gloss); dDiffuseLight = mix(dDiffuseLight, dReflection.rgb * albedo, transmission); dReflection = tmpRefl; } `; var refractionDynamicPS = ` uniform material_invAttenuationDistance: f32; uniform material_attenuation: vec3f; fn evalRefractionColor(refractionVector: vec3f, gloss: f32, refractionIndex: f32) -> vec3f { let pointOfRefraction: vec4f = vec4f(vPositionW + refractionVector, 1.0); let projectionPoint: vec4f = uniform.matrix_viewProjection * pointOfRefraction; let uv: vec2f = getGrabScreenPos(projectionPoint); let iorToRoughness: f32 = (1.0 - gloss) * clamp((1.0 / refractionIndex) * 2.0 - 2.0, 0.0, 1.0); let refractionLod: f32 = log2(uniform.uScreenSize.x) * iorToRoughness; var refraction: vec3f = textureSampleLevel(uSceneColorMap, uSceneColorMapSampler, uv, refractionLod).rgb; #ifdef SCENE_COLORMAP_GAMMA refraction = decodeGamma3(refraction); #endif return refraction; } fn addRefraction( worldNormal: vec3f, viewDir: vec3f, thickness: f32, gloss: f32, specularity: vec3f, albedo: vec3f, transmission: f32, refractionIndex: f32, dispersion: f32, #if defined(LIT_IRIDESCENCE) iridescenceFresnel: vec3f, iridescenceIntensity: f32 #endif ) { var modelScale: vec3f; modelScale.x = length(uniform.matrix_model[0].xyz); modelScale.y = length(uniform.matrix_model[1].xyz); modelScale.z = length(uniform.matrix_model[2].xyz); let scale: vec3f = thickness * modelScale; var refractionVector = normalize(refract(-viewDir, worldNormal, refractionIndex)) * scale; var refraction = evalRefractionColor(refractionVector, gloss, refractionIndex); #ifdef LIT_DISPERSION let halfSpread: f32 = (1.0 / refractionIndex - 1.0) * 0.025 * dispersion; let refractionIndexR: f32 = refractionIndex - halfSpread; refractionVector = normalize(refract(-viewDir, worldNormal, refractionIndexR)) * scale; refraction.r = evalRefractionColor(refractionVector, gloss, refractionIndexR).r; let refractionIndexB: f32 = refractionIndex + halfSpread; refractionVector = normalize(refract(-viewDir, worldNormal, refractionIndexB)) * scale; refraction.b = evalRefractionColor(refractionVector, gloss, refractionIndexB).b; #endif var transmittance: vec3f; if (uniform.material_invAttenuationDistance != 0.0) { let attenuation: vec3f = -log(uniform.material_attenuation) * uniform.material_invAttenuationDistance; transmittance = exp(-attenuation * length(refractionVector)); } else { transmittance = vec3f(1.0); } let fresnel: vec3f = vec3f(1.0) - getFresnel( dot(viewDir, worldNormal), gloss, specularity #if defined(LIT_IRIDESCENCE) , iridescenceFresnel, iridescenceIntensity #endif ); dDiffuseLight = mix(dDiffuseLight, refraction * transmittance * fresnel, transmission); } `; var reprojectPS = ` varying vUv0: vec2f; #ifdef CUBEMAP_SOURCE var sourceCube: texture_cube; var sourceCubeSampler : sampler; #else var sourceTex: texture_2d; var sourceTexSampler : sampler; #endif #ifdef USE_SAMPLES_TEX var samplesTex: texture_2d; var samplesTexSampler : sampler; uniform samplesTexInverseSize: vec2f; #endif uniform params: vec3f; fn targetFace() -> f32 { return uniform.params.x; } fn targetTotalPixels() -> f32 { return uniform.params.y; } fn sourceTotalPixels() -> f32 { return uniform.params.z; } const PI: f32 = 3.141592653589793; fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } #include "decodePS" #include "encodePS" fn modifySeams(dir: vec3f, scale: f32) -> vec3f { let adir = abs(dir); let M = max(max(adir.x, adir.y), adir.z); return dir / M * vec3f( select(scale, 1.0, adir.x == M), select(scale, 1.0, adir.y == M), select(scale, 1.0, adir.z == M) ); } fn toSpherical(dir: vec3f) -> vec2f { let nonZeroXZ = any(dir.xz != vec2f(0.0, 0.0)); return vec2f(select(0.0, atan2(dir.x, dir.z), nonZeroXZ), asin(dir.y)); } fn fromSpherical(uv: vec2f) -> vec3f { return vec3f(cos(uv.y) * sin(uv.x), sin(uv.y), cos(uv.y) * cos(uv.x)); } fn getDirectionEquirect(uv: vec2f) -> vec3f { return fromSpherical((vec2f(uv.x, 1.0 - uv.y) * 2.0 - 1.0) * vec2f(PI, PI * 0.5)); } fn signNotZero(k: f32) -> f32 { return select(-1.0, 1.0, k >= 0.0); } fn signNotZeroVec2(v: vec2f) -> vec2f { return vec2f(signNotZero(v.x), signNotZero(v.y)); } fn octDecode(o: vec2f) -> vec3f { var v = vec3f(o.x, 1.0 - abs(o.x) - abs(o.y), o.y); if (v.y < 0.0) { var temp: vec2f = (1.0 - abs(v.zx)) * signNotZeroVec2(v.xz); v = vec3f(temp.x, v.y, temp.y); } return normalize(v); } fn getDirectionOctahedral(uv: vec2f) -> vec3f { return octDecode(vec2f(uv.x, 1.0 - uv.y) * 2.0 - 1.0); } fn octEncode(v: vec3f) -> vec2f { let l1norm = abs(v.x) + abs(v.y) + abs(v.z); var result = v.xz * (1.0 / l1norm); if (v.y < 0.0) { result = (1.0 - abs(result.yx)) * signNotZeroVec2(result.xy); } return result; } #ifdef CUBEMAP_SOURCE fn sampleCubemapDir(dir: vec3f) -> vec4f { return textureSample(sourceCube, sourceCubeSampler, modifySeams(dir, 1.0)); } fn sampleCubemapSph(sph: vec2f) -> vec4f { return sampleCubemapDir(fromSpherical(sph)); } fn sampleCubemapDirLod(dir: vec3f, mipLevel: f32) -> vec4f { return textureSampleLevel(sourceCube, sourceCubeSampler, modifySeams(dir, 1.0), mipLevel); } fn sampleCubemapSphLod(sph: vec2f, mipLevel: f32) -> vec4f { return sampleCubemapDirLod(fromSpherical(sph), mipLevel); } #else fn sampleEquirectSph(sph: vec2f) -> vec4f { let uv = sph / vec2f(PI * 2.0, PI) + 0.5; return textureSample(sourceTex, sourceTexSampler, vec2f(uv.x, 1.0 - uv.y)); } fn sampleEquirectDir(dir: vec3f) -> vec4f { return sampleEquirectSph(toSpherical(dir)); } fn sampleEquirectSphLod(sph: vec2f, mipLevel: f32) -> vec4f { let uv = sph / vec2f(PI * 2.0, PI) + 0.5; return textureSampleLevel(sourceTex, sourceTexSampler, vec2f(uv.x, 1.0 - uv.y), mipLevel); } fn sampleEquirectDirLod(dir: vec3f, mipLevel: f32) -> vec4f { return sampleEquirectSphLod(toSpherical(dir), mipLevel); } fn sampleOctahedralDir(dir: vec3f) -> vec4f { let uv = octEncode(dir) * 0.5 + 0.5; return textureSample(sourceTex, sourceTexSampler, vec2f(uv.x, 1.0 - uv.y)); } fn sampleOctahedralSph(sph: vec2f) -> vec4f { return sampleOctahedralDir(fromSpherical(sph)); } fn sampleOctahedralDirLod(dir: vec3f, mipLevel: f32) -> vec4f { let uv = octEncode(dir) * 0.5 + 0.5; return textureSampleLevel(sourceTex, sourceTexSampler, vec2f(uv.x, 1.0 - uv.y), mipLevel); } fn sampleOctahedralSphLod(sph: vec2f, mipLevel: f32) -> vec4f { return sampleOctahedralDirLod(fromSpherical(sph), mipLevel); } #endif fn getDirectionCubemap(uv: vec2f) -> vec3f { let st = uv * 2.0 - 1.0; let face = targetFace(); var vec: vec3f; if (face == 0.0) { vec = vec3f(1, -st.y, -st.x); } else if (face == 1.0) { vec = vec3f(-1, -st.y, st.x); } else if (face == 2.0) { vec = vec3f(st.x, 1, st.y); } else if (face == 3.0) { vec = vec3f(st.x, -1, -st.y); } else if (face == 4.0) { vec = vec3f(st.x, -st.y, 1); } else { vec = vec3f(-st.x, -st.y, -1); } return normalize(modifySeams(vec, 1.0)); } fn matrixFromVector(n: vec3f) -> mat3x3f { let a = 1.0 / (1.0 + n.z); let b = -n.x * n.y * a; let b1 = vec3f(1.0 - n.x * n.x * a, b, -n.x); let b2 = vec3f(b, 1.0 - n.y * n.y * a, -n.y); return mat3x3f(b1, b2, n); } fn matrixFromVectorSlow(n: vec3f) -> mat3x3f { let up = select(vec3f(0.0, 0.0, select(-1.0, 1.0, n.y > 0.0)), vec3f(0.0, 1.0, 0.0), abs(n.y) > 0.0000001); let x = normalize(cross(up, n)); let y = cross(n, x); return mat3x3f(x, y, n); } fn reproject(uv: vec2f) -> vec4f { if ({NUM_SAMPLES} <= 1) { return {ENCODE_FUNC}({DECODE_FUNC}({SOURCE_FUNC}Dir({TARGET_FUNC}(uv)))); } else { let t = {TARGET_FUNC}(uv); let tu = dpdx(t); let tv = dpdy(t); var result = vec3f(0.0); for (var u = 0.0; u < {NUM_SAMPLES_SQRT}; u += 1.0) { for (var v = 0.0; v < {NUM_SAMPLES_SQRT}; v += 1.0) { result += {DECODE_FUNC}({SOURCE_FUNC}Dir(normalize(t + tu * (u / {NUM_SAMPLES_SQRT} - 0.5) + tv * (v / {NUM_SAMPLES_SQRT} - 0.5)))); } } return {ENCODE_FUNC}(result / ({NUM_SAMPLES_SQRT} * {NUM_SAMPLES_SQRT})); } } const unpackFloat: vec4f = vec4f(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0); #ifdef USE_SAMPLES_TEX fn unpackSample(i: i32, L: ptr, mipLevel: ptr) { var u = (f32(i * 4) + 0.5) * uniform.samplesTexInverseSize.x; var v = (floor(u) + 0.5) * uniform.samplesTexInverseSize.y; var raw: vec4f; raw.x = dot(textureSample(samplesTex, samplesTexSampler, vec2f(u, v)), unpackFloat); u += uniform.samplesTexInverseSize.x; raw.y = dot(textureSample(samplesTex, samplesTexSampler, vec2f(u, v)), unpackFloat); u += uniform.samplesTexInverseSize.x; raw.z = dot(textureSample(samplesTex, samplesTexSampler, vec2f(u, v)), unpackFloat); u += uniform.samplesTexInverseSize.x; raw.w = dot(textureSample(samplesTex, samplesTexSampler, vec2f(u, v)), unpackFloat); *L = raw.xyz * 2.0 - 1.0; *mipLevel = raw.w * 8.0; } fn prefilterSamples(uv: vec2f) -> vec4f { let vecSpace = matrixFromVectorSlow({TARGET_FUNC}(uv)); var L: vec3f; var mipLevel: f32; var result = vec3f(0.0); var totalWeight = 0.0; for (var i = 0; i < {NUM_SAMPLES}; i += 1) { unpackSample(i, &L, &mipLevel); result += {DECODE_FUNC}({SOURCE_FUNC}DirLod(vecSpace * L, mipLevel)) * L.z; totalWeight += L.z; } return {ENCODE_FUNC}(result / totalWeight); } fn prefilterSamplesUnweighted(uv: vec2f) -> vec4f { let vecSpace = matrixFromVectorSlow({TARGET_FUNC}(uv)); var L: vec3f; var mipLevel: f32; var result = vec3f(0.0); for (var i = 0; i < {NUM_SAMPLES}; i += 1) { unpackSample(i, &L, &mipLevel); result += {DECODE_FUNC}({SOURCE_FUNC}DirLod(vecSpace * L, mipLevel)); } return {ENCODE_FUNC}(result / f32({NUM_SAMPLES})); } #endif @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; output.color = {PROCESS_FUNC}(input.vUv0); return output; } `; var reprojectVS = ` attribute vertex_position: vec2f; uniform uvMod: vec4f; varying vUv0: vec2f; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4f(input.vertex_position, 0.5, 1.0); output.vUv0 = getImageEffectUV((input.vertex_position * 0.5 + vec2f(0.5, 0.5)) * uniform.uvMod.xy + uniform.uvMod.zw); return output; } `; var screenDepthPS = ` var uSceneDepthMap: texture_2d; #ifndef SCREENSIZE #define SCREENSIZE uniform uScreenSize: vec4f; #endif #ifndef VIEWMATRIX #define VIEWMATRIX uniform matrix_view: mat4x4f; #endif #ifndef LINEARIZE_DEPTH #define LINEARIZE_DEPTH #ifndef CAMERAPLANES #define CAMERAPLANES uniform camera_params: vec4f; #endif fn linearizeDepth(z: f32) -> f32 { if (uniform.camera_params.w == 0.0) { return (uniform.camera_params.z * uniform.camera_params.y) / (uniform.camera_params.y + z * (uniform.camera_params.z - uniform.camera_params.y)); } else { return uniform.camera_params.z + z * (uniform.camera_params.y - uniform.camera_params.z); } } #endif fn delinearizeDepth(linearDepth: f32) -> f32 { if (uniform.camera_params.w == 0.0) { return (uniform.camera_params.y * (uniform.camera_params.z - linearDepth)) / (linearDepth * (uniform.camera_params.z - uniform.camera_params.y)); } else { return (linearDepth - uniform.camera_params.z) / (uniform.camera_params.y - uniform.camera_params.z); } } fn getLinearScreenDepth(uv: vec2f) -> f32 { let textureSize = textureDimensions(uSceneDepthMap, 0); let texel: vec2i = vec2i(uv * vec2f(textureSize)); #ifdef SCENE_DEPTHMAP_LINEAR return textureLoad(uSceneDepthMap, texel, 0).r; #else return linearizeDepth(textureLoad(uSceneDepthMap, texel, 0).r); #endif } #ifndef VERTEXSHADER fn getLinearScreenDepthFrag() -> f32 { let uv: vec2f = pcPosition.xy * uniform.uScreenSize.zw; return getLinearScreenDepth(uv); } #endif fn getLinearDepth(pos: vec3f) -> f32 { return -(uniform.matrix_view * vec4f(pos, 1.0)).z; } `; var shadowCascadesPS = ` fn getShadowCascadeIndex(shadowCascadeDistances: vec4f, shadowCascadeCount: i32) -> i32 { let depth: f32 = 1.0 / pcPosition.w; let comparisons: vec4f = step(shadowCascadeDistances, vec4f(depth)); let cascadeIndex: i32 = i32(dot(comparisons, vec4f(1.0))); return min(cascadeIndex, shadowCascadeCount - 1); } fn ditherShadowCascadeIndex(cascadeIndex_in: i32, shadowCascadeDistances: vec4f, shadowCascadeCount: i32, blendFactor: f32) -> i32 { var cascadeIndex: i32 = cascadeIndex_in; if (cascadeIndex < shadowCascadeCount - 1) { let currentRangeEnd: f32 = shadowCascadeDistances[cascadeIndex]; let transitionStart: f32 = blendFactor * currentRangeEnd; let depth: f32 = 1.0 / pcPosition.w; if (depth > transitionStart) { let transitionFactor: f32 = smoothstep(transitionStart, currentRangeEnd, depth); let dither: f32 = fract(sin(dot(pcPosition.xy, vec2f(12.9898, 78.233))) * 43758.5453); if (dither < transitionFactor) { cascadeIndex = cascadeIndex + 1; } } } return cascadeIndex; } fn fadeShadow(shadowCoord_in: vec3f, shadowCascadeDistances: vec4f) -> vec3f { var shadowCoord: vec3f = shadowCoord_in; let depth: f32 = 1.0 / pcPosition.w; if (depth > shadowCascadeDistances.w) { shadowCoord.z = -9999999.0; } return shadowCoord; } `; var shadowEVSMPS = ` fn linstep(a: f32, b: f32, v: f32) -> f32 { return clamp((v - a) / (b - a), 0.0, 1.0); } fn reduceLightBleeding(pMax: f32, amount: f32) -> f32 { return linstep(amount, 1.0, pMax); } fn chebyshevUpperBound(moments: vec2f, mean: f32, minVariance: f32, lightBleedingReduction: f32) -> f32 { var variance: f32 = moments.y - (moments.x * moments.x); variance = max(variance, minVariance); let d: f32 = mean - moments.x; var pMax: f32 = variance / (variance + (d * d)); pMax = reduceLightBleeding(pMax, lightBleedingReduction); return select(pMax, 1.0, mean <= moments.x); } fn calculateEVSM(moments_in: vec3f, Z_in: f32, vsmBias: f32, exponent: f32) -> f32 { let Z: f32 = 2.0 * Z_in - 1.0; let warpedDepth: f32 = exp(exponent * Z); let moments: vec2f = moments_in.xy + vec2f(warpedDepth, warpedDepth*warpedDepth) * (1.0 - moments_in.z); let VSMBias: f32 = vsmBias; let depthScale: f32 = VSMBias * exponent * warpedDepth; let minVariance1: f32 = depthScale * depthScale; return chebyshevUpperBound(moments, warpedDepth, minVariance1, 0.1); } fn VSM16(tex: texture_2d, texSampler: sampler, texCoords: vec2f, resolution: f32, Z: f32, vsmBias: f32, exponent: f32) -> f32 { let moments: vec3f = textureSampleLevel(tex, texSampler, texCoords, 0.0).xyz; return calculateEVSM(moments, Z, vsmBias, exponent); } fn getShadowVSM16(shadowMap: texture_2d, shadowMapSampler: sampler, shadowCoord: vec3f, shadowParams: vec4f, exponent: f32) -> f32 { return VSM16(shadowMap, shadowMapSampler, shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent); } fn getShadowSpotVSM16(shadowMap: texture_2d, shadowMapSampler: sampler, shadowCoord: vec3f, shadowParams: vec4f, exponent: f32, lightDir: vec3f) -> f32 { let Z: f32 = length(lightDir) * shadowParams.w + shadowParams.z; return VSM16(shadowMap, shadowMapSampler, shadowCoord.xy, shadowParams.x, Z, shadowParams.y, exponent); } fn VSM32(tex: texture_2d, texSampler: sampler, texCoords_in: vec2f, resolution: f32, Z: f32, vsmBias: f32, exponent: f32) -> f32 { #ifdef CAPS_TEXTURE_FLOAT_FILTERABLE var moments: vec3f = textureSampleLevel(tex, texSampler, texCoords_in, 0.0).xyz; #else var pixelSize : f32 = 1.0 / resolution; let texCoords: vec2f = texCoords_in - vec2f(pixelSize); let s00: vec3f = textureSampleLevel(tex, texSampler, texCoords, 0.0).xyz; let s10: vec3f = textureSampleLevel(tex, texSampler, texCoords + vec2f(pixelSize, 0.0), 0.0).xyz; let s01: vec3f = textureSampleLevel(tex, texSampler, texCoords + vec2f(0.0, pixelSize), 0.0).xyz; let s11: vec3f = textureSampleLevel(tex, texSampler, texCoords + vec2f(pixelSize), 0.0).xyz; let fr: vec2f = fract(texCoords * resolution); let h0: vec3f = mix(s00, s10, fr.x); let h1: vec3f = mix(s01, s11, fr.x); var moments: vec3f = mix(h0, h1, fr.y); #endif return calculateEVSM(moments, Z, vsmBias, exponent); } fn getShadowVSM32(shadowMap: texture_2d, shadowMapSampler: sampler, shadowCoord: vec3f, shadowParams: vec4f, exponent: f32) -> f32 { return VSM32(shadowMap, shadowMapSampler, shadowCoord.xy, shadowParams.x, shadowCoord.z, shadowParams.y, exponent); } fn getShadowSpotVSM32(shadowMap: texture_2d, shadowMapSampler: sampler, shadowCoord: vec3f, shadowParams: vec4f, exponent: f32, lightDir: vec3f) -> f32 { let Z: f32 = length(lightDir) * shadowParams.w + shadowParams.z; return VSM32(shadowMap, shadowMapSampler, shadowCoord.xy, shadowParams.x, Z, shadowParams.y, exponent); } `; var shadowPCF1PS = ` fn getShadowPCF1x1(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return textureSampleCompareLevel(shadowMap, shadowMapSampler, shadowCoord.xy, shadowCoord.z); } fn getShadowSpotPCF1x1(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return textureSampleCompareLevel(shadowMap, shadowMapSampler, shadowCoord.xy, shadowCoord.z); } `; var shadowPCF3PS = ` fn _getShadowPCF3x3(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec3f) -> f32 { let z: f32 = shadowCoord.z; let uv: vec2f = shadowCoord.xy * shadowParams.x; let shadowMapSizeInv: f32 = 1.0 / shadowParams.x; let base_uv_temp: vec2f = floor(uv + 0.5); let s: f32 = (uv.x + 0.5 - base_uv_temp.x); let t: f32 = (uv.y + 0.5 - base_uv_temp.y); let base_uv: vec2f = (base_uv_temp - vec2f(0.5)) * shadowMapSizeInv; var sum: f32 = 0.0; let uw0: f32 = (3.0 - 2.0 * s); let uw1: f32 = (1.0 + 2.0 * s); let u0_offset: f32 = (2.0 - s) / uw0 - 1.0; let u1_offset: f32 = s / uw1 + 1.0; let vw0: f32 = (3.0 - 2.0 * t); let vw1: f32 = (1.0 + 2.0 * t); let v0_offset: f32 = (2.0 - t) / vw0 - 1.0; let v1_offset: f32 = t / vw1 + 1.0; let u0: f32 = u0_offset * shadowMapSizeInv + base_uv.x; let v0: f32 = v0_offset * shadowMapSizeInv + base_uv.y; let u1: f32 = u1_offset * shadowMapSizeInv + base_uv.x; let v1: f32 = v1_offset * shadowMapSizeInv + base_uv.y; sum = sum + uw0 * vw0 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u0, v0), z); sum = sum + uw1 * vw0 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u1, v0), z); sum = sum + uw0 * vw1 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u0, v1), z); sum = sum + uw1 * vw1 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u1, v1), z); sum = sum * (1.0 / 16.0); return sum; } fn getShadowPCF3x3(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return _getShadowPCF3x3(shadowMap, shadowMapSampler, shadowCoord, shadowParams.xyz); } fn getShadowSpotPCF3x3(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return _getShadowPCF3x3(shadowMap, shadowMapSampler, shadowCoord, shadowParams.xyz); } `; var shadowPCF5PS = ` fn _getShadowPCF5x5(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec3f) -> f32 { let z: f32 = shadowCoord.z; let uv: vec2f = shadowCoord.xy * shadowParams.x; let shadowMapSizeInv: f32 = 1.0 / shadowParams.x; let base_uv_temp: vec2f = floor(uv + 0.5); let s: f32 = (uv.x + 0.5 - base_uv_temp.x); let t: f32 = (uv.y + 0.5 - base_uv_temp.y); let base_uv: vec2f = (base_uv_temp - vec2f(0.5)) * shadowMapSizeInv; let uw0: f32 = (4.0 - 3.0 * s); let uw1: f32 = 7.0; let uw2: f32 = (1.0 + 3.0 * s); let u0_offset: f32 = (3.0 - 2.0 * s) / uw0 - 2.0; let u1_offset: f32 = (3.0 + s) / uw1; let u2_offset: f32 = s / uw2 + 2.0; let vw0: f32 = (4.0 - 3.0 * t); let vw1: f32 = 7.0; let vw2: f32 = (1.0 + 3.0 * t); let v0_offset: f32 = (3.0 - 2.0 * t) / vw0 - 2.0; let v1_offset: f32 = (3.0 + t) / vw1; let v2_offset: f32 = t / vw2 + 2.0; var sum: f32 = 0.0; let u0: f32 = u0_offset * shadowMapSizeInv + base_uv.x; let v0: f32 = v0_offset * shadowMapSizeInv + base_uv.y; let u1: f32 = u1_offset * shadowMapSizeInv + base_uv.x; let v1: f32 = v1_offset * shadowMapSizeInv + base_uv.y; let u2: f32 = u2_offset * shadowMapSizeInv + base_uv.x; let v2: f32 = v2_offset * shadowMapSizeInv + base_uv.y; sum = sum + uw0 * vw0 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u0, v0), z); sum = sum + uw1 * vw0 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u1, v0), z); sum = sum + uw2 * vw0 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u2, v0), z); sum = sum + uw0 * vw1 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u0, v1), z); sum = sum + uw1 * vw1 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u1, v1), z); sum = sum + uw2 * vw1 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u2, v1), z); sum = sum + uw0 * vw2 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u0, v2), z); sum = sum + uw1 * vw2 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u1, v2), z); sum = sum + uw2 * vw2 * textureSampleCompareLevel(shadowMap, shadowMapSampler, vec2f(u2, v2), z); sum = sum * (1.0 / 144.0); sum = saturate(sum); return sum; } fn getShadowPCF5x5(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return _getShadowPCF5x5(shadowMap, shadowMapSampler, shadowCoord, shadowParams.xyz); } fn getShadowSpotPCF5x5(shadowMap: texture_depth_2d, shadowMapSampler: sampler_comparison, shadowCoord: vec3f, shadowParams: vec4f) -> f32 { return _getShadowPCF5x5(shadowMap, shadowMapSampler, shadowCoord, shadowParams.xyz); } `; var shadowSoftPS = ` fn fractSinRand(uv: vec2f) -> f32 { let PI: f32 = 3.141592653589793; let a: f32 = 12.9898; let b: f32 = 78.233; let c: f32 = 43758.5453; let dt: f32 = dot(uv.xy, vec2f(a, b)); let sn: f32 = dt % PI; return fract(sin(sn) * c); } struct VogelDiskData { invNumSamples: f32, initialAngle: f32, currentPointId: f32, } fn prepareDiskConstants(data: ptr, sampleCount: i32, randomSeed: f32) { let pi2: f32 = 6.28318530718; data.invNumSamples = 1.0 / f32(sampleCount); data.initialAngle = randomSeed * pi2; data.currentPointId = 0.0; } fn generateDiskSample(data: ptr) -> vec2f { let GOLDEN_ANGLE: f32 = 2.399963; let r: f32 = sqrt((data.currentPointId + 0.5) * data.invNumSamples); let theta: f32 = data.currentPointId * GOLDEN_ANGLE + data.initialAngle; let offset: vec2f = vec2f(cos(theta), sin(theta)) * pow(r, 1.33); data.currentPointId = data.currentPointId + 1.0; return offset; } fn PCSSFindBlocker(shadowMap: texture_2d, shadowMapSampler: sampler, avgBlockerDepth: ptr, numBlockers: ptr, shadowCoords: vec2f, z: f32, shadowBlockerSamples: i32, penumbraSize: f32, invShadowMapSize: f32, randomSeed: f32) { var diskData: VogelDiskData; prepareDiskConstants(&diskData, shadowBlockerSamples, randomSeed); let searchWidth: f32 = penumbraSize * invShadowMapSize; var blockerSum: f32 = 0.0; var numBlockers_local: i32 = 0; for( var i: i32 = 0; i < shadowBlockerSamples; i = i + 1 ) { let diskUV: vec2f = generateDiskSample(&diskData); let sampleUV: vec2f = shadowCoords + diskUV * searchWidth; let shadowMapDepth: f32 = textureSampleLevel(shadowMap, shadowMapSampler, sampleUV, 0.0).r; if ( shadowMapDepth < z ) { blockerSum = blockerSum + shadowMapDepth; numBlockers_local = numBlockers_local + 1; } } *avgBlockerDepth = blockerSum / f32(numBlockers_local); *numBlockers = numBlockers_local; } fn PCSSFilter(shadowMap: texture_2d, shadowMapSampler: sampler, uv: vec2f, receiverDepth: f32, shadowSamples: i32, filterRadius: f32, randomSeed: f32) -> f32 { var diskData: VogelDiskData; prepareDiskConstants(&diskData, shadowSamples, randomSeed); var sum: f32 = 0.0; for (var i: i32 = 0; i < shadowSamples; i = i + 1) { let offsetUV: vec2f = generateDiskSample(&diskData) * filterRadius; let depth: f32 = textureSampleLevel(shadowMap, shadowMapSampler, uv + offsetUV, 0.0).r; sum = sum + step(receiverDepth, depth); } return sum / f32(shadowSamples); } fn getPenumbra(dblocker: f32, dreceiver: f32, penumbraSize: f32, penumbraFalloff: f32) -> f32 { let dist: f32 = dreceiver - dblocker; let penumbra: f32 = 1.0 - pow(1.0 - dist, penumbraFalloff); return penumbra * penumbraSize; } fn PCSSDirectional(shadowMap: texture_2d, shadowMapSampler: sampler, shadowCoords: vec3f, cameraParams: vec4f, softShadowParams: vec4f) -> f32 { let receiverDepth: f32 = shadowCoords.z; let randomSeed: f32 = fractSinRand(pcPosition.xy); let shadowSamples: i32 = i32(softShadowParams.x); let shadowBlockerSamples: i32 = i32(softShadowParams.y); let penumbraSize: f32 = softShadowParams.z; let penumbraFalloff: f32 = softShadowParams.w; let shadowMapSize: i32 = i32(textureDimensions(shadowMap, 0).x); var invShadowMapSize: f32 = 1.0 / f32(shadowMapSize); invShadowMapSize = invShadowMapSize * (f32(shadowMapSize) / 2048.0); var penumbra: f32; if (shadowBlockerSamples > 0) { var avgBlockerDepth: f32 = 0.0; var numBlockers: i32 = 0; PCSSFindBlocker(shadowMap, shadowMapSampler, &avgBlockerDepth, &numBlockers, shadowCoords.xy, receiverDepth, shadowBlockerSamples, penumbraSize, invShadowMapSize, randomSeed); if (numBlockers < 1) { return 1.0; } penumbra = getPenumbra(avgBlockerDepth, shadowCoords.z, penumbraSize, penumbraFalloff); } else { penumbra = penumbraSize; } let filterRadius: f32 = penumbra * invShadowMapSize; return PCSSFilter(shadowMap, shadowMapSampler, shadowCoords.xy, receiverDepth, shadowSamples, filterRadius, randomSeed); } fn getShadowPCSS(shadowMap: texture_2d, shadowMapSampler: sampler, shadowCoord: vec3f, shadowParams: vec4f, cameraParams: vec4f, softShadowParams: vec4f, lightDir: vec3f) -> f32 { return PCSSDirectional(shadowMap, shadowMapSampler, shadowCoord, cameraParams, softShadowParams); } `; var skinBatchVS = ` attribute vertex_boneIndices: f32; var texture_poseMap: texture_2d; fn getBoneMatrix(indexFloat: f32) -> mat4x4f { let width = i32(textureDimensions(texture_poseMap).x); let index: i32 = i32(indexFloat + 0.5) * 3; let iy: i32 = index / width; let ix: i32 = index % width; let v1: vec4f = textureLoad(texture_poseMap, vec2i(ix + 0, iy), 0); let v2: vec4f = textureLoad(texture_poseMap, vec2i(ix + 1, iy), 0); let v3: vec4f = textureLoad(texture_poseMap, vec2i(ix + 2, iy), 0); return mat4x4f( v1.x, v2.x, v3.x, 0, v1.y, v2.y, v3.y, 0, v1.z, v2.z, v3.z, 0, v1.w, v2.w, v3.w, 1.0 ); } `; var skinVS = ` attribute vertex_boneWeights: vec4f; attribute vertex_boneIndices: vec4f; var texture_poseMap: texture_2d; struct BoneMatrix { v1: vec4f, v2: vec4f, v3: vec4f, } fn getBoneMatrix(width: i32, index: i32) -> BoneMatrix { let v = index / width; let u = index % width; var result: BoneMatrix; result.v1 = textureLoad(texture_poseMap, vec2i(u + 0, v), 0); result.v2 = textureLoad(texture_poseMap, vec2i(u + 1, v), 0); result.v3 = textureLoad(texture_poseMap, vec2i(u + 2, v), 0); return result; } fn getSkinMatrix(indicesFloat: vec4f, weights: vec4f) -> mat4x4f { let width = i32(textureDimensions(texture_poseMap).x); var indices = vec4i(indicesFloat + 0.5) * 3; let boneA = getBoneMatrix(width, indices.x); let boneB = getBoneMatrix(width, indices.y); let boneC = getBoneMatrix(width, indices.z); let boneD = getBoneMatrix(width, indices.w); let v1 = boneA.v1 * weights.x + boneB.v1 * weights.y + boneC.v1 * weights.z + boneD.v1 * weights.w; let v2 = boneA.v2 * weights.x + boneB.v2 * weights.y + boneC.v2 * weights.z + boneD.v2 * weights.w; let v3 = boneA.v3 * weights.x + boneB.v3 * weights.y + boneC.v3 * weights.z + boneD.v3 * weights.w; let one = dot(weights, vec4f(1.0, 1.0, 1.0, 1.0)); return mat4x4f( v1.x, v2.x, v3.x, 0, v1.y, v2.y, v3.y, 0, v1.z, v2.z, v3.z, 0, v1.w, v2.w, v3.w, one ); } `; var skyboxPS = ` #define LIT_SKYBOX_INTENSITY #include "envProcPS" #include "gammaPS" #include "tonemappingPS" #ifdef PREPASS_PASS varying vLinearDepth: f32; #include "floatAsUintPS" #endif varying vViewDir : vec3f; uniform skyboxHighlightMultiplier : f32; #ifdef SKY_CUBEMAP var texture_cubeMap : texture_cube; var texture_cubeMap_sampler : sampler; #ifdef SKYMESH varying vWorldPos : vec3f; uniform cubeMapRotationMatrix : mat3x3f; uniform projectedSkydomeCenter : vec3f; #endif #else #include "sphericalPS" #include "envAtlasPS" var texture_envAtlas : texture_2d; var texture_envAtlas_sampler : sampler; uniform mipLevel : f32; #endif @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; #ifdef PREPASS_PASS output.color = float2vec4(vLinearDepth); #else var linear : vec3f; var dir : vec3f; #ifdef SKY_CUBEMAP #ifdef SKYMESH var envDir : vec3f = normalize(input.vWorldPos - uniform.projectedSkydomeCenter); dir = envDir * uniform.cubeMapRotationMatrix; #else dir = input.vViewDir; #endif dir.x *= -1.0; linear = {SKYBOX_DECODE_FNC}(textureSample(texture_cubeMap, texture_cubeMap_sampler, dir)); #else dir = input.vViewDir * vec3f(-1.0, 1.0, 1.0); let uv : vec2f = toSphericalUv(normalize(dir)); linear = {SKYBOX_DECODE_FNC}(textureSample(texture_envAtlas, texture_envAtlas_sampler, mapRoughnessUv(uv, uniform.mipLevel))); #endif if (any(linear >= vec3f(64.0))) { linear *= uniform.skyboxHighlightMultiplier; } output.color = vec4f(gammaCorrectOutput(toneMap(processEnvironment(linear))), 1.0); #endif return output; } `; var skyboxVS = ` attribute aPosition : vec4f; uniform matrix_view : mat4x4f; uniform matrix_projectionSkybox : mat4x4f; uniform cubeMapRotationMatrix : mat3x3f; varying vViewDir : vec3f; #ifdef PREPASS_PASS varying vLinearDepth: f32; #endif #ifdef SKYMESH uniform matrix_model : mat4x4f; varying vWorldPos : vec3f; #endif @vertex fn vertexMain(input : VertexInput) -> VertexOutput { var output : VertexOutput; var view : mat4x4f = uniform.matrix_view; #ifdef SKYMESH var worldPos : vec4f = uniform.matrix_model * input.aPosition; output.vWorldPos = worldPos.xyz; output.position = uniform.matrix_projectionSkybox * (view * worldPos); #ifdef PREPASS_PASS output.vLinearDepth = -(uniform.matrix_view * vec4f(worldPos.xyz, 1.0)).z; #endif #else view[3][0] = 0.0; view[3][1] = 0.0; view[3][2] = 0.0; output.position = uniform.matrix_projectionSkybox * (view * input.aPosition); output.vViewDir = input.aPosition.xyz * uniform.cubeMapRotationMatrix; #ifdef PREPASS_PASS output.vLinearDepth = -pcPosition.w; #endif #endif output.position.z = output.position.w - 1.0e-7; return output; } `; var specularPS = ` #ifdef STD_SPECULAR_CONSTANT uniform material_specular: vec3f; #endif fn getSpecularity() { var specularColor = vec3f(1.0, 1.0, 1.0); #ifdef STD_SPECULAR_CONSTANT specularColor = specularColor * uniform.material_specular; #endif #ifdef STD_SPECULAR_TEXTURE specularColor = specularColor * {STD_SPECULAR_TEXTURE_DECODE}(textureSampleBias({STD_SPECULAR_TEXTURE_NAME}, {STD_SPECULAR_TEXTURE_NAME}Sampler, {STD_SPECULAR_TEXTURE_UV}, uniform.textureBias)).{STD_SPECULAR_TEXTURE_CHANNEL}; #endif #ifdef STD_SPECULAR_VERTEX specularColor = specularColor * saturate3(vVertexColor.{STD_SPECULAR_VERTEX_CHANNEL}); #endif dSpecularity = specularColor; } `; var sphericalPS = ` fn toSpherical(dir: vec3f) -> vec2f { let angle_xz = select(0.0, atan2(dir.x, dir.z), any(dir.xz != vec2f(0.0))); return vec2f(angle_xz, asin(dir.y)); } fn toSphericalUv(dir : vec3f) -> vec2f { const PI : f32 = 3.141592653589793; let uv : vec2f = toSpherical(dir) / vec2f(PI * 2.0, PI) + vec2f(0.5, 0.5); return vec2f(uv.x, 1.0 - uv.y); } `; var specularityFactorPS = ` #ifdef STD_SPECULARITYFACTOR_CONSTANT uniform material_specularityFactor: f32; #endif fn getSpecularityFactor() { var specularityFactor = 1.0; #ifdef STD_SPECULARITYFACTOR_CONSTANT specularityFactor = specularityFactor * uniform.material_specularityFactor; #endif #ifdef STD_SPECULARITYFACTOR_TEXTURE specularityFactor = specularityFactor * textureSampleBias({STD_SPECULARITYFACTOR_TEXTURE_NAME}, {STD_SPECULARITYFACTOR_TEXTURE_NAME}Sampler, {STD_SPECULARITYFACTOR_TEXTURE_UV}, uniform.textureBias).{STD_SPECULARITYFACTOR_TEXTURE_CHANNEL}; #endif #ifdef STD_SPECULARITYFACTOR_VERTEX specularityFactor = specularityFactor * saturate(vVertexColor.{STD_SPECULARITYFACTOR_VERTEX_CHANNEL}); #endif dSpecularityFactor = specularityFactor; } `; var spotPS = ` fn getSpotEffect(lightSpotDir: vec3f, lightInnerConeAngle: f32, lightOuterConeAngle: f32, lightDirNorm: vec3f) -> f32 { let cosAngle: f32 = dot(lightDirNorm, lightSpotDir); return smoothstep(lightOuterConeAngle, lightInnerConeAngle, cosAngle); }`; var startNineSlicedPS = ` nineSlicedUv = vec2f(vUv0.x, 1.0 - vUv0.y); `; var startNineSlicedTiledPS = ` let tileMask: vec2f = step(vMask, vec2f(0.99999)); let tileSize: vec2f = 0.5 * (innerOffset.xy + innerOffset.zw); let tileScale: vec2f = vec2f(1.0) / (vec2f(1.0) - tileSize); var clampedUv: vec2f = mix(innerOffset.xy * 0.5, vec2f(1.0) - innerOffset.zw * 0.5, fract((vTiledUv - tileSize) * tileScale)); clampedUv = clampedUv * atlasRect.zw + atlasRect.xy; var nineSlicedUv: vec2f = vUv0 * tileMask + clampedUv * (vec2f(1.0) - tileMask); nineSlicedUv.y = 1.0 - nineSlicedUv.y; `; var stdDeclarationPS = ` var dAlpha: f32 = 1.0; #if LIT_BLEND_TYPE != NONE || defined(LIT_ALPHA_TEST) || defined(LIT_ALPHA_TO_COVERAGE) || STD_OPACITY_DITHER != NONE #ifdef STD_OPACITY_TEXTURE_ALLOCATE var texture_opacityMap : texture_2d; var texture_opacityMapSampler : sampler; #endif #endif #ifdef FORWARD_PASS var dAlbedo: vec3f; var dNormalW: vec3f; var dSpecularity: vec3f = vec3f(0.0, 0.0, 0.0); var dGlossiness: f32 = 0.0; #ifdef LIT_REFRACTION var dTransmission: f32; var dThickness: f32; #endif #ifdef LIT_SCENE_COLOR var uSceneColorMap : texture_2d; var uSceneColorMapSampler : sampler; #endif #ifdef LIT_SCREEN_SIZE uniform uScreenSize: vec4f; #endif #ifdef LIT_TRANSFORMS var matrix_viewProjection: mat4x4f; var matrix_model: mat4x4f; #endif #ifdef STD_HEIGHT_MAP var dUvOffset: vec2f; #ifdef STD_HEIGHT_TEXTURE_ALLOCATE var texture_heightMap : texture_2d; var texture_heightMapSampler : sampler; #endif #endif #ifdef STD_DIFFUSE_TEXTURE_ALLOCATE var texture_diffuseMap : texture_2d; var texture_diffuseMapSampler : sampler; #endif #ifdef STD_DIFFUSEDETAIL_TEXTURE_ALLOCATE var texture_diffuseDetailMap : texture_2d; var texture_diffuseDetailMapSampler : sampler; #endif #ifdef STD_NORMAL_TEXTURE_ALLOCATE var texture_normalMap : texture_2d; var texture_normalMapSampler : sampler; #endif #ifdef STD_NORMALDETAIL_TEXTURE_ALLOCATE var texture_normalDetailMap : texture_2d; var texture_normalDetailMapSampler : sampler; #endif #ifdef STD_THICKNESS_TEXTURE_ALLOCATE var texture_thicknessMap : texture_2d; var texture_thicknessMapSampler : sampler; #endif #ifdef STD_REFRACTION_TEXTURE_ALLOCATE var texture_refractionMap : texture_2d; var texture_refractionMapSampler : sampler; #endif #ifdef LIT_IRIDESCENCE var dIridescence: f32; var dIridescenceThickness: f32; #ifdef STD_IRIDESCENCE_THICKNESS_TEXTURE_ALLOCATE var texture_iridescenceThicknessMap : texture_2d; var texture_iridescenceThicknessMapSampler : sampler; #endif #ifdef STD_IRIDESCENCE_TEXTURE_ALLOCATE var texture_iridescenceMap : texture_2d; var texture_iridescenceMapSampler : sampler; #endif #endif #ifdef LIT_CLEARCOAT var ccSpecularity: f32; var ccGlossiness: f32; var ccNormalW: vec3f; #endif #ifdef LIT_GGX_SPECULAR var dAnisotropy: f32; var dAnisotropyRotation: vec2f; #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_SHEEN var sSpecularity: vec3f; var sGlossiness: f32; #ifdef STD_SHEEN_TEXTURE_ALLOCATE var texture_sheenMap : texture_2d; var texture_sheenMapSampler : sampler; #endif #ifdef STD_SHEENGLOSS_TEXTURE_ALLOCATE var texture_sheenGlossMap : texture_2d; var texture_sheenGlossMapSampler : sampler; #endif #endif #ifdef LIT_METALNESS var dMetalness: f32; var dIor: f32; #ifdef STD_METALNESS_TEXTURE_ALLOCATE var texture_metalnessMap : texture_2d; var texture_metalnessMapSampler : sampler; #endif #endif #ifdef LIT_SPECULARITY_FACTOR var dSpecularityFactor: f32; #ifdef STD_SPECULARITYFACTOR_TEXTURE_ALLOCATE var texture_specularityFactorMap : texture_2d; var texture_specularityFactorMapSampler : sampler; #endif #endif #ifdef STD_SPECULAR_COLOR #ifdef STD_SPECULAR_TEXTURE_ALLOCATE var texture_specularMap : texture_2d; var texture_specularMapSampler : sampler; #endif #endif #ifdef STD_GLOSS_TEXTURE_ALLOCATE var texture_glossMap : texture_2d; var texture_glossMapSampler : sampler; #endif #endif #ifdef STD_AO var dAo: f32; #ifdef STD_AO_TEXTURE_ALLOCATE var texture_aoMap : texture_2d; var texture_aoMapSampler : sampler; #endif #ifdef STD_AODETAIL_TEXTURE_ALLOCATE var texture_aoDetailMap : texture_2d; var texture_aoDetailMapSampler : sampler; #endif #endif var dEmission: vec3f; #ifdef STD_EMISSIVE_TEXTURE_ALLOCATE var texture_emissiveMap : texture_2d; var texture_emissiveMapSampler : sampler; #endif #ifdef LIT_CLEARCOAT #ifdef STD_CLEARCOAT_TEXTURE_ALLOCATE var texture_clearCoatMap : texture_2d; var texture_clearCoatMapSampler : sampler; #endif #ifdef STD_CLEARCOATGLOSS_TEXTURE_ALLOCATE var texture_clearCoatGlossMap : texture_2d; var texture_clearCoatGlossMapSampler : sampler; #endif #ifdef STD_CLEARCOATNORMAL_TEXTURE_ALLOCATE var texture_clearCoatNormalMap : texture_2d; var texture_clearCoatNormalMapSampler : sampler; #endif #endif #ifdef LIT_GGX_SPECULAR #ifdef STD_ANISOTROPY_TEXTURE_ALLOCATE var texture_anisotropyMap : texture_2d; var texture_anisotropyMapSampler : sampler; #endif #endif #if defined(STD_LIGHTMAP) || defined(STD_LIGHT_VERTEX_COLOR) var dLightmap: vec3f; #ifdef STD_LIGHT_TEXTURE_ALLOCATE var texture_lightMap : texture_2d; var texture_lightMapSampler : sampler; #endif #endif #endif #include "litShaderCorePS" `; var stdFrontEndPS = ` #if LIT_BLEND_TYPE != NONE || defined(LIT_ALPHA_TEST) || defined(LIT_ALPHA_TO_COVERAGE) || STD_OPACITY_DITHER != NONE #include "opacityPS" #if defined(LIT_ALPHA_TEST) #include "alphaTestPS" #endif #if STD_OPACITY_DITHER != NONE #include "opacityDitherPS" #endif #endif #ifdef FORWARD_PASS #ifdef STD_HEIGHT_MAP #include "parallaxPS" #endif #include "diffusePS" #ifdef LIT_NEEDS_NORMAL #include "normalMapPS" #endif #ifdef LIT_REFRACTION #include "transmissionPS" #include "thicknessPS" #endif #ifdef LIT_IRIDESCENCE #include "iridescencePS" #include "iridescenceThicknessPS" #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_SHEEN #include "sheenPS" #include "sheenGlossPS" #endif #ifdef LIT_METALNESS #include "metalnessPS" #include "iorPS" #endif #ifdef LIT_SPECULARITY_FACTOR #include "specularityFactorPS" #endif #ifdef STD_SPECULAR_COLOR #include "specularPS" #else fn getSpecularity() { dSpecularity = vec3f(1.0, 1.0, 1.0); } #endif #include "glossPS" #endif #ifdef STD_AO #include "aoPS" #endif #include "emissivePS" #ifdef LIT_CLEARCOAT #include "clearCoatPS" #include "clearCoatGlossPS" #include "clearCoatNormalPS" #endif #if defined(LIT_SPECULAR) && defined(LIT_LIGHTING) && defined(LIT_GGX_SPECULAR) #include "anisotropyPS" #endif #if defined(STD_LIGHTMAP) || defined(STD_LIGHT_VERTEX_COLOR) #include "lightmapPS" #endif #endif fn evaluateFrontend() { #if LIT_BLEND_TYPE != NONE || defined(LIT_ALPHA_TEST) || defined(LIT_ALPHA_TO_COVERAGE) || STD_OPACITY_DITHER != NONE getOpacity(); #if defined(LIT_ALPHA_TEST) alphaTest(dAlpha); #endif #if STD_OPACITY_DITHER != NONE opacityDither(dAlpha, 0.0); #endif litArgs_opacity = dAlpha; #endif #ifdef FORWARD_PASS #ifdef STD_HEIGHT_MAP getParallax(); #endif getAlbedo(); litArgs_albedo = dAlbedo; #ifdef LIT_NEEDS_NORMAL getNormal(); litArgs_worldNormal = dNormalW; #endif #ifdef LIT_REFRACTION getRefraction(); litArgs_transmission = dTransmission; getThickness(); litArgs_thickness = dThickness; #ifdef LIT_DISPERSION litArgs_dispersion = uniform.material_dispersion; #endif #endif #ifdef LIT_IRIDESCENCE getIridescence(); getIridescenceThickness(); litArgs_iridescence_intensity = dIridescence; litArgs_iridescence_thickness = dIridescenceThickness; #endif #ifdef LIT_SPECULAR_OR_REFLECTION #ifdef LIT_SHEEN getSheen(); litArgs_sheen_specularity = sSpecularity; getSheenGlossiness(); litArgs_sheen_gloss = sGlossiness; #endif #ifdef LIT_METALNESS getMetalness(); litArgs_metalness = dMetalness; getIor(); litArgs_ior = dIor; #endif #ifdef LIT_SPECULARITY_FACTOR getSpecularityFactor(); litArgs_specularityFactor = dSpecularityFactor; #endif getGlossiness(); getSpecularity(); litArgs_specularity = dSpecularity; litArgs_gloss = dGlossiness; #endif #ifdef STD_AO getAO(); litArgs_ao = dAo; #endif getEmission(); litArgs_emission = dEmission; #ifdef LIT_CLEARCOAT getClearCoat(); getClearCoatGlossiness(); getClearCoatNormal(); litArgs_clearcoat_specularity = ccSpecularity; litArgs_clearcoat_gloss = ccGlossiness; litArgs_clearcoat_worldNormal = ccNormalW; #endif #if defined(LIT_SPECULAR) && defined(LIT_LIGHTING) && defined(LIT_GGX_SPECULAR) getAnisotropy(); #endif #if defined(STD_LIGHTMAP) || defined(STD_LIGHT_VERTEX_COLOR) getLightMap(); litArgs_lightmap = dLightmap; #ifdef STD_LIGHTMAP_DIR litArgs_lightmapDir = dLightmapDir; #endif #endif #endif } `; var TBNPS = ` #ifdef LIT_TANGENTS #define TBN_TANGENTS #else #if defined(LIT_USE_NORMALS) || defined(LIT_USE_CLEARCOAT_NORMALS) #define TBN_DERIVATIVES #endif #endif #if defined(TBN_DERIVATIVES) uniform tbnBasis: f32; #endif fn getTBN(tangent: vec3f, binormal: vec3f, normal: vec3f) { #ifdef TBN_TANGENTS dTBN = mat3x3f(normalize(tangent), normalize(binormal), normalize(normal)); #elif defined(TBN_DERIVATIVES) let uv: vec2f = {lightingUv}; let dp1: vec3f = dpdx( vPositionW ); let dp2: vec3f = dpdy( vPositionW ); let duv1: vec2f = dpdx( uv ); let duv2: vec2f = dpdy( uv ); let dp2perp: vec3f = cross( dp2, normal ); let dp1perp: vec3f = cross( normal, dp1 ); let T: vec3f = dp2perp * duv1.x + dp1perp * duv2.x; let B: vec3f = dp2perp * duv1.y + dp1perp * duv2.y; let denom: f32 = max( dot(T, T), dot(B, B) ); let invmax: f32 = select(uniform.tbnBasis / sqrt( denom ), 0.0, denom == 0.0); dTBN = mat3x3f(T * invmax, -B * invmax, normal ); #else var B: vec3f = cross(normal, vObjectSpaceUpW); var T: vec3f = cross(normal, B); if (dot(B,B) == 0.0) { let major: f32 = max(max(normal.x, normal.y), normal.z); if (normal.x == major) { B = cross(normal, vec3f(0.0, 1.0, 0.0)); T = cross(normal, B); } else if (normal.y == major) { B = cross(normal, vec3f(0.0, 0.0, 1.0)); T = cross(normal, B); } else { B = cross(normal, vec3f(1.0, 0.0, 0.0)); T = cross(normal, B); } } dTBN = mat3x3f(normalize(T), normalize(B), normalize(normal)); #endif }`; var thicknessPS = ` #ifdef STD_THICKNESS_CONSTANT uniform material_thickness: f32; #endif fn getThickness() { dThickness = 1.0; #ifdef STD_THICKNESS_CONSTANT dThickness = dThickness * uniform.material_thickness; #endif #ifdef STD_THICKNESS_TEXTURE dThickness = dThickness * textureSampleBias({STD_THICKNESS_TEXTURE_NAME}, {STD_THICKNESS_TEXTURE_NAME}Sampler, {STD_THICKNESS_TEXTURE_UV}, uniform.textureBias).{STD_THICKNESS_TEXTURE_CHANNEL}; #endif #ifdef STD_THICKNESS_VERTEX dThickness = dThickness * saturate(vVertexColor.{STD_THICKNESS_VERTEX_CHANNEL}); #endif } `; var tonemappingPS = ` #if (TONEMAP == NONE) #include "tonemappingNonePS" #elif TONEMAP == FILMIC #include "tonemappingFilmicPS" #elif TONEMAP == LINEAR #include "tonemappingLinearPS" #elif TONEMAP == HEJL #include "tonemappingHejlPS" #elif TONEMAP == ACES #include "tonemappingAcesPS" #elif TONEMAP == ACES2 #include "tonemappingAces2PS" #elif TONEMAP == NEUTRAL #include "tonemappingNeutralPS" #endif `; var tonemappingAcesPS = ` uniform exposure: f32; fn toneMap(color: vec3f) -> vec3f { let tA: f32 = 2.51; let tB: f32 = 0.03; let tC: f32 = 2.43; let tD: f32 = 0.59; let tE: f32 = 0.14; let x: vec3f = color * uniform.exposure; return (x * (tA * x + tB)) / (x * (tC * x + tD) + tE); } `; var tonemappingAces2PS = ` uniform exposure: f32; const ACESInputMat: mat3x3f = mat3x3f( vec3f(0.59719, 0.35458, 0.04823), vec3f(0.07600, 0.90834, 0.01566), vec3f(0.02840, 0.13383, 0.83777) ); const ACESOutputMat: mat3x3f = mat3x3f( vec3f( 1.60475, -0.53108, -0.07367), vec3f(-0.10208, 1.10813, -0.00605), vec3f(-0.00327, -0.07276, 1.07602) ); fn RRTAndODTFit(v: vec3f) -> vec3f { let a: vec3f = v * (v + vec3f(0.0245786)) - vec3f(0.000090537); let b: vec3f = v * (vec3f(0.983729) * v + vec3f(0.4329510)) + vec3f(0.238081); return a / b; } fn toneMap(color: vec3f) -> vec3f { var c: vec3f = color * (uniform.exposure / 0.6); c = c * ACESInputMat; c = RRTAndODTFit(c); c = c * ACESOutputMat; return clamp(c, vec3f(0.0), vec3f(1.0)); } `; var tonemappingFilmicPS = ` const A: f32 = 0.15; const B: f32 = 0.50; const C: f32 = 0.10; const D: f32 = 0.20; const E: f32 = 0.02; const F: f32 = 0.30; const W: f32 = 11.2; uniform exposure: f32; fn uncharted2Tonemap(x: vec3f) -> vec3f { return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - vec3f(E / F); } fn toneMap(color: vec3f) -> vec3f { var c: vec3f = uncharted2Tonemap(color * uniform.exposure); let whiteScale: vec3f = vec3f(1.0) / uncharted2Tonemap(vec3f(W, W, W)); c *= whiteScale; return c; } `; var tonemappingHejlPS = ` uniform exposure: f32; fn toneMap(color: vec3f) -> vec3f { let A: f32 = 0.22; let B: f32 = 0.3; let C: f32 = 0.1; let D: f32 = 0.2; let E: f32 = 0.01; let F: f32 = 0.3; let Scl: f32 = 1.25; let adjusted_color = color * uniform.exposure; let h = max(vec3f(0.0), adjusted_color - vec3f(0.004)); return (h * ((Scl * A) * h + Scl * vec3f(C * B)) + Scl * vec3f(D * E)) / (h * (A * h + vec3f(B)) + vec3f(D * F)) - Scl * vec3f(E / F); } `; var tonemappingLinearPS = ` uniform exposure: f32; fn toneMap(color: vec3f) -> vec3f { return color * uniform.exposure; } `; var tonemappingNeutralPS = ` uniform exposure: f32; fn toneMap(col: vec3f) -> vec3f { var color = col * uniform.exposure; let startCompression = 0.8 - 0.04; let desaturation = 0.15; let x = min(color.r, min(color.g, color.b)); let offset = select(0.04, x - 6.25 * x * x, x < 0.08); color -= vec3f(offset); let peak = max(color.r, max(color.g, color.b)); if (peak < startCompression) { return color; } let d = 1.0 - startCompression; let newPeak = 1.0 - d * d / (peak + d - startCompression); color *= newPeak / peak; let g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0); return mix(color, vec3f(newPeak), vec3f(g)); } `; var tonemappingNonePS = ` fn toneMap(color: vec3f) -> vec3f { return color; } `; var transformVS = ` #ifdef PIXELSNAP uniform uScreenSize: vec4f; #endif #ifdef SCREENSPACE uniform projectionFlipY: f32; #endif fn evalWorldPosition(vertexPosition: vec3f, modelMatrix: mat4x4f) -> vec4f { var localPos: vec3f = getLocalPosition(vertexPosition); #ifdef NINESLICED var localPosXZ: vec2f = localPos.xz; localPosXZ = localPosXZ * uniform.outerScale; let positiveUnitOffset: vec2f = clamp(vertexPosition.xz, vec2f(0.0), vec2f(1.0)); let negativeUnitOffset: vec2f = clamp(-vertexPosition.xz, vec2f(0.0), vec2f(1.0)); localPosXZ = localPosXZ + (-positiveUnitOffset * uniform.innerOffset.xy + negativeUnitOffset * uniform.innerOffset.zw) * vertex_texCoord0.xy; dTiledUvGlobal = (localPosXZ - uniform.outerScale + uniform.innerOffset.xy) * -0.5 + 1.0; localPosXZ = localPosXZ * -0.5; localPos = vec3f(localPosXZ.x, localPosXZ.y, localPos.y); #endif var posW: vec4f = modelMatrix * vec4f(localPos, 1.0); #ifdef SCREENSPACE posW = vec4f(posW.xy, 0.0, 1.0); #endif return posW; } fn getPosition() -> vec4f { dModelMatrix = getModelMatrix(); let posW: vec4f = evalWorldPosition(vertex_position.xyz, dModelMatrix); dPositionW = posW.xyz; var screenPos: vec4f; #ifdef UV1LAYOUT screenPos = vec4f(vertex_texCoord1.xy * 2.0 - 1.0, 0.5, 1.0); screenPos.y *= -1.0; #else #ifdef SCREENSPACE screenPos = posW; screenPos.y *= uniform.projectionFlipY; #else screenPos = uniform.matrix_viewProjection * posW; #endif #ifdef PIXELSNAP screenPos.xy = (screenPos.xy * 0.5) + 0.5; screenPos.xy *= uniforms.uScreenSize.xy; screenPos.xy = floor(screenPos.xy); screenPos.xy *= uniforms.uScreenSize.zw; screenPos.xy = (screenPos.xy * 2.0) - 1.0; #endif #endif return screenPos; } fn getWorldPosition() -> vec3f { return dPositionW; } `; var transformCoreVS = ` attribute vertex_position: vec4f; uniform matrix_viewProjection: mat4x4f; uniform matrix_model: mat4x4f; #ifdef MORPHING uniform morph_tex_params: vec2f; attribute morph_vertex_id: u32; fn getTextureMorphCoords() -> vec2i { var textureSize: vec2i = vec2i(uniform.morph_tex_params); var morphGridV: i32 = i32(morph_vertex_id) / textureSize.x; var morphGridU: i32 = i32(morph_vertex_id) - (morphGridV * textureSize.x); morphGridV = textureSize.y - morphGridV - 1; return vec2i(morphGridU, morphGridV); } #ifdef MORPHING_POSITION #ifdef MORPHING_INT uniform aabbSize: vec3f; uniform aabbMin: vec3f; var morphPositionTex: texture_2d; #else var morphPositionTex: texture_2d; #endif #endif #endif #ifdef defined(BATCH) #include "skinBatchVS" fn getModelMatrix() -> mat4x4f { return getBoneMatrix(vertex_boneIndices); } #elif defined(SKIN) #include "skinVS" fn getModelMatrix() -> mat4x4f { return uniform.matrix_model * getSkinMatrix(vertex_boneIndices, vertex_boneWeights); } #elif defined(INSTANCING) #include "transformInstancingVS" #else fn getModelMatrix() -> mat4x4f { return uniform.matrix_model; } #endif fn getLocalPosition(vertexPosition: vec3f) -> vec3f { var localPos: vec3f = vertexPosition; #ifdef MORPHING_POSITION var morphUV: vec2i = getTextureMorphCoords(); #ifdef MORPHING_INT var morphPos: vec3f = vec3f(textureLoad(morphPositionTex, morphUV, 0).xyz) / 65535.0 * uniform.aabbSize + uniform.aabbMin; #else var morphPos: vec3f = textureLoad(morphPositionTex, morphUV, 0).xyz; #endif localPos += morphPos; #endif return localPos; } `; var transformInstancingVS = ` attribute instance_line1: vec4f; attribute instance_line2: vec4f; attribute instance_line3: vec4f; attribute instance_line4: vec4f; fn getModelMatrix() -> mat4x4f { return uniform.matrix_model * mat4x4f(instance_line1, instance_line2, instance_line3, instance_line4); } `; var transmissionPS = ` #ifdef STD_REFRACTION_CONSTANT uniform material_refraction: f32; #endif fn getRefraction() { var refraction: f32 = 1.0; #ifdef STD_REFRACTION_CONSTANT refraction = uniform.material_refraction; #endif #ifdef STD_REFRACTION_TEXTURE refraction = refraction * textureSampleBias({STD_REFRACTION_TEXTURE_NAME}, {STD_REFRACTION_TEXTURE_NAME}Sampler, {STD_REFRACTION_TEXTURE_UV}, uniform.textureBias).{STD_REFRACTION_TEXTURE_CHANNEL}; #endif #ifdef STD_REFRACTION_VERTEX refraction = refraction * saturate(vVertexColor.{STD_REFRACTION_VERTEX_CHANNEL}); #endif dTransmission = refraction; } `; var twoSidedLightingPS = ` uniform twoSidedLightingNegScaleFactor: f32; fn handleTwoSidedLighting() { dTBN[2] = dTBN[2] * select(-uniform.twoSidedLightingNegScaleFactor, uniform.twoSidedLightingNegScaleFactor, pcFrontFacing); } `; var uv0VS = ` #ifdef NINESLICED fn getUv0() -> vec2f { var uv = vertex_position.xz; let positiveUnitOffset = clamp(vertex_position.xz, vec2f(0.0, 0.0), vec2f(1.0, 1.0)); let negativeUnitOffset = clamp(-vertex_position.xz, vec2f(0.0, 0.0), vec2f(1.0, 1.0)); uv = uv + ((-positiveUnitOffset * uniform.innerOffset.xy) + (negativeUnitOffset * uniform.innerOffset.zw)) * vertex_texCoord0.xy; uv = uv * -0.5 + vec2f(0.5, 0.5); uv = uv * uniform.atlasRect.zw + uniform.atlasRect.xy; dMaskGlobal = vertex_texCoord0.xy; return uv; } #else fn getUv0() -> vec2f { return vertex_texCoord0; } #endif `; var uv1VS = ` fn getUv1() -> vec2f { return vertex_texCoord1; } `; var uvTransformVS = ` output.vUV{TRANSFORM_UV_{i}}_{TRANSFORM_ID_{i}} = vec2f( dot(vec3f(uv{TRANSFORM_UV_{i}}, 1), uniform.{TRANSFORM_NAME_{i}}0), dot(vec3f(uv{TRANSFORM_UV_{i}}, 1), uniform.{TRANSFORM_NAME_{i}}1) ); `; var uvTransformUniformsPS = ` uniform {TRANSFORM_NAME_{i}}0: vec3f; uniform {TRANSFORM_NAME_{i}}1: vec3f; `; var viewDirPS = ` fn getViewDir() { dViewDirW = normalize(uniform.view_position - vPositionW); } `; const shaderChunksWGSL = { alphaTestPS, ambientPS, anisotropyPS, aoPS, aoDiffuseOccPS, aoSpecOccPS, bakeDirLmEndPS, bakeLmEndPS, basePS, baseNineSlicedPS, baseNineSlicedTiledPS, bayerPS, blurVSMPS, clearCoatPS, clearCoatGlossPS, clearCoatNormalPS, clusteredLightCookiesPS, clusteredLightShadowsPS, clusteredLightUtilsPS, clusteredLightPS, combinePS, cookieBlit2DPS, cookieBlitCubePS, cookieBlitVS, cubeMapProjectPS, cubeMapRotatePS, debugOutputPS, debugProcessFrontendPS, detailModesPS, diffusePS, decodePS, emissivePS, encodePS, endPS, envAtlasPS, envProcPS, falloffInvSquaredPS, falloffLinearPS, floatAsUintPS, fogPS, fresnelSchlickPS, frontendCodePS: '', frontendDeclPS: '', fullscreenQuadVS, gammaPS, glossPS, quadVS, indirectCoreCS, immediateLinePS, immediateLineVS, iridescenceDiffractionPS, iridescencePS, iridescenceThicknessPS, iorPS, lightDeclarationPS, lightDiffuseLambertPS, lightDirPointPS, lightEvaluationPS, lightFunctionLightPS, lightFunctionShadowPS, lightingPS, lightmapAddPS, lightmapPS, lightSpecularAnisoGGXPS, lightSpecularGGXPS, lightSpecularBlinnPS, lightSheenPS, linearizeDepthPS, litForwardBackendPS, litForwardDeclarationPS, litForwardMainPS, litForwardPostCodePS, litForwardPreCodePS, litMainPS, litMainVS, litOtherMainPS, litShaderArgsPS, litShaderCorePS, litShadowMainPS, litUserDeclarationPS: '', litUserDeclarationVS: '', litUserCodePS: '', litUserCodeVS: '', litUserMainStartPS: '', litUserMainStartVS: '', litUserMainEndPS: '', litUserMainEndVS: '', ltcPS, metalnessPS, metalnessModulatePS, morphPS, morphVS, msdfPS, msdfVS, normalVS, normalCoreVS, normalMapPS, opacityPS, opacityDitherPS, outputPS, outputAlphaPS, outputTex2DPS, sheenPS, sheenGlossPS, parallaxPS, pickPS, reflDirPS, reflDirAnisoPS, reflectionCCPS, reflectionCubePS, reflectionEnvHQPS, reflectionEnvPS, reflectionSpherePS, reflectionSheenPS, refractionCubePS, refractionDynamicPS, reprojectPS, reprojectVS, screenDepthPS, shadowCascadesPS, shadowEVSMPS, shadowPCF1PS, shadowPCF3PS, shadowPCF5PS, shadowSoftPS, skinBatchVS, skinVS, skyboxPS, skyboxVS, specularPS, sphericalPS, specularityFactorPS, spotPS, startNineSlicedPS, startNineSlicedTiledPS, stdDeclarationPS, stdFrontEndPS, TBNPS, thicknessPS, tonemappingPS, tonemappingAcesPS, tonemappingAces2PS, tonemappingFilmicPS, tonemappingHejlPS, tonemappingLinearPS, tonemappingNeutralPS, tonemappingNonePS, transformVS, transformCoreVS, transformInstancingVS, transmissionPS, twoSidedLightingPS, uv0VS, uv1VS, uvTransformVS, uvTransformUniformsPS, viewDirPS, webgpuPS, webgpuVS }; class AppBase extends EventHandler { init(appOptions) { const { assetPrefix, batchManager, componentSystems, elementInput, gamepads, graphicsDevice, keyboard, lightmapper, mouse, resourceHandlers, scriptsOrder, scriptPrefix, soundManager, touch, xr } = appOptions; this.graphicsDevice = graphicsDevice; ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_GLSL).add(shaderChunksGLSL); ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_WGSL).add(shaderChunksWGSL); this._initDefaultMaterial(); this._initProgramLibrary(); this.stats = new ApplicationStats(graphicsDevice); this._soundManager = soundManager; this.scene = new Scene(graphicsDevice); this._registerSceneImmediate(this.scene); this.assets = new AssetRegistry(this.loader); if (assetPrefix) this.assets.prefix = assetPrefix; this.bundles = new BundleRegistry(this.assets); this.scriptsOrder = scriptsOrder || []; this.defaultLayerWorld = new Layer({ name: 'World', id: LAYERID_WORLD }); this.defaultLayerDepth = new Layer({ name: 'Depth', id: LAYERID_DEPTH, enabled: false, opaqueSortMode: SORTMODE_NONE }); this.defaultLayerSkybox = new Layer({ name: 'Skybox', id: LAYERID_SKYBOX, opaqueSortMode: SORTMODE_NONE }); this.defaultLayerUi = new Layer({ name: 'UI', id: LAYERID_UI, transparentSortMode: SORTMODE_MANUAL }); this.defaultLayerImmediate = new Layer({ name: 'Immediate', id: LAYERID_IMMEDIATE, opaqueSortMode: SORTMODE_NONE }); const defaultLayerComposition = new LayerComposition('default'); defaultLayerComposition.pushOpaque(this.defaultLayerWorld); defaultLayerComposition.pushOpaque(this.defaultLayerDepth); defaultLayerComposition.pushOpaque(this.defaultLayerSkybox); defaultLayerComposition.pushTransparent(this.defaultLayerWorld); defaultLayerComposition.pushOpaque(this.defaultLayerImmediate); defaultLayerComposition.pushTransparent(this.defaultLayerImmediate); defaultLayerComposition.pushTransparent(this.defaultLayerUi); this.scene.layers = defaultLayerComposition; AreaLightLuts.createPlaceholder(graphicsDevice); this.renderer = new ForwardRenderer(graphicsDevice, this.scene); if (lightmapper) { this.lightmapper = new lightmapper(graphicsDevice, this.root, this.scene, this.renderer, this.assets); this.once('prerender', this._firstBake, this); } if (batchManager) { this._batcher = new batchManager(graphicsDevice, this.root, this.scene); this.once('prerender', this._firstBatch, this); } this.keyboard = keyboard || null; this.mouse = mouse || null; this.touch = touch || null; this.gamepads = gamepads || null; if (elementInput) { this.elementInput = elementInput; this.elementInput.app = this; } this.xr = xr ? new xr(this) : null; if (this.elementInput) this.elementInput.attachSelectEvents(); this._scriptPrefix = scriptPrefix || ''; if (this.enableBundles) { this.loader.addHandler('bundle', new BundleHandler(this)); } resourceHandlers.forEach((resourceHandler)=>{ const handler = new resourceHandler(this); this.loader.addHandler(handler.handlerType, handler); }); componentSystems.forEach((componentSystem)=>{ this.systems.add(new componentSystem(this)); }); this._visibilityChangeHandler = this.onVisibilityChange.bind(this); if (typeof document !== 'undefined') { if (document.hidden !== undefined) { this._hiddenAttr = 'hidden'; document.addEventListener('visibilitychange', this._visibilityChangeHandler, false); } else if (document.mozHidden !== undefined) { this._hiddenAttr = 'mozHidden'; document.addEventListener('mozvisibilitychange', this._visibilityChangeHandler, false); } else if (document.msHidden !== undefined) { this._hiddenAttr = 'msHidden'; document.addEventListener('msvisibilitychange', this._visibilityChangeHandler, false); } else if (document.webkitHidden !== undefined) { this._hiddenAttr = 'webkitHidden'; document.addEventListener('webkitvisibilitychange', this._visibilityChangeHandler, false); } } this.tick = makeTick(this); } static getApplication(id) { return id ? AppBase._applications[id] : getApplication(); } _initDefaultMaterial() { const material = new StandardMaterial(); material.name = 'Default Material'; setDefaultMaterial(this.graphicsDevice, material); } _initProgramLibrary() { const library = new ProgramLibrary(this.graphicsDevice, new StandardMaterial()); setProgramLibrary(this.graphicsDevice, library); } get soundManager() { return this._soundManager; } get batcher() { return this._batcher; } get fillMode() { return this._fillMode; } get resolutionMode() { return this._resolutionMode; } configure(url, callback) { http.get(url, (err, response)=>{ if (err) { callback(err); return; } const props = response.application_properties; const scenes = response.scenes; const assets = response.assets; this._parseApplicationProperties(props, (err)=>{ this._parseScenes(scenes); this._parseAssets(assets); if (!err) { callback(null); } else { callback(err); } }); }); } preload(callback) { this.fire('preload:start'); const assets = this.assets.list({ preload: true }); if (assets.length === 0) { this.fire('preload:end'); callback(); return; } let loadedCount = 0; const onAssetLoadOrError = ()=>{ loadedCount++; this.fire('preload:progress', loadedCount / assets.length); if (loadedCount === assets.length) { this.fire('preload:end'); callback(); } }; assets.forEach((asset)=>{ if (!asset.loaded) { asset.once('load', onAssetLoadOrError); asset.once('error', onAssetLoadOrError); this.assets.load(asset); } else { onAssetLoadOrError(); } }); } _preloadScripts(sceneData, callback) { callback(); } _parseApplicationProperties(props, callback) { if (typeof props.maxAssetRetries === 'number' && props.maxAssetRetries > 0) { this.loader.enableRetry(props.maxAssetRetries); } if (!props.useDevicePixelRatio) { props.useDevicePixelRatio = props.use_device_pixel_ratio; } if (!props.resolutionMode) { props.resolutionMode = props.resolution_mode; } if (!props.fillMode) { props.fillMode = props.fill_mode; } this._width = props.width; this._height = props.height; if (props.useDevicePixelRatio) { this.graphicsDevice.maxPixelRatio = window.devicePixelRatio; } this.setCanvasResolution(props.resolutionMode, this._width, this._height); this.setCanvasFillMode(props.fillMode, this._width, this._height); if (props.layers && props.layerOrder) { const composition = new LayerComposition('application'); const layers = {}; for(const key in props.layers){ const data = props.layers[key]; data.id = parseInt(key, 10); data.enabled = data.id !== LAYERID_DEPTH; layers[key] = new Layer(data); } for(let i = 0, len = props.layerOrder.length; i < len; i++){ const sublayer = props.layerOrder[i]; const layer = layers[sublayer.layer]; if (!layer) continue; if (sublayer.transparent) { composition.pushTransparent(layer); } else { composition.pushOpaque(layer); } composition.subLayerEnabled[i] = sublayer.enabled; } this.scene.layers = composition; } if (props.batchGroups) { const batcher = this.batcher; if (batcher) { for(let i = 0, len = props.batchGroups.length; i < len; i++){ const grp = props.batchGroups[i]; batcher.addGroup(grp.name, grp.dynamic, grp.maxAabbSize, grp.id, grp.layers); } } } if (props.i18nAssets) { this.i18n.assets = props.i18nAssets; } this._loadLibraries(props.libraries, callback); } _loadLibraries(urls, callback) { const len = urls.length; let count = len; const regex = /^https?:\/\//; if (len) { const onLoad = (err, script)=>{ count--; if (err) { callback(err); } else if (count === 0) { this.onLibrariesLoaded(); callback(null); } }; for(let i = 0; i < len; ++i){ let url = urls[i]; if (!regex.test(url.toLowerCase()) && this._scriptPrefix) { url = path.join(this._scriptPrefix, url); } this.loader.load(url, "script", onLoad); } } else { this.onLibrariesLoaded(); callback(null); } } _parseScenes(scenes) { if (!scenes) return; for(let i = 0; i < scenes.length; i++){ this.scenes.add(scenes[i].name, scenes[i].url); } } _parseAssets(assets) { const list = []; const scriptsIndex = {}; const bundlesIndex = {}; for(let i = 0; i < this.scriptsOrder.length; i++){ const id = this.scriptsOrder[i]; if (!assets[id]) { continue; } scriptsIndex[id] = true; list.push(assets[id]); } if (this.enableBundles) { for(const id in assets){ if (assets[id].type === 'bundle') { bundlesIndex[id] = true; list.push(assets[id]); } } } for(const id in assets){ if (scriptsIndex[id] || bundlesIndex[id]) { continue; } list.push(assets[id]); } for(let i = 0; i < list.length; i++){ const data = list[i]; const asset = new Asset(data.name, data.type, data.file, data.data); asset.id = parseInt(data.id, 10); asset.preload = data.preload ? data.preload : false; asset.loaded = data.type === "script" && data.data && data.data.loadingType > 0; asset.tags.add(data.tags); if (data.i18n) { for(const locale in data.i18n){ asset.addLocalizedAssetId(locale, data.i18n[locale]); } } this.assets.add(asset); } } start() { this.frame = 0; this.fire('start', { timestamp: now(), target: this }); if (!this._librariesLoaded) { this.onLibrariesLoaded(); } this.systems.fire('initialize', this.root); this.fire('initialize'); this.systems.fire('postInitialize', this.root); this.systems.fire('postPostInitialize', this.root); this.fire('postinitialize'); this.requestAnimationFrame(); } requestAnimationFrame() { if (this.xr?.session) { this.frameRequestId = this.xr.session.requestAnimationFrame(this.tick); } else { this.frameRequestId = platform.browser || platform.worker ? requestAnimationFrame(this.tick) : null; } } inputUpdate(dt) { if (this.controller) { this.controller.update(dt); } if (this.mouse) { this.mouse.update(); } if (this.keyboard) { this.keyboard.update(); } if (this.gamepads) { this.gamepads.update(); } } update(dt) { this.frame++; this.graphicsDevice.update(); this.stats.frame.scriptUpdateStart = now(); this.systems.fire(this._inTools ? 'toolsUpdate' : 'update', dt); this.stats.frame.scriptUpdate = now() - this.stats.frame.scriptUpdateStart; this.stats.frame.animUpdateStart = now(); this.systems.fire('animationUpdate', dt); this.stats.frame.animUpdate = now() - this.stats.frame.animUpdateStart; this.stats.frame.scriptPostUpdateStart = now(); this.systems.fire('postUpdate', dt); this.stats.frame.scriptPostUpdate = now() - this.stats.frame.scriptPostUpdateStart; this.fire('update', dt); this.inputUpdate(dt); } render() { this.updateCanvasSize(); this.graphicsDevice.frameStart(); this.fire('prerender'); this.root.syncHierarchy(); if (this._batcher) { this._batcher.updateAll(); } this.renderComposition(this.scene.layers); this.fire('postrender'); this.stats.frame.renderTime = now() - this.stats.frame.renderStart; this.graphicsDevice.frameEnd(); } renderComposition(layerComposition) { this.renderer.update(layerComposition); this.renderer.buildFrameGraph(this.frameGraph, layerComposition); this.frameGraph.render(this.graphicsDevice); } _fillFrameStatsBasic(now, dt, ms) { const stats = this.stats.frame; stats.dt = dt; stats.ms = ms; if (now > stats._timeToCountFrames) { stats.fps = stats._fpsAccum; stats._fpsAccum = 0; stats._timeToCountFrames = now + 1000; } else { stats._fpsAccum++; } this.stats.drawCalls.total = this.graphicsDevice._drawCallsPerFrame; this.graphicsDevice._drawCallsPerFrame = 0; stats.gsplats = this.renderer._gsplatCount; } _fillFrameStats() { let stats = this.stats.frame; stats.cameras = this.renderer._camerasRendered; stats.materials = this.renderer._materialSwitches; stats.shaders = this.graphicsDevice._shaderSwitchesPerFrame; stats.shadowMapUpdates = this.renderer._shadowMapUpdates; stats.shadowMapTime = this.renderer._shadowMapTime; stats.depthMapTime = this.renderer._depthMapTime; stats.forwardTime = this.renderer._forwardTime; const prims = this.graphicsDevice._primsPerFrame; stats.triangles = prims[PRIMITIVE_TRIANGLES] / 3 + Math.max(prims[PRIMITIVE_TRISTRIP] - 2, 0) + Math.max(prims[PRIMITIVE_TRIFAN] - 2, 0); stats.cullTime = this.renderer._cullTime; stats.sortTime = this.renderer._sortTime; stats.skinTime = this.renderer._skinTime; stats.morphTime = this.renderer._morphTime; stats.lightClusters = this.renderer._lightClusters; stats.lightClustersTime = this.renderer._lightClustersTime; stats.otherPrimitives = 0; for(let i = 0; i < prims.length; i++){ if (i < PRIMITIVE_TRIANGLES) { stats.otherPrimitives += prims[i]; } prims[i] = 0; } this.renderer._camerasRendered = 0; this.renderer._materialSwitches = 0; this.renderer._shadowMapUpdates = 0; this.graphicsDevice._shaderSwitchesPerFrame = 0; this.renderer._cullTime = 0; this.renderer._layerCompositionUpdateTime = 0; this.renderer._lightClustersTime = 0; this.renderer._sortTime = 0; this.renderer._skinTime = 0; this.renderer._morphTime = 0; this.renderer._shadowMapTime = 0; this.renderer._depthMapTime = 0; this.renderer._forwardTime = 0; stats = this.stats.drawCalls; stats.forward = this.renderer._forwardDrawCalls; stats.culled = this.renderer._numDrawCallsCulled; stats.depth = 0; stats.shadow = this.renderer._shadowDrawCalls; stats.skinned = this.renderer._skinDrawCalls; stats.immediate = 0; stats.instanced = 0; stats.removedByInstancing = 0; stats.misc = stats.total - (stats.forward + stats.shadow); this.renderer._depthDrawCalls = 0; this.renderer._shadowDrawCalls = 0; this.renderer._forwardDrawCalls = 0; this.renderer._numDrawCallsCulled = 0; this.renderer._skinDrawCalls = 0; this.renderer._immediateRendered = 0; this.renderer._instancedDrawCalls = 0; this.stats.misc.renderTargetCreationTime = this.graphicsDevice.renderTargetCreationTime; stats = this.stats.particles; stats.updatesPerFrame = stats._updatesPerFrame; stats.frameTime = stats._frameTime; stats._updatesPerFrame = 0; stats._frameTime = 0; } setCanvasFillMode(mode, width, height) { this._fillMode = mode; this.resizeCanvas(width, height); } setCanvasResolution(mode, width, height) { this._resolutionMode = mode; if (mode === RESOLUTION_AUTO && width === undefined) { width = this.graphicsDevice.canvas.clientWidth; height = this.graphicsDevice.canvas.clientHeight; } this.graphicsDevice.resizeCanvas(width, height); } isHidden() { return document[this._hiddenAttr]; } onVisibilityChange() { if (this.isHidden()) { if (this._soundManager) { this._soundManager.suspend(); } } else { if (this._soundManager) { this._soundManager.resume(); } } } resizeCanvas(width, height) { if (!this._allowResize) return undefined; if (this.xr && this.xr.session) { return undefined; } const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; if (this._fillMode === FILLMODE_KEEP_ASPECT) { const r = this.graphicsDevice.canvas.width / this.graphicsDevice.canvas.height; const winR = windowWidth / windowHeight; if (r > winR) { width = windowWidth; height = width / r; } else { height = windowHeight; width = height * r; } } else if (this._fillMode === FILLMODE_FILL_WINDOW) { width = windowWidth; height = windowHeight; } this.graphicsDevice.canvas.style.width = `${width}px`; this.graphicsDevice.canvas.style.height = `${height}px`; this.updateCanvasSize(); return { width: width, height: height }; } updateCanvasSize() { if (!this._allowResize || this.xr?.active) { return; } if (this._resolutionMode === RESOLUTION_AUTO) { const canvas = this.graphicsDevice.canvas; this.graphicsDevice.resizeCanvas(canvas.clientWidth, canvas.clientHeight); } } onLibrariesLoaded() { this._librariesLoaded = true; if (this.systems.rigidbody) { this.systems.rigidbody.onLibraryLoaded(); } } applySceneSettings(settings) { let asset; if (this.systems.rigidbody && typeof Ammo !== 'undefined') { const [x, y, z] = settings.physics.gravity; this.systems.rigidbody.gravity.set(x, y, z); } this.scene.applySettings(settings); if (settings.render.hasOwnProperty('skybox')) { if (settings.render.skybox) { asset = this.assets.get(settings.render.skybox); if (asset) { this.setSkybox(asset); } else { this.assets.once(`add:${settings.render.skybox}`, this.setSkybox, this); } } else { this.setSkybox(null); } } } setAreaLightLuts(ltcMat1, ltcMat2) { if (ltcMat1 && ltcMat2) { AreaLightLuts.set(this.graphicsDevice, ltcMat1, ltcMat2); } } setSkybox(asset) { if (asset !== this._skyboxAsset) { const onSkyboxRemoved = ()=>{ this.setSkybox(null); }; const onSkyboxChanged = ()=>{ this.scene.setSkybox(this._skyboxAsset ? this._skyboxAsset.resources : null); }; if (this._skyboxAsset) { this.assets.off(`load:${this._skyboxAsset.id}`, onSkyboxChanged, this); this.assets.off(`remove:${this._skyboxAsset.id}`, onSkyboxRemoved, this); this._skyboxAsset.off('change', onSkyboxChanged, this); } this._skyboxAsset = asset; if (this._skyboxAsset) { this.assets.on(`load:${this._skyboxAsset.id}`, onSkyboxChanged, this); this.assets.once(`remove:${this._skyboxAsset.id}`, onSkyboxRemoved, this); this._skyboxAsset.on('change', onSkyboxChanged, this); if (this.scene.skyboxMip === 0 && !this._skyboxAsset.loadFaces) { this._skyboxAsset.loadFaces = true; } this.assets.load(this._skyboxAsset); } onSkyboxChanged(); } } _firstBake() { this.lightmapper?.bake(null, this.scene.lightmapMode); } _firstBatch() { this.batcher?.generate(); } _processTimestamp(timestamp) { return timestamp; } drawLine(start, end, color, depthTest, layer) { this.scene.drawLine(start, end, color, depthTest, layer); } drawLines(positions, colors, depthTest = true, layer = this.scene.defaultDrawLayer) { this.scene.drawLines(positions, colors, depthTest, layer); } drawLineArrays(positions, colors, depthTest = true, layer = this.scene.defaultDrawLayer) { this.scene.drawLineArrays(positions, colors, depthTest, layer); } drawWireSphere(center, radius, color = Color.WHITE, segments = 20, depthTest = true, layer = this.scene.defaultDrawLayer) { this.scene.immediate.drawWireSphere(center, radius, color, segments, depthTest, layer); } drawWireAlignedBox(minPoint, maxPoint, color = Color.WHITE, depthTest = true, layer = this.scene.defaultDrawLayer, mat) { this.scene.immediate.drawWireAlignedBox(minPoint, maxPoint, color, depthTest, layer, mat); } drawMeshInstance(meshInstance, layer = this.scene.defaultDrawLayer) { this.scene.immediate.drawMesh(null, null, null, meshInstance, layer); } drawMesh(mesh, material, matrix, layer = this.scene.defaultDrawLayer) { this.scene.immediate.drawMesh(material, matrix, mesh, null, layer); } drawQuad(matrix, material, layer = this.scene.defaultDrawLayer) { this.scene.immediate.drawMesh(material, matrix, this.scene.immediate.getQuadMesh(), null, layer); } drawTexture(x, y, width, height, texture, material, layer = this.scene.defaultDrawLayer, filterable = true) { if (filterable === false && !this.graphicsDevice.isWebGPU) { return; } const matrix = new Mat4(); matrix.setTRS(new Vec3(x, y, 0.0), Quat.IDENTITY, new Vec3(width, -height, 0.0)); if (!material) { material = new ShaderMaterial(); material.cull = CULLFACE_NONE; material.setParameter('colorMap', texture); material.shaderDesc = filterable ? this.scene.immediate.getTextureShaderDesc(texture.encoding) : this.scene.immediate.getUnfilterableTextureShaderDesc(); material.update(); } this.drawQuad(matrix, material, layer); } drawDepthTexture(x, y, width, height, layer = this.scene.defaultDrawLayer) { const material = new ShaderMaterial(); material.cull = CULLFACE_NONE; material.shaderDesc = this.scene.immediate.getDepthTextureShaderDesc(); material.update(); this.drawTexture(x, y, width, height, null, material, layer); } destroy() { if (this._inFrameUpdate) { this._destroyRequested = true; return; } const canvasId = this.graphicsDevice.canvas.id; this.fire('destroy', this); this.off('librariesloaded'); this._gsplatSortedEvt?.off(); this._gsplatSortedEvt = null; if (typeof document !== 'undefined') { document.removeEventListener('visibilitychange', this._visibilityChangeHandler, false); document.removeEventListener('mozvisibilitychange', this._visibilityChangeHandler, false); document.removeEventListener('msvisibilitychange', this._visibilityChangeHandler, false); document.removeEventListener('webkitvisibilitychange', this._visibilityChangeHandler, false); } this._visibilityChangeHandler = null; this.root.destroy(); this.root = null; if (this.mouse) { this.mouse.off(); this.mouse.detach(); this.mouse = null; } if (this.keyboard) { this.keyboard.off(); this.keyboard.detach(); this.keyboard = null; } if (this.touch) { this.touch.off(); this.touch.detach(); this.touch = null; } if (this.elementInput) { this.elementInput.detach(); this.elementInput = null; } if (this.gamepads) { this.gamepads.destroy(); this.gamepads = null; } if (this.controller) { this.controller = null; } this.systems.destroy(); if (this.scene.layers) { this.scene.layers.destroy(); } this.bundles.destroy(); this.bundles = null; this.i18n.destroy(); this.i18n = null; const scriptHandler = this.loader.getHandler("script"); scriptHandler?.clearCache(); this.loader.destroy(); this.loader = null; this.systems = null; this.context = null; this.scripts.destroy(); this.scripts = null; this.scenes.destroy(); this.scenes = null; this.lightmapper?.destroy(); this.lightmapper = null; if (this._batcher) { this._batcher.destroy(); this._batcher = null; } this._entityIndex = {}; this.defaultLayerDepth.onDisable = null; this.defaultLayerDepth.onEnable = null; this.defaultLayerDepth = null; this.defaultLayerWorld = null; this.xr?.end(); this.xr?.destroy(); this.renderer.destroy(); this.renderer = null; const assets = this.assets.list(); for(let i = 0; i < assets.length; i++){ assets[i].unload(); assets[i].off(); } this.assets.off(); this.scene.destroy(); this.scene = null; this.graphicsDevice.destroy(); this.graphicsDevice = null; this.tick = null; this.off(); this._soundManager?.destroy(); this._soundManager = null; AppBase._applications[canvasId] = null; if (getApplication() === this) { setApplication(null); } AppBase.cancelTick(this); } static cancelTick(app) { if (app.frameRequestId) { cancelAnimationFrame(app.frameRequestId); app.frameRequestId = undefined; } } getEntityFromIndex(guid) { return this._entityIndex[guid]; } _registerSceneImmediate(scene) { this.on('postrender', scene.immediate.onPostRender, scene.immediate); this._gsplatSortedEvt = scene.on('gsplat:sorted', (sortTime)=>{ this.stats.frame.gsplatSort += sortTime; }); } constructor(canvas){ super(), this._batcher = null, this._destroyRequested = false, this._inFrameUpdate = false, this._librariesLoaded = false, this._fillMode = FILLMODE_KEEP_ASPECT, this._resolutionMode = RESOLUTION_FIXED, this._allowResize = true, this._skyboxAsset = null, this._entityIndex = {}, this._inTools = false, this._scriptPrefix = '', this._time = 0, this.enableBundles = typeof TextDecoder !== 'undefined', this.timeScale = 1, this.maxDeltaTime = 0.1, this.frame = 0, this.frameGraph = new FrameGraph(), this.scriptsOrder = [], this.autoRender = true, this.renderNextFrame = false, this.lightmapper = null, this.loader = new ResourceLoader(this), this.scenes = new SceneRegistry(this), this.scripts = new ScriptRegistry(this), this.systems = new ComponentSystemRegistry(), this.i18n = new I18n(this), this.keyboard = null, this.mouse = null, this.touch = null, this.gamepads = null, this.elementInput = null, this.xr = null; AppBase._applications[canvas.id] = this; setApplication(this); this.root = new Entity(); this.root._enabledInHierarchy = true; } } AppBase._applications = {}; const makeTick = function(_app) { const application = _app; return function(timestamp, xrFrame) { if (!application.graphicsDevice) { return; } if (application.frameRequestId) { application.xr?.session?.cancelAnimationFrame(application.frameRequestId); cancelAnimationFrame(application.frameRequestId); application.frameRequestId = null; } application._inFrameUpdate = true; setApplication(application); const currentTime = application._processTimestamp(timestamp) || now(); const ms = currentTime - (application._time || currentTime); let dt = ms / 1000.0; dt = math.clamp(dt, 0, application.maxDeltaTime); dt *= application.timeScale; application._time = currentTime; application.requestAnimationFrame(); if (application.graphicsDevice.contextLost) { return; } application._fillFrameStatsBasic(currentTime, dt, ms); application.fire('frameupdate', ms); let skipUpdate = false; if (xrFrame) { skipUpdate = !application.xr?.update(xrFrame); application.graphicsDevice.defaultFramebuffer = xrFrame.session.renderState.baseLayer.framebuffer; } else { application.graphicsDevice.defaultFramebuffer = null; } if (!skipUpdate) { application.update(dt); application.fire('framerender'); if (application.autoRender || application.renderNextFrame) { application.render(); application.renderNextFrame = false; } application.fire('frameend'); application.stats.frameEnd(); } application._inFrameUpdate = false; if (application._destroyRequested) { application.destroy(); } }; }; class AppOptions { constructor(){ this.componentSystems = []; this.resourceHandlers = []; } } const tempSphere = new BoundingSphere(); class BakeLight { store() { this.mask = this.light.mask; this.shadowUpdateMode = this.light.shadowUpdateMode; this.enabled = this.light.enabled; this.intensity = this.light.intensity; this.rotation = this.light._node.getLocalRotation().clone(); this.numCascades = this.light.numCascades; this.castShadows = this.light._castShadows; } restore() { const light = this.light; light.mask = this.mask; light.shadowUpdateMode = this.shadowUpdateMode; light.enabled = this.enabled; light.intensity = this.intensity; light._node.setLocalRotation(this.rotation); light.numCascades = this.numCascades; light._castShadows = this.castShadows; } startBake() { this.light.enabled = true; this.light._destroyShadowMap(); this.light.beginFrame(); } endBake(shadowMapCache) { const light = this.light; light.enabled = false; if (light.shadowMap) { if (light.shadowMap.cached) { shadowMapCache.add(light, light.shadowMap); } light.shadowMap = null; } } constructor(scene, light, lightingParams){ this.scene = scene; this.light = light; this.store(); light.numCascades = 1; if (this.scene.clusteredLightingEnabled && !lightingParams.shadowsEnabled) { light.castShadows = false; } if (light.type !== LIGHTTYPE_DIRECTIONAL) { light._node.getWorldTransform(); light.getBoundingSphere(tempSphere); this.lightBounds = new BoundingBox(); this.lightBounds.center.copy(tempSphere.center); this.lightBounds.halfExtents.set(tempSphere.radius, tempSphere.radius, tempSphere.radius); } } } const _tempPoint$1 = new Vec2(); class BakeLightSimple extends BakeLight { get numVirtualLights() { if (this.light.type === LIGHTTYPE_DIRECTIONAL) { return this.light.bakeNumSamples; } return 1; } prepareVirtualLight(index, numVirtualLights) { const light = this.light; light._node.setLocalRotation(this.rotation); if (index > 0) { const directionalSpreadAngle = light.bakeArea; random.circlePointDeterministic(_tempPoint$1, index, numVirtualLights); _tempPoint$1.mulScalar(directionalSpreadAngle * 0.5); light._node.rotateLocal(_tempPoint$1.x, 0, _tempPoint$1.y); } light._node.getWorldTransform(); const gamma = 2.2; const linearIntensity = Math.pow(this.intensity, gamma); light.intensity = Math.pow(linearIntensity / numVirtualLights, 1 / gamma); } constructor(lightmapper, light){ super(lightmapper.scene, light, lightmapper.lightingParams); } } const _tempPoint = new Vec3(); class BakeLightAmbient extends BakeLight { get numVirtualLights() { return this.light.bakeNumSamples; } prepareVirtualLight(index, numVirtualLights) { random.spherePointDeterministic(_tempPoint, index, numVirtualLights, 0, this.scene.ambientBakeSpherePart); this.light._node.lookAt(_tempPoint.mulScalar(-1)); this.light._node.rotateLocal(90, 0, 0); const gamma = 2.2; const fullIntensity = 2 * Math.PI * this.scene.ambientBakeSpherePart; const linearIntensity = Math.pow(fullIntensity, gamma); this.light.intensity = Math.pow(linearIntensity / numVirtualLights, 1 / gamma); } constructor(lightmapper){ const scene = lightmapper.scene; const lightEntity = new Entity('AmbientLight'); lightEntity.addComponent('light', { type: 'directional', affectDynamic: true, affectLightmapped: false, bake: true, bakeNumSamples: scene.ambientBakeNumSamples, castShadows: true, normalOffsetBias: 0.05, shadowBias: 0.2, shadowDistance: 1, shadowResolution: 2048, shadowType: SHADOW_PCF3_32F, color: Color.WHITE, intensity: 1, bakeDir: false }); super(scene, lightEntity.light.light, lightmapper.lightingParams); } } class BakeMeshNode { store() { this.castShadows = this.component.castShadows; } restore() { this.component.castShadows = this.castShadows; } constructor(node, meshInstances = null){ this.node = node; this.component = node.render || node.model; meshInstances = meshInstances || this.component.meshInstances; this.store(); this.meshInstances = meshInstances; this.bounds = null; this.renderTargets = []; } } var glslBilateralDeNoisePS = ` float normpdf3(in vec3 v, in float sigma) { return 0.39894 * exp(-0.5 * dot(v, v) / (sigma * sigma)) / sigma; } vec3 decodeRGBM(vec4 rgbm) { vec3 color = (8.0 * rgbm.a) * rgbm.rgb; return color * color; } float saturate(float x) { return clamp(x, 0.0, 1.0); } vec4 encodeRGBM(vec3 color) { vec4 encoded; encoded.rgb = pow(color.rgb, vec3(0.5)); encoded.rgb *= 1.0 / 8.0; encoded.a = saturate( max( max( encoded.r, encoded.g ), max( encoded.b, 1.0 / 255.0 ) ) ); encoded.a = ceil(encoded.a * 255.0) / 255.0; encoded.rgb /= encoded.a; return encoded; } vec3 decode(vec4 pixel) { #if HDR return pixel.rgb; #else return decodeRGBM(pixel); #endif } bool isUsed(vec4 pixel) { #if HDR return any(greaterThan(pixel.rgb, vec3(0.0))); #else return pixel.a > 0.0; #endif } varying vec2 vUv0; uniform sampler2D source; uniform vec2 pixelOffset; uniform vec2 sigmas; uniform float bZnorm; uniform float kernel[{MSIZE}]; void main(void) { vec4 pixel = texture2DLod(source, vUv0, 0.0); if (!isUsed(pixel)) { gl_FragColor = pixel; return ; } float sigma = sigmas.x; float bSigma = sigmas.y; vec3 pixelHdr = decode(pixel); vec3 accumulatedHdr = vec3(0.0); float accumulatedFactor = 0.000001; const int kSize = ({MSIZE} - 1) / 2; for (int i = -kSize; i <= kSize; ++i) { for (int j = -kSize; j <= kSize; ++j) { vec2 coord = vUv0 + vec2(float(i), float(j)) * pixelOffset; vec4 pix = texture2DLod(source, coord, 0.0); if (isUsed(pix)) { vec3 hdr = decode(pix); float factor = kernel[kSize + j] * kernel[kSize + i]; factor *= normpdf3(hdr - pixelHdr, bSigma) * bZnorm; accumulatedHdr += factor * hdr; accumulatedFactor += factor; } } } vec3 finalHDR = accumulatedHdr / accumulatedFactor; #if HDR gl_FragColor = vec4(finalHDR, 1.0); #else gl_FragColor = encodeRGBM(finalHDR); #endif } `; var glslDilatePS = ` varying vec2 vUv0; uniform sampler2D source; uniform vec2 pixelOffset; bool isUsed(vec4 pixel) { #if HDR return any(greaterThan(pixel.rgb, vec3(0.0))); #else return pixel.a > 0.0; #endif } void main(void) { vec4 c = texture2DLod(source, vUv0, 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 - pixelOffset, 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(0, -pixelOffset.y), 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(pixelOffset.x, -pixelOffset.y), 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(-pixelOffset.x, 0), 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(pixelOffset.x, 0), 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(-pixelOffset.x, pixelOffset.y), 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + vec2(0, pixelOffset.y), 0.0); c = isUsed(c) ? c : texture2DLod(source, vUv0 + pixelOffset, 0.0); gl_FragColor = c; } `; var wgslBilateralDeNoisePS = ` fn normpdf3(v: vec3f, sigma: f32) -> f32 { return 0.39894 * exp(-0.5 * dot(v, v) / (sigma * sigma)) / sigma; } fn decodeRGBM(rgbm: vec4f) -> vec3f { let color = (8.0 * rgbm.a) * rgbm.rgb; return color * color; } fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn encodeRGBM(color: vec3f) -> vec4f { var encoded: vec4f; let rgb_processed = pow(color.rgb, vec3f(0.5)) * (1.0 / 8.0); encoded = vec4f(rgb_processed, 0.0); let max_g_b = max( encoded.g, max( encoded.b, 1.0 / 255.0 ) ); let max_rgb = max( encoded.r, max_g_b ); encoded.a = clamp(max_rgb, 0.0, 1.0); encoded.a = ceil(encoded.a * 255.0) / 255.0; encoded = vec4f(encoded.rgb / encoded.a, encoded.a); return encoded; } fn decode(pixel: vec4f) -> vec3f { #if HDR return pixel.rgb; #else return decodeRGBM(pixel); #endif } fn isUsed(pixel: vec4f) -> bool { #if HDR return any(pixel.rgb > vec3f(0.0)); #else return pixel.a > 0.0; #endif } varying vUv0: vec2f; var source: texture_2d; var sourceSampler: sampler; uniform kernel: array; uniform pixelOffset: vec2f; uniform sigmas: vec2f; uniform bZnorm: f32; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let pixel = textureSampleLevel(source, sourceSampler, input.vUv0, 0.0); if (!isUsed(pixel)) { output.color = pixel; return output; } let sigma = uniform.sigmas.x; let bSigma = uniform.sigmas.y; let pixelHdr = decode(pixel); var accumulatedHdr = vec3f(0.0); var accumulatedFactor = 0.000001; const kSize = ({MSIZE} - 1) / 2; for (var i: i32 = -kSize; i <= kSize; i = i + 1) { for (var j: i32 = -kSize; j <= kSize; j = j + 1) { let coord = input.vUv0 + vec2f(f32(i), f32(j)) * uniform.pixelOffset; let pix = textureSampleLevel(source, sourceSampler, coord, 0.0); if (isUsed(pix)) { let hdr = decode(pix); var factor = uniform.kernel[u32(kSize + j)].element * uniform.kernel[u32(kSize + i)].element; factor = factor * normpdf3(hdr - pixelHdr, bSigma) * uniform.bZnorm; accumulatedHdr = accumulatedHdr + factor * hdr; accumulatedFactor = accumulatedFactor + factor; } } } let finalHDR = accumulatedHdr / accumulatedFactor; #if HDR output.color = vec4f(finalHDR, 1.0); #else output.color = encodeRGBM(finalHDR); #endif return output; } `; var wgslDilatePS = ` varying vUv0: vec2f; var source: texture_2d; var sourceSampler: sampler; uniform pixelOffset: vec2f; fn isUsed(pixel: vec4f) -> bool { #ifdef HDR return any(pixel.rgb > vec3f(0.0)); #else return pixel.a > 0.0; #endif } @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var c: vec4f = textureSampleLevel(source, sourceSampler, input.vUv0, 0.0); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 - uniform.pixelOffset, 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + vec2f(0.0, -uniform.pixelOffset.y), 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + vec2f(uniform.pixelOffset.x, -uniform.pixelOffset.y), 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + vec2f(-uniform.pixelOffset.x, 0.0), 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + vec2f(uniform.pixelOffset.x, 0.0), 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + vec2f(-uniform.pixelOffset.x, uniform.pixelOffset.y), 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + vec2f(0.0, uniform.pixelOffset.y), 0.0), c, isUsed(c)); c = select(textureSampleLevel(source, sourceSampler, input.vUv0 + uniform.pixelOffset, 0.0), c, isUsed(c)); var output: FragmentOutput; output.color = c; return output; } `; const DENOISE_FILTER_SIZE = 15; const lightmapFiltersChunksGLSL = { glslBilateralDeNoisePS, glslDilatePS }; const lightmapFiltersChunksWLSL = { wgslBilateralDeNoisePS, wgslDilatePS }; class LightmapFilters { setSourceTexture(texture) { this.constantTexSource.setValue(texture); } prepare(textureWidth, textureHeight) { this.pixelOffset[0] = 1 / textureWidth; this.pixelOffset[1] = 1 / textureHeight; this.constantPixelOffset.setValue(this.pixelOffset); } prepareDenoise(filterRange, filterSmoothness, bakeHDR) { const index = bakeHDR ? 0 : 1; if (!this.shaderDenoise[index]) { const defines = new Map(); defines.set('{MSIZE}', 15); if (bakeHDR) defines.set('HDR', ''); this.shaderDenoise[index] = ShaderUtils.createShader(this.device, { uniqueName: `lmBilateralDeNoise-${bakeHDR ? 'hdr' : 'rgbm'}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexGLSL: ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).get('fullscreenQuadVS'), vertexWGSL: ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).get('fullscreenQuadVS'), fragmentGLSL: ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).get('glslBilateralDeNoisePS'), fragmentWGSL: ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).get('wgslBilateralDeNoisePS'), fragmentDefines: defines }); this.sigmas = new Float32Array(2); this.constantSigmas = this.device.scope.resolve('sigmas'); this.constantKernel = this.device.scope.resolve('kernel[0]'); this.bZnorm = this.device.scope.resolve('bZnorm'); } this.sigmas[0] = filterRange; this.sigmas[1] = filterSmoothness; this.constantSigmas.setValue(this.sigmas); this.evaluateDenoiseUniforms(filterRange, filterSmoothness); } getDenoise(bakeHDR) { const index = bakeHDR ? 0 : 1; return this.shaderDenoise[index]; } getDilate(device, bakeHDR) { const index = bakeHDR ? 0 : 1; if (!this.shaderDilate[index]) { const define = bakeHDR ? '#define HDR\n' : ''; this.shaderDilate[index] = ShaderUtils.createShader(device, { uniqueName: `lmDilate-${bakeHDR ? 'hdr' : 'rgbm'}`, attributes: { vertex_position: SEMANTIC_POSITION }, vertexGLSL: ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).get('fullscreenQuadVS'), vertexWGSL: ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).get('fullscreenQuadVS'), fragmentGLSL: define + ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).get('glslDilatePS'), fragmentWGSL: define + ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).get('wgslDilatePS') }); } return this.shaderDilate[index]; } evaluateDenoiseUniforms(filterRange, filterSmoothness) { function normpdf(x, sigma) { return 0.39894 * Math.exp(-0.5 * x * x / (sigma * sigma)) / sigma; } this.kernel = this.kernel || new Float32Array(DENOISE_FILTER_SIZE); const kernel = this.kernel; const kSize = Math.floor((DENOISE_FILTER_SIZE - 1) / 2); for(let j = 0; j <= kSize; ++j){ const value = normpdf(j, filterRange); kernel[kSize + j] = value; kernel[kSize - j] = value; } this.constantKernel.setValue(this.kernel); const bZnorm = 1 / normpdf(0.0, filterSmoothness); this.bZnorm.setValue(bZnorm); } constructor(device){ this.shaderDilate = []; this.shaderDenoise = []; this.device = device; ShaderChunks.get(this.device, SHADERLANGUAGE_GLSL).add(lightmapFiltersChunksGLSL); ShaderChunks.get(this.device, SHADERLANGUAGE_WGSL).add(lightmapFiltersChunksWLSL); this.constantTexSource = device.scope.resolve('source'); this.constantPixelOffset = device.scope.resolve('pixelOffset'); this.pixelOffset = new Float32Array(2); this.sigmas = null; this.constantSigmas = null; this.kernel = null; } } class RenderPassLightmapper extends RenderPass { destroy() { this.viewBindGroups.forEach((bg)=>{ bg.defaultUniformBuffer.destroy(); bg.destroy(); }); this.viewBindGroups.length = 0; } execute() { const device = this.device; const { renderer, camera, receivers, renderTarget, worldClusters, lightArray } = this; if (device.supportsUniformBuffers && !renderer.viewUniformFormat) { renderer.initViewBindGroupFormat(renderer.scene.clusteredLightingEnabled); } renderer.renderForwardLayer(camera, renderTarget, null, undefined, SHADER_FORWARD, this.viewBindGroups, { meshInstances: receivers, splitLights: lightArray, lightClusters: worldClusters }); } constructor(device, renderer, camera, worldClusters, receivers, lightArray){ super(device), this.viewBindGroups = []; this.renderer = renderer; this.camera = camera; this.worldClusters = worldClusters; this.receivers = receivers; this.lightArray = lightArray; } } const MAX_LIGHTMAP_SIZE = 2048; const PASS_COLOR = 0; const PASS_DIR = 1; const tempVec$1 = new Vec3(); class Lightmapper { destroy() { LightmapCache.decRef(this.blackTex); this.blackTex = null; LightmapCache.destroy(); this.device = null; this.root = null; this.scene = null; this.renderer = null; this.assets = null; this.camera?.destroy(); this.camera = null; } initBake(device) { this.bakeHDR = this.scene.lightmapPixelFormat !== PIXELFORMAT_RGBA8; if (!this._initCalled) { this._initCalled = true; this.lightmapFilters = new LightmapFilters(device); this.constantBakeDir = device.scope.resolve('bakeDir'); this.materials = []; this.blackTex = new Texture(this.device, { width: 4, height: 4, format: PIXELFORMAT_RGBA8, type: TEXTURETYPE_RGBM, name: 'lightmapBlack' }); LightmapCache.incRef(this.blackTex); const camera = new Camera$1(); camera.clearColor.set(0, 0, 0, 0); camera.clearColorBuffer = true; camera.clearDepthBuffer = false; camera.clearStencilBuffer = false; camera.frustumCulling = false; camera.projection = PROJECTION_ORTHOGRAPHIC; camera.aspectRatio = 1; camera.node = new GraphNode(); this.camera = camera; this.camera.shaderParams.gammaCorrection = GAMMA_NONE; this.camera.shaderParams.toneMapping = TONEMAP_LINEAR; } if (this.scene.clusteredLightingEnabled) { const lightingParams = new LightingParams(device.supportsAreaLights, device.maxTextureSize, ()=>{}); this.lightingParams = lightingParams; const srcParams = this.scene.lighting; lightingParams.shadowsEnabled = srcParams.shadowsEnabled; lightingParams.shadowAtlasResolution = srcParams.shadowAtlasResolution; lightingParams.cookiesEnabled = srcParams.cookiesEnabled; lightingParams.cookieAtlasResolution = srcParams.cookieAtlasResolution; lightingParams.areaLightsEnabled = srcParams.areaLightsEnabled; lightingParams.cells = new Vec3(3, 3, 3); lightingParams.maxLightsPerCell = 4; this.worldClusters = new WorldClusters(device); this.worldClusters.name = 'ClusterLightmapper'; this.shadowLocalClusteredPass = new RenderPassShadowLocalClustered(device, this.renderer.shadowRenderer, this.renderer._shadowRendererLocal); } } finishBake(bakeNodes) { this.materials = []; function destroyRT(rt) { LightmapCache.decRef(rt.colorBuffer); rt.destroy(); } this.renderTargets.forEach((rt)=>{ destroyRT(rt); }); this.renderTargets.clear(); bakeNodes.forEach((node)=>{ node.renderTargets.forEach((rt)=>{ destroyRT(rt); }); node.renderTargets.length = 0; }); this.ambientAOMaterial = null; if (this.worldClusters) { this.worldClusters.destroy(); this.worldClusters = null; } } createMaterialForPass(scene, pass, addAmbient) { const material = new StandardMaterial(); material.name = `lmMaterial-pass:${pass}-ambient:${addAmbient}`; material.setDefine('UV1LAYOUT', ''); material.setDefine('LIT_LIGHTMAP_BAKING', ''); if (pass === PASS_COLOR) { material.setDefine('LIT_LIGHTMAP_BAKING_COLOR', ''); if (addAmbient) { material.setDefine('LIT_LIGHTMAP_BAKING_ADD_AMBIENT', ''); } else { material.ambient = new Color(0, 0, 0); } if (!this.bakeHDR) material.setDefine('LIGHTMAP_RGBM', ''); material.lightMap = this.blackTex; } else { material.setDefine('LIT_LIGHTMAP_BAKING_DIR', ''); material.setDefine('STD_LIGHTMAP_DIR', ''); } material.cull = CULLFACE_NONE; material.forceUv1 = true; material.update(); return material; } createMaterials(device, scene, passCount) { for(let pass = 0; pass < passCount; pass++){ if (!this.passMaterials[pass]) { this.passMaterials[pass] = this.createMaterialForPass(scene, pass, false); } } if (!this.ambientAOMaterial) { this.ambientAOMaterial = this.createMaterialForPass(scene, 0, true); this.ambientAOMaterial.onUpdateShader = function(options) { options.litOptions.lightMapWithoutAmbient = true; options.litOptions.separateAmbient = true; return options; }; } } createTexture(size, name) { return new Texture(this.device, { width: size, height: size, format: this.scene.lightmapPixelFormat, mipmaps: false, type: this.bakeHDR ? TEXTURETYPE_DEFAULT : TEXTURETYPE_RGBM, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: name }); } collectModels(node, bakeNodes, allNodes) { if (!node.enabled) return; let meshInstances; if (node.model?.model && node.model?.enabled) { if (allNodes) allNodes.push(new BakeMeshNode(node)); if (node.model.lightmapped) { if (bakeNodes) { meshInstances = node.model.model.meshInstances; } } } if (node.render?.enabled) { if (allNodes) allNodes.push(new BakeMeshNode(node)); if (node.render.lightmapped) { if (bakeNodes) { meshInstances = node.render.meshInstances; } } } if (meshInstances) { let hasUv1 = true; for(let i = 0; i < meshInstances.length; i++){ if (!meshInstances[i].mesh.vertexBuffer.format.hasUv1) { hasUv1 = false; break; } } if (hasUv1) { const notInstancedMeshInstances = []; for(let i = 0; i < meshInstances.length; i++){ const mesh = meshInstances[i].mesh; if (this._tempSet.has(mesh)) { bakeNodes.push(new BakeMeshNode(node, [ meshInstances[i] ])); } else { notInstancedMeshInstances.push(meshInstances[i]); } this._tempSet.add(mesh); } this._tempSet.clear(); if (notInstancedMeshInstances.length > 0) { bakeNodes.push(new BakeMeshNode(node, notInstancedMeshInstances)); } } } for(let i = 0; i < node._children.length; i++){ this.collectModels(node._children[i], bakeNodes, allNodes); } } prepareShadowCasters(nodes) { const casters = []; for(let n = 0; n < nodes.length; n++){ const component = nodes[n].component; component.castShadows = component.castShadowsLightmap; if (component.castShadowsLightmap) { const meshes = nodes[n].meshInstances; for(let i = 0; i < meshes.length; i++){ meshes[i].visibleThisFrame = true; casters.push(meshes[i]); } } } return casters; } updateTransforms(nodes) { for(let i = 0; i < nodes.length; i++){ const meshInstances = nodes[i].meshInstances; for(let j = 0; j < meshInstances.length; j++){ meshInstances[j].node.getWorldTransform(); } } } calculateLightmapSize(node) { let data; const sizeMult = this.scene.lightmapSizeMultiplier || 16; const scale = tempVec$1; let srcArea, lightmapSizeMultiplier; if (node.model) { lightmapSizeMultiplier = node.model.lightmapSizeMultiplier; if (node.model.asset) { data = this.assets.get(node.model.asset).data; if (data.area) { srcArea = data.area; } } else if (node.model._area) { data = node.model; if (data._area) { srcArea = data._area; } } } else if (node.render) { lightmapSizeMultiplier = node.render.lightmapSizeMultiplier; if (node.render.type !== 'asset') { if (node.render._area) { data = node.render; if (data._area) { srcArea = data._area; } } } } const area = { x: 1, y: 1, z: 1, uv: 1 }; if (srcArea) { area.x = srcArea.x; area.y = srcArea.y; area.z = srcArea.z; area.uv = srcArea.uv; } const areaMult = lightmapSizeMultiplier || 1; area.x *= areaMult; area.y *= areaMult; area.z *= areaMult; const component = node.render || node.model; const bounds = this.computeNodeBounds(component.meshInstances); scale.copy(bounds.halfExtents); let totalArea = area.x * scale.y * scale.z + area.y * scale.x * scale.z + area.z * scale.x * scale.y; totalArea /= area.uv; totalArea = Math.sqrt(totalArea); const lightmapSize = Math.min(math.nextPowerOfTwo(totalArea * sizeMult), this.scene.lightmapMaxResolution || MAX_LIGHTMAP_SIZE); return lightmapSize; } setLightmapping(nodes, value, passCount, shaderDefs) { for(let i = 0; i < nodes.length; i++){ const node = nodes[i]; const meshInstances = node.meshInstances; for(let j = 0; j < meshInstances.length; j++){ const meshInstance = meshInstances[j]; meshInstance.setLightmapped(value); if (value) { if (shaderDefs) { meshInstance._shaderDefs |= shaderDefs; } meshInstance.mask = MASK_AFFECT_LIGHTMAPPED; for(let pass = 0; pass < passCount; pass++){ const tex = node.renderTargets[pass].colorBuffer; tex.minFilter = FILTER_LINEAR; tex.magFilter = FILTER_LINEAR; meshInstance.setRealtimeLightmap(MeshInstance.lightmapParamNames[pass], tex); } } } } } bake(nodes, mode = BAKE_COLORDIR) { const device = this.device; const startTime = now(); this.scene._updateSkyMesh(); this.stats.renderPasses = 0; this.stats.shadowMapTime = 0; this.stats.forwardTime = 0; const startShaders = device._shaderStats.linked; const startFboTime = device._renderTargetCreationTime; const startCompileTime = device._shaderStats.compileTime; const bakeNodes = []; const allNodes = []; if (nodes) { for(let i = 0; i < nodes.length; i++){ this.collectModels(nodes[i], bakeNodes, null); } this.collectModels(this.root, null, allNodes); } else { this.collectModels(this.root, bakeNodes, allNodes); } if (bakeNodes.length > 0) { this.renderer.shadowRenderer.frameUpdate(); const passCount = mode === BAKE_COLORDIR ? 2 : 1; this.setLightmapping(bakeNodes, false, passCount); this.initBake(device); this.bakeInternal(passCount, bakeNodes, allNodes); let shaderDefs = SHADERDEF_LM; if (mode === BAKE_COLORDIR) { shaderDefs |= SHADERDEF_DIRLM; } if (this.scene.ambientBake) { shaderDefs |= SHADERDEF_LMAMBIENT; } this.setLightmapping(bakeNodes, true, passCount, shaderDefs); this.finishBake(bakeNodes); } const nowTime = now(); this.stats.totalRenderTime = nowTime - startTime; this.stats.shadersLinked = device._shaderStats.linked - startShaders; this.stats.compileTime = device._shaderStats.compileTime - startCompileTime; this.stats.fboTime = device._renderTargetCreationTime - startFboTime; this.stats.lightmapCount = bakeNodes.length; } allocateTextures(bakeNodes, passCount) { for(let i = 0; i < bakeNodes.length; i++){ const bakeNode = bakeNodes[i]; const size = this.calculateLightmapSize(bakeNode.node); for(let pass = 0; pass < passCount; pass++){ const tex = this.createTexture(size, `lightmapper_lightmap_${i}`); LightmapCache.incRef(tex); bakeNode.renderTargets[pass] = new RenderTarget({ colorBuffer: tex, depth: false }); } if (!this.renderTargets.has(size)) { const tex = this.createTexture(size, `lightmapper_temp_lightmap_${size}`); LightmapCache.incRef(tex); this.renderTargets.set(size, new RenderTarget({ colorBuffer: tex, depth: false })); } } } prepareLightsToBake(allLights, bakeLights) { if (this.scene.ambientBake) { const ambientLight = new BakeLightAmbient(this); bakeLights.push(ambientLight); } const sceneLights = this.renderer.lights; for(let i = 0; i < sceneLights.length; i++){ const light = sceneLights[i]; const bakeLight = new BakeLightSimple(this, light); allLights.push(bakeLight); if (light.enabled && (light.mask & MASK_BAKE) !== 0) { light.mask = MASK_BAKE | MASK_AFFECT_LIGHTMAPPED | MASK_AFFECT_DYNAMIC; light.shadowUpdateMode = light.type === LIGHTTYPE_DIRECTIONAL ? SHADOWUPDATE_REALTIME : SHADOWUPDATE_THISFRAME; bakeLights.push(bakeLight); } } bakeLights.sort(); } restoreLights(allLights) { for(let i = 0; i < allLights.length; i++){ allLights[i].restore(); } } setupScene() { this.ambientLight.copy(this.scene.ambientLight); if (!this.scene.ambientBake) { this.scene.ambientLight.set(0, 0, 0); } this.renderer.setSceneConstants(); this.device.scope.resolve('ambientBakeOcclusionContrast').setValue(this.scene.ambientBakeOcclusionContrast); this.device.scope.resolve('ambientBakeOcclusionBrightness').setValue(this.scene.ambientBakeOcclusionBrightness); } restoreScene() { this.scene.ambientLight.copy(this.ambientLight); } computeNodeBounds(meshInstances) { const bounds = new BoundingBox(); if (meshInstances.length > 0) { bounds.copy(meshInstances[0].aabb); for(let m = 1; m < meshInstances.length; m++){ bounds.add(meshInstances[m].aabb); } } return bounds; } computeNodesBounds(nodes) { for(let i = 0; i < nodes.length; i++){ const meshInstances = nodes[i].meshInstances; nodes[i].bounds = this.computeNodeBounds(meshInstances); } } computeBounds(meshInstances) { const bounds = new BoundingBox(); for(let i = 0; i < meshInstances.length; i++){ bounds.copy(meshInstances[0].aabb); for(let m = 1; m < meshInstances.length; m++){ bounds.add(meshInstances[m].aabb); } } return bounds; } backupMaterials(meshInstances) { for(let i = 0; i < meshInstances.length; i++){ this.materials[i] = meshInstances[i].material; } } restoreMaterials(meshInstances) { for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].material = this.materials[i]; } } lightCameraPrepare(device, bakeLight) { const light = bakeLight.light; let shadowCam; if (light.type === LIGHTTYPE_SPOT) { const lightRenderData = light.getRenderData(null, 0); shadowCam = lightRenderData.shadowCamera; shadowCam._node.setPosition(light._node.getPosition()); shadowCam._node.setRotation(light._node.getRotation()); shadowCam._node.rotateLocal(-90, 0, 0); shadowCam.projection = PROJECTION_PERSPECTIVE; shadowCam.nearClip = light.attenuationEnd / 1000; shadowCam.farClip = light.attenuationEnd; shadowCam.aspectRatio = 1; shadowCam.fov = light._outerConeAngle * 2; this.renderer.updateCameraFrustum(shadowCam); } return shadowCam; } lightCameraPrepareAndCull(bakeLight, bakeNode, shadowCam, casterBounds) { const light = bakeLight.light; let lightAffectsNode = true; if (light.type === LIGHTTYPE_DIRECTIONAL) { tempVec$1.copy(casterBounds.center); tempVec$1.y += casterBounds.halfExtents.y; this.camera.node.setPosition(tempVec$1); this.camera.node.setEulerAngles(-90, 0, 0); this.camera.nearClip = 0; this.camera.farClip = casterBounds.halfExtents.y * 2; const frustumSize = Math.max(casterBounds.halfExtents.x, casterBounds.halfExtents.z); this.camera.orthoHeight = frustumSize; } else { if (!bakeLight.lightBounds.intersects(bakeNode.bounds)) { lightAffectsNode = false; } } if (light.type === LIGHTTYPE_SPOT) { let nodeVisible = false; const meshInstances = bakeNode.meshInstances; for(let i = 0; i < meshInstances.length; i++){ if (meshInstances[i]._isVisible(shadowCam)) { nodeVisible = true; break; } } if (!nodeVisible) { lightAffectsNode = false; } } return lightAffectsNode; } setupLightArray(lightArray, light) { lightArray[LIGHTTYPE_DIRECTIONAL].length = 0; lightArray[LIGHTTYPE_OMNI].length = 0; lightArray[LIGHTTYPE_SPOT].length = 0; lightArray[light.type][0] = light; light.visibleThisFrame = true; } renderShadowMap(comp, shadowMapRendered, casters, bakeLight) { const light = bakeLight.light; const isClustered = this.scene.clusteredLightingEnabled; const castShadow = light.castShadows && (!isClustered || this.scene.lighting.shadowsEnabled); if (!shadowMapRendered && castShadow) { if (!light.shadowMap && !isClustered) { light.shadowMap = this.shadowMapCache.get(this.device, light); } if (light.type === LIGHTTYPE_DIRECTIONAL) { this.renderer._shadowRendererDirectional.cull(light, comp, this.camera, casters); const shadowPass = this.renderer._shadowRendererDirectional.getLightRenderPass(light, this.camera); shadowPass?.render(); } else { this.renderer._shadowRendererLocal.cull(light, comp, casters); if (isClustered) { this.shadowLocalClusteredPass.update([ light ]); if (this.shadowLocalClusteredPass.enabled) { this.shadowLocalClusteredPass.render(); } } else { const faceCount = light.numShadowFaces; const applyVsm = light._type === LIGHTTYPE_SPOT; for(let face = 0; face < faceCount; face++){ const renderPass = new RenderPassShadowLocalNonClustered(this.device, this.renderer.shadowRenderer, light, face, applyVsm); renderPass.render(); } } } } return true; } postprocessTextures(device, bakeNodes, passCount) { const numDilates2x = 1; const dilateShader = this.lightmapFilters.getDilate(device, this.bakeHDR); let denoiseShader; const filterLightmap = this.scene.lightmapFilterEnabled; if (filterLightmap) { this.lightmapFilters.prepareDenoise(this.scene.lightmapFilterRange, this.scene.lightmapFilterSmoothness, this.bakeHDR); denoiseShader = this.lightmapFilters.getDenoise(this.bakeHDR); } device.setBlendState(BlendState.NOBLEND); device.setDepthState(DepthState.NODEPTH); device.setStencilState(null, null); for(let node = 0; node < bakeNodes.length; node++){ const bakeNode = bakeNodes[node]; for(let pass = 0; pass < passCount; pass++){ const nodeRT = bakeNode.renderTargets[pass]; const lightmap = nodeRT.colorBuffer; const tempRT = this.renderTargets.get(lightmap.width); const tempTex = tempRT.colorBuffer; this.lightmapFilters.prepare(lightmap.width, lightmap.height); for(let i = 0; i < numDilates2x; i++){ this.lightmapFilters.setSourceTexture(lightmap); const bilateralFilterEnabled = filterLightmap && pass === 0 && i === 0; drawQuadWithShader(device, tempRT, bilateralFilterEnabled ? denoiseShader : dilateShader); this.lightmapFilters.setSourceTexture(tempTex); drawQuadWithShader(device, nodeRT, dilateShader); } } } } bakeInternal(passCount, bakeNodes, allNodes) { const scene = this.scene; const comp = scene.layers; const device = this.device; const clusteredLightingEnabled = scene.clusteredLightingEnabled; this.createMaterials(device, scene, passCount); this.setupScene(); comp._update(); this.computeNodesBounds(bakeNodes); this.allocateTextures(bakeNodes, passCount); this.renderer.collectLights(comp); const allLights = [], bakeLights = []; this.prepareLightsToBake(allLights, bakeLights); this.updateTransforms(allNodes); const casters = this.prepareShadowCasters(allNodes); this.renderer.updateCpuSkinMatrices(casters); this.renderer.gpuUpdate(casters); const casterBounds = this.computeBounds(casters); let i, j, rcv, m; for(i = 0; i < bakeNodes.length; i++){ const bakeNode = bakeNodes[i]; rcv = bakeNode.meshInstances; for(j = 0; j < rcv.length; j++){ m = rcv[j]; m.setLightmapped(false); m.mask = MASK_BAKE; m.setRealtimeLightmap(MeshInstance.lightmapParamNames[0], this.blackTex); m.setRealtimeLightmap(MeshInstance.lightmapParamNames[1], this.blackTex); } } for(j = 0; j < bakeLights.length; j++){ bakeLights[j].light.enabled = false; } const lightArray = [ [], [], [] ]; let pass, node; let shadersUpdatedOn1stPass = false; for(i = 0; i < bakeLights.length; i++){ const bakeLight = bakeLights[i]; const isAmbientLight = bakeLight instanceof BakeLightAmbient; const isDirectional = bakeLight.light.type === LIGHTTYPE_DIRECTIONAL; let numVirtualLights = bakeLight.numVirtualLights; if (passCount > 1 && numVirtualLights > 1 && bakeLight.light.bakeDir) { numVirtualLights = 1; } for(let virtualLightIndex = 0; virtualLightIndex < numVirtualLights; virtualLightIndex++){ if (numVirtualLights > 1) { bakeLight.prepareVirtualLight(virtualLightIndex, numVirtualLights); } bakeLight.startBake(); let shadowMapRendered = false; const shadowCam = this.lightCameraPrepare(device, bakeLight); for(node = 0; node < bakeNodes.length; node++){ const bakeNode = bakeNodes[node]; rcv = bakeNode.meshInstances; const lightAffectsNode = this.lightCameraPrepareAndCull(bakeLight, bakeNode, shadowCam, casterBounds); if (!lightAffectsNode) { continue; } this.setupLightArray(lightArray, bakeLight.light); const clusterLights = isDirectional ? [] : [ bakeLight.light ]; if (clusteredLightingEnabled) { this.renderer.lightTextureAtlas.update(clusterLights, this.lightingParams); } shadowMapRendered = this.renderShadowMap(comp, shadowMapRendered, casters, bakeLight); if (clusteredLightingEnabled) { this.worldClusters.update(clusterLights, this.lightingParams); } this.backupMaterials(rcv); for(pass = 0; pass < passCount; pass++){ if (pass > 0 && virtualLightIndex > 0) { break; } if (isAmbientLight && pass > 0) { break; } const nodeRT = bakeNode.renderTargets[pass]; const lightmapSize = bakeNode.renderTargets[pass].colorBuffer.width; const tempRT = this.renderTargets.get(lightmapSize); const tempTex = tempRT.colorBuffer; if (pass === 0) { shadersUpdatedOn1stPass = scene.updateShaders; } else if (shadersUpdatedOn1stPass) { scene.updateShaders = true; } let passMaterial = this.passMaterials[pass]; if (isAmbientLight) { const lastVirtualLightForPass = virtualLightIndex + 1 === numVirtualLights; if (lastVirtualLightForPass && pass === 0) { passMaterial = this.ambientAOMaterial; } } for(j = 0; j < rcv.length; j++){ rcv[j].material = passMaterial; } this.renderer.updateShaders(rcv); if (pass === PASS_DIR) { this.constantBakeDir.setValue(bakeLight.light.bakeDir ? 1 : 0); } const renderPass = new RenderPassLightmapper(device, this.renderer, this.camera, clusteredLightingEnabled ? this.worldClusters : null, rcv, lightArray); renderPass.init(tempRT); renderPass.colorOps.clear = true; renderPass.colorOps.clearValue.copy(this.camera.clearColor); renderPass.render(); renderPass.destroy(); bakeNode.renderTargets[pass] = tempRT; this.renderTargets.set(lightmapSize, nodeRT); for(j = 0; j < rcv.length; j++){ m = rcv[j]; m.setRealtimeLightmap(MeshInstance.lightmapParamNames[pass], tempTex); m._shaderDefs |= SHADERDEF_LM; } } this.restoreMaterials(rcv); } bakeLight.endBake(this.shadowMapCache); } } this.postprocessTextures(device, bakeNodes, passCount); for(node = 0; node < allNodes.length; node++){ allNodes[node].restore(); } this.restoreLights(allLights); this.restoreScene(); if (!clusteredLightingEnabled) { this.shadowMapCache.clear(); } } constructor(device, root, scene, renderer, assets){ this.device = device; this.root = root; this.scene = scene; this.renderer = renderer; this.assets = assets; this.shadowMapCache = renderer.shadowMapCache; this._tempSet = new Set(); this._initCalled = false; this.passMaterials = []; this.ambientAOMaterial = null; this.fog = ''; this.ambientLight = new Color(); this.renderTargets = new Map(); this.stats = { renderPasses: 0, lightmapCount: 0, totalRenderTime: 0, forwardTime: 0, fboTime: 0, shadowMapTime: 0, compileTime: 0, shadersLinked: 0 }; } } class Component extends EventHandler { static _buildAccessors(obj, schema) { schema.forEach((descriptor)=>{ const name = typeof descriptor === 'object' ? descriptor.name : descriptor; Object.defineProperty(obj, name, { get: function() { return this.data[name]; }, set: function(value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); }, configurable: true }); }); obj._accessorsBuilt = true; } buildAccessors(schema) { Component._buildAccessors(this, schema); } onSetEnabled(name, oldValue, newValue) { if (oldValue !== newValue) { if (this.entity.enabled) { if (newValue) { this.onEnable(); } else { this.onDisable(); } } } } onEnable() {} onDisable() {} onPostStateChange() {} get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) {} get enabled() { return true; } constructor(system, entity){ super(); this.system = system; this.entity = entity; if (this.system.schema && !this._accessorsBuilt) { this.buildAccessors(this.system.schema); } this.on('set', function(name, oldValue, newValue) { this.fire(`set_${name}`, name, oldValue, newValue); }); this.on('set_enabled', this.onSetEnabled, this); } } Component.order = 0; class ComponentSystem extends EventHandler { addComponent(entity, data = {}) { const component = new this.ComponentType(this, entity); const componentData = new this.DataType(); this.store[entity.getGuid()] = { entity: entity, data: componentData }; entity[this.id] = component; entity.c[this.id] = component; this.initializeComponentData(component, data, []); this.fire('add', entity, component); return component; } removeComponent(entity) { const id = this.id; const record = this.store[entity.getGuid()]; const component = entity.c[id]; component.fire('beforeremove'); this.fire('beforeremove', entity, component); delete this.store[entity.getGuid()]; entity[id] = undefined; delete entity.c[id]; this.fire('remove', entity, record.data); } cloneComponent(entity, clone) { const src = this.store[entity.getGuid()]; return this.addComponent(clone, src.data); } initializeComponentData(component, data = {}, properties) { for(let i = 0, len = properties.length; i < len; i++){ const descriptor = properties[i]; let name, type; if (typeof descriptor === 'object') { name = descriptor.name; type = descriptor.type; } else { name = descriptor; type = undefined; } let value = data[name]; if (value !== undefined) { if (type !== undefined) { value = convertValue(value, type); } component[name] = value; } else { component[name] = component.data[name]; } } if (component.enabled && component.entity.enabled) { component.onEnable(); } } getPropertiesOfType(type) { const matchingProperties = []; const schema = this.schema || []; schema.forEach((descriptor)=>{ if (descriptor && typeof descriptor === 'object' && descriptor.type === type) { matchingProperties.push(descriptor); } }); return matchingProperties; } destroy() { this.off(); } constructor(app){ super(); this.app = app; this.store = {}; this.schema = []; } } function convertValue(value, type) { if (!value) { return value; } switch(type){ case 'rgb': if (value instanceof Color) { return value.clone(); } return new Color(value[0], value[1], value[2]); case 'rgba': if (value instanceof Color) { return value.clone(); } return new Color(value[0], value[1], value[2], value[3]); case 'vec2': if (value instanceof Vec2) { return value.clone(); } return new Vec2(value[0], value[1]); case 'vec3': if (value instanceof Vec3) { return value.clone(); } return new Vec3(value[0], value[1], value[2]); case 'vec4': if (value instanceof Vec4) { return value.clone(); } return new Vec4(value[0], value[1], value[2], value[3]); case 'boolean': case 'number': case 'string': return value; case 'entity': return value; default: throw new Error(`Could not convert unhandled type: ${type}`); } } const INTERPOLATION_STEP = 0; const INTERPOLATION_LINEAR = 1; const INTERPOLATION_CUBIC = 2; class AnimCache { update(time, input) { if (time < this._left || time >= this._right) { const len = input.length; if (!len) { this._left = -Infinity; this._right = Infinity; this._len = 0; this._recip = 0; this._p0 = this._p1 = 0; } else { if (time < input[0]) { this._left = -Infinity; this._right = input[0]; this._len = 0; this._recip = 0; this._p0 = this._p1 = 0; } else if (time >= input[len - 1]) { this._left = input[len - 1]; this._right = Infinity; this._len = 0; this._recip = 0; this._p0 = this._p1 = len - 1; } else { const index = this._findKey(time, input); this._left = input[index]; this._right = input[index + 1]; this._len = this._right - this._left; const diff = 1.0 / this._len; this._recip = isFinite(diff) ? diff : 0; this._p0 = index; this._p1 = index + 1; } } } this._t = this._recip === 0 ? 0 : (time - this._left) * this._recip; this._hermite.valid = false; } _findKey(time, input) { let index = 0; while(time >= input[index + 1]){ index++; } return index; } eval(result, interpolation, output) { const data = output._data; const comp = output._components; const idx0 = this._p0 * comp; if (interpolation === INTERPOLATION_STEP) { for(let i = 0; i < comp; ++i){ result[i] = data[idx0 + i]; } } else { const t = this._t; const idx1 = this._p1 * comp; switch(interpolation){ case INTERPOLATION_LINEAR: for(let i = 0; i < comp; ++i){ result[i] = math.lerp(data[idx0 + i], data[idx1 + i], t); } break; case INTERPOLATION_CUBIC: { const hermite = this._hermite; if (!hermite.valid) { const t2 = t * t; const twot = t + t; const omt = 1 - t; const omt2 = omt * omt; hermite.valid = true; hermite.p0 = (1 + twot) * omt2; hermite.m0 = t * omt2; hermite.p1 = t2 * (3 - twot); hermite.m1 = t2 * (t - 1); } const p0 = (this._p0 * 3 + 1) * comp; const m0 = (this._p0 * 3 + 2) * comp; const p1 = (this._p1 * 3 + 1) * comp; const m1 = (this._p1 * 3 + 0) * comp; for(let i = 0; i < comp; ++i){ result[i] = hermite.p0 * data[p0 + i] + hermite.m0 * data[m0 + i] * this._len + hermite.p1 * data[p1 + i] + hermite.m1 * data[m1 + i] * this._len; } break; } } } } constructor(){ this._left = Infinity; this._right = -Infinity; this._len = 0; this._recip = 0; this._p0 = 0; this._p1 = 0; this._t = 0; this._hermite = { valid: false, p0: 0, m0: 0, p1: 0, m1: 0 }; } } class AnimSnapshot { constructor(animTrack){ this._name = `${animTrack.name}Snapshot`; this._time = -1; this._cache = []; this._results = []; for(let i = 0; i < animTrack._inputs.length; ++i){ this._cache[i] = new AnimCache(); } const curves = animTrack._curves; const outputs = animTrack._outputs; for(let i = 0; i < curves.length; ++i){ const curve = curves[i]; const output = outputs[curve._output]; const storage = []; for(let j = 0; j < output._components; ++j){ storage[j] = 0; } this._results[i] = storage; } } } class AnimClip { set name(name) { this._name = name; } get name() { return this._name; } set track(track) { this._track = track; this._snapshot = new AnimSnapshot(track); } get track() { return this._track; } get snapshot() { return this._snapshot; } set time(time) { this._time = time; this.alignCursorToCurrentTime(); } get time() { return this._time; } set speed(speed) { const signChanged = Math.sign(speed) !== Math.sign(this._speed); this._speed = speed; if (signChanged) { this.alignCursorToCurrentTime(); } } get speed() { return this._speed; } set loop(loop) { this._loop = loop; } get loop() { return this._loop; } set blendWeight(blendWeight) { this._blendWeight = blendWeight; } get blendWeight() { return this._blendWeight; } set blendOrder(blendOrder) { this._blendOrder = blendOrder; } get blendOrder() { return this._blendOrder; } set eventCursor(value) { this._eventCursor = value; } get eventCursor() { return this._eventCursor; } get eventCursorEnd() { return this.isReverse ? 0 : this._track.events.length - 1; } get nextEvent() { return this._track.events[this._eventCursor]; } get isReverse() { return this._speed < 0; } nextEventAheadOfTime(time) { if (!this.nextEvent) return false; return this.isReverse ? this.nextEvent.time <= time : this.nextEvent.time >= time; } nextEventBehindTime(time) { if (!this.nextEvent) return false; if (time === this.track.duration) { return this.isReverse ? this.nextEvent.time >= time : this.nextEvent.time <= time; } return this.isReverse ? this.nextEvent.time > time : this.nextEvent.time < time; } resetEventCursor() { this._eventCursor = this.isReverse ? this._track.events.length - 1 : 0; } moveEventCursor() { this._eventCursor += this.isReverse ? -1 : 1; if (this._eventCursor >= this.track.events.length) { this._eventCursor = 0; } else if (this._eventCursor < 0) { this._eventCursor = this.track.events.length - 1; } } clipFrameTime(frameEndTime) { const eventFrame = AnimClip.eventFrame; eventFrame.start = 0; eventFrame.end = frameEndTime; eventFrame.residual = 0; if (this.isReverse) { if (frameEndTime < 0) { eventFrame.start = this.track.duration; eventFrame.end = 0; eventFrame.residual = frameEndTime + this.track.duration; } } else { if (frameEndTime > this.track.duration) { eventFrame.start = 0; eventFrame.end = this.track.duration; eventFrame.residual = frameEndTime - this.track.duration; } } } alignCursorToCurrentTime() { this.resetEventCursor(); while(this.nextEventBehindTime(this._time) && this._eventCursor !== this.eventCursorEnd){ this.moveEventCursor(); } } fireNextEvent() { this._eventHandler.fire(this.nextEvent.name, { track: this.track, ...this.nextEvent }); this.moveEventCursor(); } fireNextEventInFrame(frameStartTime, frameEndTime) { if (this.nextEventAheadOfTime(frameStartTime) && this.nextEventBehindTime(frameEndTime)) { this.fireNextEvent(); return true; } return false; } activeEventsForFrame(frameStartTime, frameEndTime) { const eventFrame = AnimClip.eventFrame; this.clipFrameTime(frameEndTime); const initialCursor = this.eventCursor; while(this.fireNextEventInFrame(frameStartTime, eventFrame.end)){ if (initialCursor === this.eventCursor) { break; } } if (this.loop && Math.abs(eventFrame.residual) > 0) { this.activeEventsForFrame(eventFrame.start, eventFrame.residual); } } progressForTime(time) { return time * this._speed / this._track.duration; } _update(deltaTime) { if (this._playing) { let time = this._time; const duration = this._track.duration; const speed = this._speed; const loop = this._loop; if (this._track.events.length > 0 && duration > 0) { this.activeEventsForFrame(time, time + speed * deltaTime); } time += speed * deltaTime; if (speed >= 0) { if (time > duration) { if (loop) { time = time % duration || 0; } else { time = this._track.duration; this.pause(); } } } else { if (time < 0) { if (loop) { time = duration + (time % duration || 0); } else { time = 0; this.pause(); } } } this._time = time; } if (this._time !== this._snapshot._time) { this._track.eval(this._time, this._snapshot); } } play() { this._playing = true; this._time = 0; } stop() { this._playing = false; this._time = 0; } pause() { this._playing = false; } resume() { this._playing = true; } reset() { this._time = 0; } constructor(track, time, speed, playing, loop, eventHandler){ this._name = track.name; this._track = track; this._snapshot = new AnimSnapshot(track); this._playing = playing; this._time = time; this._speed = speed; this._loop = loop; this._blendWeight = 1.0; this._blendOrder = 0.0; this._eventHandler = eventHandler; this.alignCursorToCurrentTime(); } } AnimClip.eventFrame = { start: 0, end: 0, residual: 0 }; const ANIM_INTERRUPTION_NONE = 'NONE'; const ANIM_INTERRUPTION_PREV = 'PREV_STATE'; const ANIM_INTERRUPTION_NEXT = 'NEXT_STATE'; const ANIM_INTERRUPTION_PREV_NEXT = 'PREV_STATE_NEXT_STATE'; const ANIM_INTERRUPTION_NEXT_PREV = 'NEXT_STATE_PREV_STATE'; const ANIM_GREATER_THAN = 'GREATER_THAN'; const ANIM_LESS_THAN = 'LESS_THAN'; const ANIM_GREATER_THAN_EQUAL_TO = 'GREATER_THAN_EQUAL_TO'; const ANIM_LESS_THAN_EQUAL_TO = 'LESS_THAN_EQUAL_TO'; const ANIM_EQUAL_TO = 'EQUAL_TO'; const ANIM_NOT_EQUAL_TO = 'NOT_EQUAL_TO'; const ANIM_PARAMETER_INTEGER = 'INTEGER'; const ANIM_PARAMETER_FLOAT = 'FLOAT'; const ANIM_PARAMETER_BOOLEAN = 'BOOLEAN'; const ANIM_PARAMETER_TRIGGER = 'TRIGGER'; const ANIM_BLEND_1D = '1D'; const ANIM_BLEND_2D_DIRECTIONAL = '2D_DIRECTIONAL'; const ANIM_BLEND_2D_CARTESIAN = '2D_CARTESIAN'; const ANIM_BLEND_DIRECT = 'DIRECT'; const ANIM_STATE_START = 'START'; const ANIM_STATE_END = 'END'; const ANIM_STATE_ANY = 'ANY'; const ANIM_CONTROL_STATES = [ ANIM_STATE_START, ANIM_STATE_END, ANIM_STATE_ANY ]; const ANIM_LAYER_OVERWRITE = 'OVERWRITE'; const ANIM_LAYER_ADDITIVE = 'ADDITIVE'; class AnimBlend { static dot(a, b) { const len = a.length; let result = 0; for(let i = 0; i < len; ++i){ result += a[i] * b[i]; } return result; } static normalize(a) { let l = AnimBlend.dot(a, a); if (l > 0) { l = 1.0 / Math.sqrt(l); const len = a.length; for(let i = 0; i < len; ++i){ a[i] *= l; } } } static set(a, b, type) { const len = a.length; if (type === 'quaternion') { let l = AnimBlend.dot(b, b); if (l > 0) { l = 1.0 / Math.sqrt(l); } for(let i = 0; i < len; ++i){ a[i] = b[i] * l; } } else { for(let i = 0; i < len; ++i){ a[i] = b[i]; } } } static blendVec(a, b, t, additive) { const it = additive ? 1.0 : 1.0 - t; const len = a.length; for(let i = 0; i < len; ++i){ a[i] = a[i] * it + b[i] * t; } } static blendQuat(a, b, t, additive) { const len = a.length; const it = additive ? 1.0 : 1.0 - t; if (AnimBlend.dot(a, b) < 0) { t = -t; } for(let i = 0; i < len; ++i){ a[i] = a[i] * it + b[i] * t; } if (!additive) { AnimBlend.normalize(a); } } static blend(a, b, t, type, additive) { if (type === 'quaternion') { AnimBlend.blendQuat(a, b, t, additive); } else { AnimBlend.blendVec(a, b, t, additive); } } static stableSort(a, lessFunc) { const len = a.length; for(let i = 0; i < len - 1; ++i){ for(let j = i + 1; j < len; ++j){ if (lessFunc(a[j], a[i])) { const tmp = a[i]; a[i] = a[j]; a[j] = tmp; } } } } } class AnimTargetValue { get _normalizeWeights() { return this._component.normalizeWeights; } getWeight(index) { if (this.dirty) this.updateWeights(); if (this._normalizeWeights && this.totalWeight === 0 || !this.mask[index]) { return 0; } else if (this._normalizeWeights) { return this.weights[index] / this.totalWeight; } return math.clamp(this.weights[index], 0, 1); } _layerBlendType(index) { return this._component.layers[index].blendType; } setMask(index, value) { this.mask[index] = value; if (this._normalizeWeights) { if (this._component.layers[index].blendType === ANIM_LAYER_OVERWRITE) { this.mask = this.mask.fill(0, 0, index); } this.dirty = true; } } updateWeights() { this.totalWeight = 0; for(let i = 0; i < this.weights.length; i++){ this.weights[i] = this._component.layers[i].weight; this.totalWeight += this.mask[i] * this.weights[i]; } this.dirty = false; } updateValue(index, value) { if (this.counter === 0) { AnimBlend.set(this.value, AnimTargetValue.IDENTITY_QUAT_ARR, this.valueType); if (!this._normalizeWeights) { AnimBlend.blend(this.value, this.baseValue, 1, this.valueType); } } if (!this.mask[index] || this.getWeight(index) === 0) return; if (this._layerBlendType(index) === ANIM_LAYER_ADDITIVE && !this._normalizeWeights) { if (this.valueType === AnimTargetValue.TYPE_QUAT) { const v = AnimTargetValue.q1.set(this.value[0], this.value[1], this.value[2], this.value[3]); const aV1 = AnimTargetValue.q2.set(this.baseValue[0], this.baseValue[1], this.baseValue[2], this.baseValue[3]); const aV2 = AnimTargetValue.q3.set(value[0], value[1], value[2], value[3]); const aV = aV1.invert().mul(aV2); aV.slerp(Quat.IDENTITY, aV, this.getWeight(index)); v.mul(aV); AnimTargetValue.quatArr[0] = v.x; AnimTargetValue.quatArr[1] = v.y; AnimTargetValue.quatArr[2] = v.z; AnimTargetValue.quatArr[3] = v.w; AnimBlend.set(this.value, AnimTargetValue.quatArr, this.valueType); } else { AnimTargetValue.vecArr[0] = value[0] - this.baseValue[0]; AnimTargetValue.vecArr[1] = value[1] - this.baseValue[1]; AnimTargetValue.vecArr[2] = value[2] - this.baseValue[2]; AnimBlend.blend(this.value, AnimTargetValue.vecArr, this.getWeight(index), this.valueType, true); } } else { AnimBlend.blend(this.value, value, this.getWeight(index), this.valueType); } if (this.setter) this.setter(this.value); } unbind() { if (this.setter) { this.setter(this.baseValue); } } constructor(component, type){ this._component = component; this.mask = new Int8Array(component.layers.length); this.weights = new Float32Array(component.layers.length); this.totalWeight = 0; this.counter = 0; this.layerCounter = 0; this.valueType = type; this.dirty = true; this.value = type === AnimTargetValue.TYPE_QUAT ? [ 0, 0, 0, 1 ] : [ 0, 0, 0 ]; this.baseValue = null; this.setter = null; } } AnimTargetValue.TYPE_QUAT = 'quaternion'; AnimTargetValue.TYPE_VEC3 = 'vector3'; AnimTargetValue.q1 = new Quat(); AnimTargetValue.q2 = new Quat(); AnimTargetValue.q3 = new Quat(); AnimTargetValue.quatArr = [ 0, 0, 0, 1 ]; AnimTargetValue.vecArr = [ 0, 0, 0 ]; AnimTargetValue.IDENTITY_QUAT_ARR = [ 0, 0, 0, 1 ]; class AnimEvaluator { get clips() { return this._clips; } addClip(clip) { const targets = this._targets; const binder = this._binder; const curves = clip.track.curves; const snapshot = clip.snapshot; const inputs = []; const outputs = []; for(let i = 0; i < curves.length; ++i){ const curve = curves[i]; const paths = curve.paths; for(let j = 0; j < paths.length; ++j){ const path = paths[j]; const resolved = binder.resolve(path); let target = targets[resolved && resolved.targetPath || null]; if (!target && resolved) { target = { target: resolved, value: [], curves: 0, blendCounter: 0 }; for(let k = 0; k < target.target.components; ++k){ target.value.push(0); } targets[resolved.targetPath] = target; if (binder.animComponent) { if (!binder.animComponent.targets[resolved.targetPath]) { let type; if (resolved.targetPath.substring(resolved.targetPath.length - 13) === 'localRotation') { type = AnimTargetValue.TYPE_QUAT; } else { type = AnimTargetValue.TYPE_VEC3; } binder.animComponent.targets[resolved.targetPath] = new AnimTargetValue(binder.animComponent, type); } binder.animComponent.targets[resolved.targetPath].layerCounter++; binder.animComponent.targets[resolved.targetPath].setMask(binder.layerIndex, 1); } } if (target) { target.curves++; inputs.push(snapshot._results[i]); outputs.push(target); } } } this._clips.push(clip); this._inputs.push(inputs); this._outputs.push(outputs); } removeClip(index) { const targets = this._targets; const binder = this._binder; const clips = this._clips; const clip = clips[index]; const curves = clip.track.curves; for(let i = 0; i < curves.length; ++i){ const curve = curves[i]; const paths = curve.paths; for(let j = 0; j < paths.length; ++j){ const path = paths[j]; const target = this._binder.resolve(path); if (target) { target.curves--; if (target.curves === 0) { binder.unresolve(path); delete targets[target.targetPath]; if (binder.animComponent) { binder.animComponent.targets[target.targetPath].layerCounter--; } } } } } clips.splice(index, 1); this._inputs.splice(index, 1); this._outputs.splice(index, 1); } removeClips() { while(this._clips.length > 0){ this.removeClip(0); } } updateClipTrack(name, animTrack) { this._clips.forEach((clip)=>{ if (clip.name.includes(name)) { clip.track = animTrack; } }); this.rebind(); } findClip(name) { const clips = this._clips; for(let i = 0; i < clips.length; ++i){ const clip = clips[i]; if (clip.name === name) { return clip; } } return null; } rebind() { this._binder.rebind(); this._targets = {}; const clips = [ ...this.clips ]; this.removeClips(); clips.forEach((clip)=>{ this.addClip(clip); }); } assignMask(mask) { return this._binder.assignMask(mask); } update(deltaTime, outputAnimation = true) { const clips = this._clips; const order = clips.map((c, i)=>{ return i; }); AnimBlend.stableSort(order, (a, b)=>{ return clips[a].blendOrder < clips[b].blendOrder; }); for(let i = 0; i < order.length; ++i){ const index = order[i]; const clip = clips[index]; const inputs = this._inputs[index]; const outputs = this._outputs[index]; const blendWeight = clip.blendWeight; if (blendWeight > 0.0) { clip._update(deltaTime); } if (!outputAnimation) break; let input; let output; let value; if (blendWeight >= 1.0) { for(let j = 0; j < inputs.length; ++j){ input = inputs[j]; output = outputs[j]; value = output.value; AnimBlend.set(value, input, output.target.type); output.blendCounter++; } } else if (blendWeight > 0.0) { for(let j = 0; j < inputs.length; ++j){ input = inputs[j]; output = outputs[j]; value = output.value; if (output.blendCounter === 0) { AnimBlend.set(value, input, output.target.type); } else { AnimBlend.blend(value, input, blendWeight, output.target.type); } output.blendCounter++; } } } const targets = this._targets; const binder = this._binder; for(const path in targets){ if (targets.hasOwnProperty(path)) { const target = targets[path]; if (binder.animComponent && target.target.usesLayerBlending) { const animTarget = binder.animComponent.targets[path]; if (animTarget.counter === animTarget.layerCounter) { animTarget.counter = 0; } if (!animTarget.path) { animTarget.path = path; animTarget.baseValue = target.target.get(); animTarget.setter = target.target.set; } animTarget.updateValue(binder.layerIndex, target.value); animTarget.counter++; } else { target.target.set(target.value); } target.blendCounter = 0; } } this._binder.update(deltaTime); } constructor(binder){ this._binder = binder; this._clips = []; this._inputs = []; this._outputs = []; this._targets = {}; } } class AnimEvents { get events() { return this._events; } constructor(events){ this._events = [ ...events ]; this._events.sort((a, b)=>a.time - b.time); } } class AnimTrack { get name() { return this._name; } get duration() { return this._duration; } get inputs() { return this._inputs; } get outputs() { return this._outputs; } get curves() { return this._curves; } set events(animEvents) { this._animEvents = animEvents; } get events() { return this._animEvents.events; } eval(time, snapshot) { snapshot._time = time; const inputs = this._inputs; const outputs = this._outputs; const curves = this._curves; const cache = snapshot._cache; const results = snapshot._results; for(let i = 0; i < inputs.length; ++i){ cache[i].update(time, inputs[i]._data); } for(let i = 0; i < curves.length; ++i){ const curve = curves[i]; const output = outputs[curve._output]; const result = results[i]; cache[curve._input].eval(result, curve._interpolation, output); } } constructor(name, duration, inputs, outputs, curves, animEvents = new AnimEvents([])){ this._name = name; this._duration = duration; this._inputs = inputs; this._outputs = outputs; this._curves = curves; this._animEvents = animEvents; } } AnimTrack.EMPTY = Object.freeze(new AnimTrack('empty', Number.MAX_VALUE, [], [], [])); class AnimBinder { static joinPath(pathSegments, character) { character = character || '.'; const escape = function(string) { return string.replace(/\\/g, '\\\\').replace(new RegExp(`\\${character}`, 'g'), `\\${character}`); }; return pathSegments.map(escape).join(character); } static splitPath(path, character) { character = character || '.'; const result = []; let curr = ''; let i = 0; while(i < path.length){ let c = path[i++]; if (c === '\\' && i < path.length) { c = path[i++]; if (c === '\\' || c === character) { curr += c; } else { curr += `\\${c}`; } } else if (c === character) { result.push(curr); curr = ''; } else { curr += c; } } if (curr.length > 0) { result.push(curr); } return result; } static encode(entityPath, component, propertyPath) { return `${Array.isArray(entityPath) ? entityPath.join('/') : entityPath}/${component}/${Array.isArray(propertyPath) ? propertyPath.join('/') : propertyPath}`; } resolve(path) { return null; } unresolve(path) {} update(deltaTime) {} } class AnimTarget { get set() { return this._set; } get get() { return this._get; } get type() { return this._type; } get components() { return this._components; } get targetPath() { return this._targetPath; } get isTransform() { return this._isTransform; } get isWeight() { return this._isWeight; } get usesLayerBlending() { return this._isTransform || this._isWeight; } constructor(func, type, components, targetPath){ if (func.set) { this._set = func.set; this._get = func.get; } else { this._set = func; } this._type = type; this._components = components; this._targetPath = targetPath; this._isTransform = this._targetPath.substring(this._targetPath.length - 13) === 'localRotation' || this._targetPath.substring(this._targetPath.length - 13) === 'localPosition' || this._targetPath.substring(this._targetPath.length - 10) === 'localScale'; this._isWeight = this._targetPath.indexOf('weight.') !== -1; } } class DefaultAnimBinder { _isPathActive(path) { if (!this._mask) return true; const rootNodeNames = [ path.entityPath[0], this.graph.name ]; for(let j = 0; j < rootNodeNames.length; ++j){ let currEntityPath = rootNodeNames[j]; if (this._isPathInMask(currEntityPath, path.entityPath.length === 1)) return true; for(let i = 1; i < path.entityPath.length; i++){ currEntityPath += `/${path.entityPath[i]}`; if (this._isPathInMask(currEntityPath, i === path.entityPath.length - 1)) return true; } } return false; } findNode(path) { if (!this._isPathActive(path)) { return null; } let node; if (this.graph) { node = this.graph.findByPath(path.entityPath); if (!node) { node = this.graph.findByPath(path.entityPath.slice(1)); } } if (!node) { node = this.nodes[path.entityPath[path.entityPath.length - 1] || '']; } return node; } static createAnimTarget(func, type, valueCount, node, propertyPath, componentType) { const targetPath = AnimBinder.encode(node.path, componentType ? componentType : 'entity', propertyPath); return new AnimTarget(func, type, valueCount, targetPath); } resolve(path) { const encodedPath = AnimBinder.encode(path.entityPath, path.component, path.propertyPath); let target = this.targetCache[encodedPath]; if (target) return target; const node = this.findNode(path); if (!node) { return null; } const handler = this.handlers[path.propertyPath]; if (!handler) { return null; } target = handler(node); if (!target) { return null; } this.targetCache[encodedPath] = target; if (!this.nodeCounts[node.path]) { this.activeNodes.push(node); this.nodeCounts[node.path] = 1; } else { this.nodeCounts[node.path]++; } return target; } unresolve(path) { if (path.component !== 'graph') { return; } const node = this.nodes[path.entityPath[path.entityPath.length - 1] || '']; this.nodeCounts[node.path]--; if (this.nodeCounts[node.path] === 0) { const activeNodes = this.activeNodes; const i = activeNodes.indexOf(node.node); const len = activeNodes.length; if (i < len - 1) { activeNodes[i] = activeNodes[len - 1]; } activeNodes.pop(); } } update(deltaTime) { const activeNodes = this.activeNodes; for(let i = 0; i < activeNodes.length; ++i){ activeNodes[i]._dirtifyLocal(); } } assignMask(mask) { if (mask !== this._mask) { this._mask = mask; return true; } return false; } constructor(graph){ this._isPathInMask = (path, checkMaskValue)=>{ const maskItem = this._mask[path]; if (!maskItem) return false; else if (maskItem.children || checkMaskValue && maskItem.value !== false) return true; return false; }; this.graph = graph; if (!graph) return; this._mask = null; const nodes = {}; const flatten = function(node) { nodes[node.name] = node; for(let i = 0; i < node.children.length; ++i){ flatten(node.children[i]); } }; flatten(graph); this.nodes = nodes; this.targetCache = {}; const findMeshInstances = function(node) { let object = node; while(object && !(object instanceof Entity)){ object = object.parent; } let meshInstances; if (object) { if (object.render) { meshInstances = object.render.meshInstances; } else if (object.model) { meshInstances = object.model.meshInstances; } } return meshInstances; }; this.nodeCounts = {}; this.activeNodes = []; this.handlers = { 'localPosition': function(node) { const object = node.localPosition; const func = function(value) { object.set(...value); }; return DefaultAnimBinder.createAnimTarget(func, 'vector', 3, node, 'localPosition'); }, 'localRotation': function(node) { const object = node.localRotation; const func = function(value) { object.set(...value); }; return DefaultAnimBinder.createAnimTarget(func, 'quaternion', 4, node, 'localRotation'); }, 'localScale': function(node) { const object = node.localScale; const func = function(value) { object.set(...value); }; return DefaultAnimBinder.createAnimTarget(func, 'vector', 3, node, 'localScale'); }, 'weight': function(node, weightName) { if (weightName.indexOf('name.') === 0) { weightName = weightName.replace('name.', ''); } else { weightName = Number(weightName); } const meshInstances = findMeshInstances(node); const instances = []; if (meshInstances) { for(let i = 0; i < meshInstances.length; ++i){ if (meshInstances[i].node.name === node.name && meshInstances[i].morphInstance) { instances.push(meshInstances[i].morphInstance); } } } if (instances.length > 0) { const func = { set: (value)=>{ for(let i = 0; i < instances.length; ++i){ instances[i].setWeight(weightName, value[0]); } }, get: ()=>{ return [ instances[0].getWeight(weightName) ]; } }; return DefaultAnimBinder.createAnimTarget(func, 'number', 1, node, `weight.${weightName}`); } return null; }, 'materialTexture': (node, textureName)=>{ const meshInstances = findMeshInstances(node); if (meshInstances) { let meshInstance; for(let i = 0; i < meshInstances.length; ++i){ if (meshInstances[i].node.name === node.name) { meshInstance = meshInstances[i]; break; } } if (meshInstance) { const func = (value)=>{ const textureAsset = this.animComponent.system.app.assets.get(value[0]); if (textureAsset && textureAsset.resource && textureAsset.type === 'texture') { meshInstance.material[textureName] = textureAsset.resource; meshInstance.material.update(); } }; return DefaultAnimBinder.createAnimTarget(func, 'vector', 1, node, 'materialTexture', 'material'); } } return null; } }; } } class AnimationComponent extends Component { set animations(value) { this._animations = value; this.onSetAnimations(); } get animations() { return this._animations; } set assets(value) { const assets = this._assets; if (assets && assets.length) { for(let i = 0; i < assets.length; i++){ if (assets[i]) { const asset = this.system.app.assets.get(assets[i]); if (asset) { asset.off('change', this.onAssetChanged, this); asset.off('remove', this.onAssetRemoved, this); const animName = this.animationsIndex[asset.id]; if (this.currAnim === animName) { this._stopCurrentAnimation(); } delete this.animations[animName]; delete this.animationsIndex[asset.id]; } } } } this._assets = value; const assetIds = value.map((value)=>{ return value instanceof Asset ? value.id : value; }); this.loadAnimationAssets(assetIds); } get assets() { return this._assets; } set currentTime(currentTime) { if (this.skeleton) { this.skeleton.currentTime = currentTime; this.skeleton.addTime(0); this.skeleton.updateGraph(); } if (this.animEvaluator) { const clips = this.animEvaluator.clips; for(let i = 0; i < clips.length; ++i){ clips[i].time = currentTime; } } } get currentTime() { if (this.skeleton) { return this.skeleton._time; } if (this.animEvaluator) { const clips = this.animEvaluator.clips; if (clips.length > 0) { return clips[clips.length - 1].time; } } return 0; } get duration() { if (this.currAnim) { return this.animations[this.currAnim].duration; } return 0; } set loop(value) { this._loop = value; if (this.skeleton) { this.skeleton.looping = value; } if (this.animEvaluator) { for(let i = 0; i < this.animEvaluator.clips.length; ++i){ this.animEvaluator.clips[i].loop = value; } } } get loop() { return this._loop; } play(name, blendTime = 0) { if (!this.enabled || !this.entity.enabled) { return; } if (!this.animations[name]) { return; } this.prevAnim = this.currAnim; this.currAnim = name; if (this.model) { if (!this.skeleton && !this.animEvaluator) { this._createAnimationController(); } const prevAnim = this.animations[this.prevAnim]; const currAnim = this.animations[this.currAnim]; this.blending = blendTime > 0 && !!this.prevAnim; if (this.blending) { this.blend = 0; this.blendSpeed = 1 / blendTime; } if (this.skeleton) { if (this.blending) { this.fromSkel.animation = prevAnim; this.fromSkel.addTime(this.skeleton._time); this.toSkel.animation = currAnim; } else { this.skeleton.animation = currAnim; } } if (this.animEvaluator) { const animEvaluator = this.animEvaluator; if (this.blending) { while(animEvaluator.clips.length > 1){ animEvaluator.removeClip(0); } } else { this.animEvaluator.removeClips(); } const clip = new AnimClip(this.animations[this.currAnim], 0, 1.0, true, this.loop); clip.name = this.currAnim; clip.blendWeight = this.blending ? 0 : 1; clip.reset(); this.animEvaluator.addClip(clip); } } this.playing = true; } getAnimation(name) { return this.animations[name]; } setModel(model) { if (model !== this.model) { this._resetAnimationController(); this.model = model; if (this.animations && this.currAnim && this.animations[this.currAnim]) { this.play(this.currAnim); } } } onSetAnimations() { const modelComponent = this.entity.model; if (modelComponent) { const m = modelComponent.model; if (m && m !== this.model) { this.setModel(m); } } if (!this.currAnim && this.activate && this.enabled && this.entity.enabled) { const animationNames = Object.keys(this._animations); if (animationNames.length > 0) { this.play(animationNames[0]); } } } _resetAnimationController() { this.skeleton = null; this.fromSkel = null; this.toSkel = null; this.animEvaluator = null; } _createAnimationController() { const model = this.model; const animations = this.animations; let hasJson = false; let hasGlb = false; for(const animation in animations){ if (animations.hasOwnProperty(animation)) { const anim = animations[animation]; if (anim.constructor === AnimTrack) { hasGlb = true; } else { hasJson = true; } } } const graph = model.getGraph(); if (hasJson) { this.fromSkel = new Skeleton(graph); this.toSkel = new Skeleton(graph); this.skeleton = new Skeleton(graph); this.skeleton.looping = this.loop; this.skeleton.setGraph(graph); } else if (hasGlb) { this.animEvaluator = new AnimEvaluator(new DefaultAnimBinder(this.entity)); } } loadAnimationAssets(ids) { if (!ids || !ids.length) { return; } const assets = this.system.app.assets; const onAssetReady = (asset)=>{ if (asset.resources.length > 1) { for(let i = 0; i < asset.resources.length; i++){ this.animations[asset.resources[i].name] = asset.resources[i]; this.animationsIndex[asset.id] = asset.resources[i].name; } } else { this.animations[asset.name] = asset.resource; this.animationsIndex[asset.id] = asset.name; } this.animations = this.animations; }; const onAssetAdd = (asset)=>{ asset.off('change', this.onAssetChanged, this); asset.on('change', this.onAssetChanged, this); asset.off('remove', this.onAssetRemoved, this); asset.on('remove', this.onAssetRemoved, this); if (asset.resource) { onAssetReady(asset); } else { asset.once('load', onAssetReady, this); if (this.enabled && this.entity.enabled) { assets.load(asset); } } }; for(let i = 0, l = ids.length; i < l; i++){ const asset = assets.get(ids[i]); if (asset) { onAssetAdd(asset); } else { assets.on(`add:${ids[i]}`, onAssetAdd); } } } onAssetChanged(asset, attribute, newValue, oldValue) { if (attribute === 'resource' || attribute === 'resources') { if (attribute === 'resources' && newValue && newValue.length === 0) { newValue = null; } if (newValue) { let restarted = false; if (newValue.length > 1) { if (oldValue && oldValue.length > 1) { for(let i = 0; i < oldValue.length; i++){ delete this.animations[oldValue[i].name]; } } else { delete this.animations[asset.name]; } restarted = false; for(let i = 0; i < newValue.length; i++){ this.animations[newValue[i].name] = newValue[i]; if (!restarted && this.currAnim === newValue[i].name) { if (this.playing && this.enabled && this.entity.enabled) { restarted = true; this.play(newValue[i].name); } } } if (!restarted) { this._stopCurrentAnimation(); this.onSetAnimations(); } } else { if (oldValue && oldValue.length > 1) { for(let i = 0; i < oldValue.length; i++){ delete this.animations[oldValue[i].name]; } } this.animations[asset.name] = newValue[0] || newValue; restarted = false; if (this.currAnim === asset.name) { if (this.playing && this.enabled && this.entity.enabled) { restarted = true; this.play(asset.name); } } if (!restarted) { this._stopCurrentAnimation(); this.onSetAnimations(); } } this.animationsIndex[asset.id] = asset.name; } else { if (oldValue.length > 1) { for(let i = 0; i < oldValue.length; i++){ delete this.animations[oldValue[i].name]; if (this.currAnim === oldValue[i].name) { this._stopCurrentAnimation(); } } } else { delete this.animations[asset.name]; if (this.currAnim === asset.name) { this._stopCurrentAnimation(); } } delete this.animationsIndex[asset.id]; } } } onAssetRemoved(asset) { asset.off('remove', this.onAssetRemoved, this); if (this.animations) { if (asset.resources.length > 1) { for(let i = 0; i < asset.resources.length; i++){ delete this.animations[asset.resources[i].name]; if (this.currAnim === asset.resources[i].name) { this._stopCurrentAnimation(); } } } else { delete this.animations[asset.name]; if (this.currAnim === asset.name) { this._stopCurrentAnimation(); } } delete this.animationsIndex[asset.id]; } } _stopCurrentAnimation() { this.currAnim = null; this.playing = false; if (this.skeleton) { this.skeleton.currentTime = 0; this.skeleton.animation = null; } if (this.animEvaluator) { for(let i = 0; i < this.animEvaluator.clips.length; ++i){ this.animEvaluator.clips[i].stop(); } this.animEvaluator.update(0); this.animEvaluator.removeClips(); } } onEnable() { super.onEnable(); const assets = this.assets; const registry = this.system.app.assets; if (assets) { for(let i = 0, len = assets.length; i < len; i++){ let asset = assets[i]; if (!(asset instanceof Asset)) { asset = registry.get(asset); } if (asset && !asset.resource) { registry.load(asset); } } } if (this.activate && !this.currAnim) { const animationNames = Object.keys(this.animations); if (animationNames.length > 0) { this.play(animationNames[0]); } } } onBeforeRemove() { for(let i = 0; i < this.assets.length; i++){ let asset = this.assets[i]; if (typeof asset === 'number') { asset = this.system.app.assets.get(asset); } if (!asset) continue; asset.off('change', this.onAssetChanged, this); asset.off('remove', this.onAssetRemoved, this); } this.skeleton = null; this.fromSkel = null; this.toSkel = null; this.animEvaluator = null; } update(dt) { if (this.blending) { this.blend += dt * this.blendSpeed; if (this.blend >= 1) { this.blend = 1; } } if (this.playing) { const skeleton = this.skeleton; if (skeleton !== null && this.model !== null) { if (this.blending) { skeleton.blend(this.fromSkel, this.toSkel, this.blend); } else { const delta = dt * this.speed; skeleton.addTime(delta); if (this.speed > 0 && skeleton._time === skeleton.animation.duration && !this.loop) { this.playing = false; } else if (this.speed < 0 && skeleton._time === 0 && !this.loop) { this.playing = false; } } if (this.blending && this.blend === 1) { skeleton.animation = this.toSkel.animation; } skeleton.updateGraph(); } } const animEvaluator = this.animEvaluator; if (animEvaluator) { for(let i = 0; i < animEvaluator.clips.length; ++i){ const clip = animEvaluator.clips[i]; clip.speed = this.speed; if (!this.playing) { clip.pause(); } else { clip.resume(); } } if (this.blending && animEvaluator.clips.length > 1) { animEvaluator.clips[1].blendWeight = this.blend; } animEvaluator.update(dt); } if (this.blending && this.blend === 1) { this.blending = false; } } constructor(...args){ super(...args), this._animations = {}, this._assets = [], this._loop = true, this.animEvaluator = null, this.model = null, this.skeleton = null, this.fromSkel = null, this.toSkel = null, this.animationsIndex = {}, this.prevAnim = null, this.currAnim = null, this.blend = 0, this.blending = false, this.blendSpeed = 0, this.activate = true, this.speed = 1; } } class AnimationComponentData { constructor(){ this.enabled = true; } } const _schema$k = [ 'enabled' ]; class AnimationComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { properties = [ 'activate', 'enabled', 'loop', 'speed', 'assets' ]; for (const property of properties){ if (data.hasOwnProperty(property)) { component[property] = data[property]; } } super.initializeComponentData(component, data, _schema$k); } cloneComponent(entity, clone) { this.addComponent(clone, {}); clone.animation.assets = entity.animation.assets.slice(); clone.animation.speed = entity.animation.speed; clone.animation.loop = entity.animation.loop; clone.animation.activate = entity.animation.activate; clone.animation.enabled = entity.animation.enabled; const clonedAnimations = {}; const animations = entity.animation.animations; for(const key in animations){ if (animations.hasOwnProperty(key)) { clonedAnimations[key] = animations[key]; } } clone.animation.animations = clonedAnimations; const clonedAnimationsIndex = {}; const animationsIndex = entity.animation.animationsIndex; for(const key in animationsIndex){ if (animationsIndex.hasOwnProperty(key)) { clonedAnimationsIndex[key] = animationsIndex[key]; } } clone.animation.animationsIndex = clonedAnimationsIndex; return clone.animation; } onBeforeRemove(entity, component) { component.onBeforeRemove(); } onUpdate(dt) { const components = this.store; for(const id in components){ if (components.hasOwnProperty(id)) { const component = components[id]; if (component.data.enabled && component.entity.enabled) { component.entity.animation.update(dt); } } } } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); } constructor(app){ super(app); this.id = 'animation'; this.ComponentType = AnimationComponent; this.DataType = AnimationComponentData; this.schema = _schema$k; this.on('beforeremove', this.onBeforeRemove, this); this.app.systems.on('update', this.onUpdate, this); } } Component._buildAccessors(AnimationComponent.prototype, _schema$k); class AnimNode { get parent() { return this._parent; } get name() { return this._name; } get path() { return this._parent ? `${this._parent.path}.${this._name}` : this._name; } get point() { return this._point; } get pointLength() { return this._pointLength; } set weight(value) { this._weight = value; } get weight() { return this._parent ? this._parent.weight * this._weight : this._weight; } get normalizedWeight() { const totalWeight = this._state.totalWeight; if (totalWeight === 0.0) return 0.0; return this.weight / totalWeight; } get speed() { return this._weightedSpeed * this._speed; } get absoluteSpeed() { return Math.abs(this._speed); } set weightedSpeed(weightedSpeed) { this._weightedSpeed = weightedSpeed; } get weightedSpeed() { return this._weightedSpeed; } set animTrack(value) { this._animTrack = value; } get animTrack() { return this._animTrack; } constructor(state, parent, name, point, speed = 1){ this._state = state; this._parent = parent; this._name = name; if (Array.isArray(point)) { this._point = new Vec2(point[0], point[1]); this._pointLength = this._point.length(); } else { this._point = point; this._pointLength = point; } this._speed = speed; this._weightedSpeed = 1.0; this._weight = 1.0; this._animTrack = null; } } class AnimBlendTree extends AnimNode { get weight() { this.calculateWeights(); return this._parent ? this._parent.weight * this._weight : this._weight; } get syncAnimations() { return this._syncAnimations; } getChild(name) { for(let i = 0; i < this._children.length; i++){ if (this._children[i].name === name) return this._children[i]; } return null; } updateParameterValues() { let paramsEqual = true; for(let i = 0; i < this._parameterValues.length; i++){ const updatedParameter = this._findParameter(this._parameters[i]).value; if (this._parameterValues[i] !== updatedParameter) { this._parameterValues[i] = updatedParameter; paramsEqual = false; } } return paramsEqual; } getNodeWeightedDuration(i) { return this._children[i].animTrack.duration / this._children[i].speedMultiplier * this._children[i].weight; } getNodeCount() { let count = 0; for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; if (child.constructor === AnimBlendTree) { count += this._children[i].getNodeCount(); } else { count++; } } return count; } constructor(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter){ super(state, parent, name, point); this._parameters = parameters; this._parameterValues = new Array(parameters.length); this._children = []; this._findParameter = findParameter; this._syncAnimations = syncAnimations !== false; this._pointCache = {}; for(let i = 0; i < children.length; i++){ const child = children[i]; if (child.children) { this._children.push(createTree(child.type, state, this, child.name, 1.0, child.parameter ? [ child.parameter ] : child.parameters, child.children, child.syncAnimations, createTree, findParameter)); } else { this._children.push(new AnimNode(state, this, child.name, child.point, child.speed)); } } } } class AnimBlendTree1D extends AnimBlendTree { calculateWeights() { if (this.updateParameterValues()) return; let weightedDurationSum = 0.0; this._children[0].weight = 0.0; for(let i = 0; i < this._children.length; i++){ const c1 = this._children[i]; if (i !== this._children.length - 1) { const c2 = this._children[i + 1]; if (c1.point === c2.point) { c1.weight = 0.5; c2.weight = 0.5; } else if (math.between(this._parameterValues[0], c1.point, c2.point, true)) { const child2Distance = Math.abs(c1.point - c2.point); const parameterDistance = Math.abs(c1.point - this._parameterValues[0]); const weight = (child2Distance - parameterDistance) / child2Distance; c1.weight = weight; c2.weight = 1.0 - weight; } else { c2.weight = 0.0; } } if (this._syncAnimations) { weightedDurationSum += c1.animTrack.duration / c1.absoluteSpeed * c1.weight; } } if (this._syncAnimations) { for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; child.weightedSpeed = child.animTrack.duration / child.absoluteSpeed / weightedDurationSum; } } } constructor(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter){ children.sort((a, b)=>a.point - b.point); super(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter); } } class AnimBlendTreeCartesian2D extends AnimBlendTree { pointDistanceCache(i, j) { const pointKey = `${i}${j}`; if (!this._pointCache[pointKey]) { this._pointCache[pointKey] = this._children[j].point.clone().sub(this._children[i].point); } return this._pointCache[pointKey]; } calculateWeights() { if (this.updateParameterValues()) return; let weightSum, weightedDurationSum; AnimBlendTreeCartesian2D._p.set(...this._parameterValues); weightSum = 0.0; weightedDurationSum = 0.0; for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; const pi = child.point; AnimBlendTreeCartesian2D._pip.set(AnimBlendTreeCartesian2D._p.x, AnimBlendTreeCartesian2D._p.y).sub(pi); let minj = Number.MAX_VALUE; for(let j = 0; j < this._children.length; j++){ if (i === j) continue; const pipj = this.pointDistanceCache(i, j); const result = math.clamp(1.0 - AnimBlendTreeCartesian2D._pip.dot(pipj) / pipj.lengthSq(), 0.0, 1.0); if (result < minj) minj = result; } child.weight = minj; weightSum += minj; if (this._syncAnimations) { weightedDurationSum += child.animTrack.duration / child.absoluteSpeed * child.weight; } } for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; child.weight = child._weight / weightSum; if (this._syncAnimations) { child.weightedSpeed = child.animTrack.duration / child.absoluteSpeed / weightedDurationSum; } } } } AnimBlendTreeCartesian2D._p = new Vec2(); AnimBlendTreeCartesian2D._pip = new Vec2(); class AnimBlendTreeDirectional2D extends AnimBlendTree { pointCache(i, j) { const pointKey = `${i}${j}`; if (!this._pointCache[pointKey]) { this._pointCache[pointKey] = new Vec2((this._children[j].pointLength - this._children[i].pointLength) / ((this._children[j].pointLength + this._children[i].pointLength) / 2), Vec2.angleRad(this._children[i].point, this._children[j].point) * 2.0); } return this._pointCache[pointKey]; } calculateWeights() { if (this.updateParameterValues()) return; let weightSum, weightedDurationSum; AnimBlendTreeDirectional2D._p.set(...this._parameterValues); const pLength = AnimBlendTreeDirectional2D._p.length(); weightSum = 0.0; weightedDurationSum = 0.0; for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; const pi = child.point; const piLength = child.pointLength; let minj = Number.MAX_VALUE; for(let j = 0; j < this._children.length; j++){ if (i === j) continue; const pipj = this.pointCache(i, j); const pjLength = this._children[j].pointLength; AnimBlendTreeDirectional2D._pip.set((pLength - piLength) / ((pjLength + piLength) / 2), Vec2.angleRad(pi, AnimBlendTreeDirectional2D._p) * 2.0); const result = math.clamp(1.0 - Math.abs(AnimBlendTreeDirectional2D._pip.dot(pipj) / pipj.lengthSq()), 0.0, 1.0); if (result < minj) minj = result; } child.weight = minj; weightSum += minj; if (this._syncAnimations) { weightedDurationSum += child.animTrack.duration / child.absoluteSpeed * child.weight; } } for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; child.weight = child._weight / weightSum; if (this._syncAnimations) { const weightedChildDuration = child.animTrack.duration / weightedDurationSum * weightSum; child.weightedSpeed = child.absoluteSpeed * weightedChildDuration; } } } } AnimBlendTreeDirectional2D._p = new Vec2(); AnimBlendTreeDirectional2D._pip = new Vec2(); class AnimBlendTreeDirect extends AnimBlendTree { calculateWeights() { if (this.updateParameterValues()) return; let weightSum = 0.0; let weightedDurationSum = 0.0; for(let i = 0; i < this._children.length; i++){ weightSum += Math.max(this._parameterValues[i], 0.0); if (this._syncAnimations) { const child = this._children[i]; weightedDurationSum += child.animTrack.duration / child.absoluteSpeed * child.weight; } } for(let i = 0; i < this._children.length; i++){ const child = this._children[i]; const weight = Math.max(this._parameterValues[i], 0.0); if (weightSum) { child.weight = weight / weightSum; if (this._syncAnimations) { child.weightedSpeed = child.animTrack.duration / child.absoluteSpeed / weightedDurationSum; } } else { child.weight = 0.0; if (this._syncAnimations) { child.weightedSpeed = 0; } } } } } let AnimState$1 = class AnimState { _createTree(type, state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter) { switch(type){ case ANIM_BLEND_1D: return new AnimBlendTree1D(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter); case ANIM_BLEND_2D_CARTESIAN: return new AnimBlendTreeCartesian2D(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter); case ANIM_BLEND_2D_DIRECTIONAL: return new AnimBlendTreeDirectional2D(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter); case ANIM_BLEND_DIRECT: return new AnimBlendTreeDirect(state, parent, name, point, parameters, children, syncAnimations, createTree, findParameter); } return undefined; } _getNodeFromPath(path) { let currNode = this._blendTree; for(let i = 1; i < path.length; i++){ currNode = currNode.getChild(path[i]); } return currNode; } addAnimation(path, animTrack) { const pathString = path.join('.'); const indexOfAnimation = this._animationList.findIndex((animation)=>{ return animation.path === pathString; }); if (indexOfAnimation >= 0) { this._animationList[indexOfAnimation].animTrack = animTrack; } else { const node = this._getNodeFromPath(path); node.animTrack = animTrack; this._animationList.push(node); } this._updateHasAnimations(); } _updateHasAnimations() { this._hasAnimations = this._animationList.length > 0 && this._animationList.every((animation)=>animation.animTrack && animation.animTrack !== AnimTrack.EMPTY); } get name() { return this._name; } set animations(value) { this._animationList = value; this._updateHasAnimations(); } get animations() { return this._animationList; } get hasAnimations() { return this._hasAnimations; } set speed(value) { this._speed = value; } get speed() { return this._speed; } set loop(value) { this._loop = value; } get loop() { return this._loop; } get nodeCount() { if (!this._blendTree || this._blendTree.constructor === AnimNode) return 1; return this._blendTree.getNodeCount(); } get playable() { return ANIM_CONTROL_STATES.indexOf(this.name) !== -1 || this.animations.length === this.nodeCount; } get looping() { if (this.animations.length > 0) { const trackClipName = `${this.name}.${this.animations[0].animTrack.name}`; const trackClip = this._controller.animEvaluator.findClip(trackClipName); if (trackClip) { return trackClip.loop; } } return false; } get totalWeight() { let sum = 0; for(let i = 0; i < this.animations.length; i++){ sum += this.animations[i].weight; } return sum; } get timelineDuration() { let duration = 0; for(let i = 0; i < this.animations.length; i++){ const animation = this.animations[i]; if (animation.animTrack.duration > duration) { duration = animation.animTrack.duration; } } return duration; } constructor(controller, name, speed = 1, loop = true, blendTree){ this._animations = {}; this._animationList = []; this._controller = controller; this._name = name; this._speed = speed; this._loop = loop; this._hasAnimations = false; if (blendTree) { this._blendTree = this._createTree(blendTree.type, this, null, name, 1.0, blendTree.parameter ? [ blendTree.parameter ] : blendTree.parameters, blendTree.children, blendTree.syncAnimations, this._createTree, this._controller.findParameter); } else { this._blendTree = new AnimNode(this, null, name, 1.0, speed); } } }; class AnimTransition { get from() { return this._from; } set to(value) { this._to = value; } get to() { return this._to; } get time() { return this._time; } get priority() { return this._priority; } get conditions() { return this._conditions; } get exitTime() { return this._exitTime; } get transitionOffset() { return this._transitionOffset; } get interruptionSource() { return this._interruptionSource; } get hasExitTime() { return !!this.exitTime; } constructor({ from, to, time = 0, priority = 0, conditions = [], exitTime = null, transitionOffset = null, interruptionSource = ANIM_INTERRUPTION_NONE }){ this._from = from; this._to = to; this._time = time; this._priority = priority; this._conditions = conditions; this._exitTime = exitTime; this._transitionOffset = transitionOffset; this._interruptionSource = interruptionSource; } } let AnimController$1 = class AnimController { get animEvaluator() { return this._animEvaluator; } set activeState(stateName) { this._activeStateName = stateName; } get activeState() { return this._findState(this._activeStateName); } get activeStateName() { return this._activeStateName; } get activeStateAnimations() { return this.activeState.animations; } set previousState(stateName) { this._previousStateName = stateName; } get previousState() { return this._findState(this._previousStateName); } get previousStateName() { return this._previousStateName; } get playable() { let playable = true; for(let i = 0; i < this._stateNames.length; i++){ if (!this._states[this._stateNames[i]].playable) { playable = false; } } return playable; } set playing(value) { this._playing = value; } get playing() { return this._playing; } get activeStateProgress() { return this._getActiveStateProgressForTime(this._timeInState); } get activeStateDuration() { if (this._activeStateDurationDirty) { let maxDuration = 0.0; for(let i = 0; i < this.activeStateAnimations.length; i++){ const activeClip = this._animEvaluator.findClip(this.activeStateAnimations[i].name); if (activeClip) { maxDuration = Math.max(maxDuration, activeClip.track.duration); } } this._activeStateDuration = maxDuration; this._activeStateDurationDirty = false; } return this._activeStateDuration; } set activeStateCurrentTime(time) { this._timeInStateBefore = time; this._timeInState = time; for(let i = 0; i < this.activeStateAnimations.length; i++){ const clip = this.animEvaluator.findClip(this.activeStateAnimations[i].name); if (clip) { clip.time = time; } } } get activeStateCurrentTime() { return this._timeInState; } get transitioning() { return this._isTransitioning; } get transitionProgress() { return this._currTransitionTime / this._totalTransitionTime; } get states() { return this._stateNames; } assignMask(mask) { return this._animEvaluator.assignMask(mask); } _findState(stateName) { return this._states[stateName]; } _getActiveStateProgressForTime(time) { if (this.activeStateName === ANIM_STATE_START || this.activeStateName === ANIM_STATE_END || this.activeStateName === ANIM_STATE_ANY) { return 1.0; } const activeClip = this._animEvaluator.findClip(this.activeStateAnimations[0].name); if (activeClip) { return activeClip.progressForTime(time); } return null; } _findTransitionsFromState(stateName) { let transitions = this._findTransitionsFromStateCache[stateName]; if (!transitions) { transitions = this._transitions.filter((transition)=>{ return transition.from === stateName; }); sortPriority(transitions); this._findTransitionsFromStateCache[stateName] = transitions; } return transitions; } _findTransitionsBetweenStates(sourceStateName, destinationStateName) { let transitions = this._findTransitionsBetweenStatesCache[`${sourceStateName}->${destinationStateName}`]; if (!transitions) { transitions = this._transitions.filter((transition)=>{ return transition.from === sourceStateName && transition.to === destinationStateName; }); sortPriority(transitions); this._findTransitionsBetweenStatesCache[`${sourceStateName}->${destinationStateName}`] = transitions; } return transitions; } _transitionHasConditionsMet(transition) { const conditions = transition.conditions; for(let i = 0; i < conditions.length; i++){ const condition = conditions[i]; const parameter = this._findParameter(condition.parameterName); switch(condition.predicate){ case ANIM_GREATER_THAN: if (!(parameter.value > condition.value)) return false; break; case ANIM_LESS_THAN: if (!(parameter.value < condition.value)) return false; break; case ANIM_GREATER_THAN_EQUAL_TO: if (!(parameter.value >= condition.value)) return false; break; case ANIM_LESS_THAN_EQUAL_TO: if (!(parameter.value <= condition.value)) return false; break; case ANIM_EQUAL_TO: if (!(parameter.value === condition.value)) return false; break; case ANIM_NOT_EQUAL_TO: if (!(parameter.value !== condition.value)) return false; break; } } return true; } _findTransition(from, to) { let transitions = []; if (from && to) { transitions = transitions.concat(this._findTransitionsBetweenStates(from, to)); } else { if (!this._isTransitioning) { transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName)); transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY)); } else { switch(this._transitionInterruptionSource){ case ANIM_INTERRUPTION_PREV: transitions = transitions.concat(this._findTransitionsFromState(this._previousStateName)); transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY)); break; case ANIM_INTERRUPTION_NEXT: transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName)); transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY)); break; case ANIM_INTERRUPTION_PREV_NEXT: transitions = transitions.concat(this._findTransitionsFromState(this._previousStateName)); transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName)); transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY)); break; case ANIM_INTERRUPTION_NEXT_PREV: transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName)); transitions = transitions.concat(this._findTransitionsFromState(this._previousStateName)); transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY)); break; } } } transitions = transitions.filter((transition)=>{ if (transition.to === this.activeStateName) { return false; } if (transition.hasExitTime) { let progressBefore = this._getActiveStateProgressForTime(this._timeInStateBefore); let progress = this._getActiveStateProgressForTime(this._timeInState); if (transition.exitTime < 1.0 && this.activeState.loop) { progressBefore -= Math.floor(progressBefore); progress -= Math.floor(progress); } if (progress === progressBefore) { if (progress !== transition.exitTime) { return null; } } else if (!(transition.exitTime > progressBefore && transition.exitTime <= progress)) { return null; } } return this._transitionHasConditionsMet(transition); }); if (transitions.length > 0) { const transition = transitions[0]; if (transition.to === ANIM_STATE_END) { const startTransition = this._findTransitionsFromState(ANIM_STATE_START)[0]; transition.to = startTransition.to; } return transition; } return null; } updateStateFromTransition(transition) { let state; let animation; let clip; this.previousState = transition.from ? this.activeStateName : null; this.activeState = transition.to; this._activeStateDurationDirty = true; for(let i = 0; i < transition.conditions.length; i++){ const condition = transition.conditions[i]; const parameter = this._findParameter(condition.parameterName); if (parameter.type === ANIM_PARAMETER_TRIGGER) { this._consumeTrigger(condition.parameterName); } } if (this.previousState) { if (!this._isTransitioning) { this._transitionPreviousStates = []; } this._transitionPreviousStates.push({ name: this._previousStateName, weight: 1 }); const interpolatedTime = Math.min(this._totalTransitionTime !== 0 ? this._currTransitionTime / this._totalTransitionTime : 1, 1.0); for(let i = 0; i < this._transitionPreviousStates.length; i++){ if (!this._isTransitioning) { this._transitionPreviousStates[i].weight = 1.0; } else if (i !== this._transitionPreviousStates.length - 1) { this._transitionPreviousStates[i].weight *= 1.0 - interpolatedTime; } else { this._transitionPreviousStates[i].weight = interpolatedTime; } state = this._findState(this._transitionPreviousStates[i].name); for(let j = 0; j < state.animations.length; j++){ animation = state.animations[j]; clip = this._animEvaluator.findClip(`${animation.name}.previous.${i}`); if (!clip) { clip = this._animEvaluator.findClip(animation.name); clip.name = `${animation.name}.previous.${i}`; } if (i !== this._transitionPreviousStates.length - 1) { clip.pause(); } } } } this._isTransitioning = true; this._totalTransitionTime = transition.time; this._currTransitionTime = 0; this._transitionInterruptionSource = transition.interruptionSource; const activeState = this.activeState; const hasTransitionOffset = transition.transitionOffset && transition.transitionOffset > 0.0 && transition.transitionOffset < 1.0; let timeInState = 0; let timeInStateBefore = 0; if (hasTransitionOffset) { const offsetTime = activeState.timelineDuration * transition.transitionOffset; timeInState = offsetTime; timeInStateBefore = offsetTime; } this._timeInState = timeInState; this._timeInStateBefore = timeInStateBefore; for(let i = 0; i < activeState.animations.length; i++){ clip = this._animEvaluator.findClip(activeState.animations[i].name); if (!clip) { const speed = Number.isFinite(activeState.animations[i].speed) ? activeState.animations[i].speed : activeState.speed; clip = new AnimClip(activeState.animations[i].animTrack, this._timeInState, speed, true, activeState.loop, this._eventHandler); clip.name = activeState.animations[i].name; this._animEvaluator.addClip(clip); } else { clip.reset(); } if (transition.time > 0) { clip.blendWeight = 0.0; } else { clip.blendWeight = activeState.animations[i].normalizedWeight; } clip.play(); if (hasTransitionOffset) { clip.time = activeState.timelineDuration * transition.transitionOffset; } else { const startTime = activeState.speed >= 0 ? 0 : this.activeStateDuration; clip.time = startTime; } } } _transitionToState(newStateName) { if (!this._findState(newStateName)) { return; } let transition = this._findTransition(this._activeStateName, newStateName); if (!transition) { this._animEvaluator.removeClips(); transition = new AnimTransition({ from: null, to: newStateName }); } this.updateStateFromTransition(transition); } assignAnimation(pathString, animTrack, speed, loop) { const path = pathString.split('.'); let state = this._findState(path[0]); if (!state) { state = new AnimState$1(this, path[0], speed); this._states[path[0]] = state; this._stateNames.push(path[0]); } state.addAnimation(path, animTrack); this._animEvaluator.updateClipTrack(state.name, animTrack); if (speed !== undefined) { state.speed = speed; } if (loop !== undefined) { state.loop = loop; } if (!this._playing && this._activate && this.playable) { this.play(); } this._activeStateDurationDirty = true; } removeNodeAnimations(nodeName) { if (ANIM_CONTROL_STATES.indexOf(nodeName) !== -1) { return false; } const state = this._findState(nodeName); if (!state) { return false; } state.animations = []; return true; } play(stateName) { if (stateName) { this._transitionToState(stateName); } this._playing = true; } pause() { this._playing = false; } reset() { this._previousStateName = null; this._activeStateName = ANIM_STATE_START; this._playing = false; this._currTransitionTime = 1.0; this._totalTransitionTime = 1.0; this._isTransitioning = false; this._timeInState = 0; this._timeInStateBefore = 0; this._animEvaluator.removeClips(); } rebind() { this._animEvaluator.rebind(); } update(dt) { if (!this._playing) { return; } let state; let animation; let clip; if (this.activeState.loop || this._timeInState < this.activeStateDuration) { this._timeInStateBefore = this._timeInState; this._timeInState += dt * this.activeState.speed; if (!this.activeState.loop && this._timeInState > this.activeStateDuration) { this._timeInState = this.activeStateDuration; dt = this.activeStateDuration - this._timeInStateBefore; } } const transition = this._findTransition(this._activeStateName); if (transition) { this.updateStateFromTransition(transition); } if (this._isTransitioning) { this._currTransitionTime += dt; if (this._currTransitionTime <= this._totalTransitionTime) { const interpolatedTime = this._totalTransitionTime !== 0 ? this._currTransitionTime / this._totalTransitionTime : 1; for(let i = 0; i < this._transitionPreviousStates.length; i++){ state = this._findState(this._transitionPreviousStates[i].name); const stateWeight = this._transitionPreviousStates[i].weight; for(let j = 0; j < state.animations.length; j++){ animation = state.animations[j]; clip = this._animEvaluator.findClip(`${animation.name}.previous.${i}`); if (clip) { clip.blendWeight = (1.0 - interpolatedTime) * animation.normalizedWeight * stateWeight; } } } state = this.activeState; for(let i = 0; i < state.animations.length; i++){ animation = state.animations[i]; this._animEvaluator.findClip(animation.name).blendWeight = interpolatedTime * animation.normalizedWeight; } } else { this._isTransitioning = false; const activeClips = this.activeStateAnimations.length; const totalClips = this._animEvaluator.clips.length; for(let i = 0; i < totalClips - activeClips; i++){ this._animEvaluator.removeClip(0); } this._transitionPreviousStates = []; state = this.activeState; for(let i = 0; i < state.animations.length; i++){ animation = state.animations[i]; clip = this._animEvaluator.findClip(animation.name); if (clip) { clip.blendWeight = animation.normalizedWeight; } } } } else { if (this.activeState._blendTree.constructor !== AnimNode) { state = this.activeState; for(let i = 0; i < state.animations.length; i++){ animation = state.animations[i]; clip = this._animEvaluator.findClip(animation.name); if (clip) { clip.blendWeight = animation.normalizedWeight; if (animation.parent.syncAnimations) { clip.speed = animation.speed; } } } } } this._animEvaluator.update(dt, this.activeState.hasAnimations); } constructor(animEvaluator, states, transitions, activate, eventHandler, findParameter, consumeTrigger){ this._states = {}; this._stateNames = []; this._findTransitionsFromStateCache = {}; this._findTransitionsBetweenStatesCache = {}; this._previousStateName = null; this._activeStateName = ANIM_STATE_START; this._activeStateDuration = 0; this._activeStateDurationDirty = true; this._playing = false; this._currTransitionTime = 1; this._totalTransitionTime = 1; this._isTransitioning = false; this._transitionInterruptionSource = ANIM_INTERRUPTION_NONE; this._transitionPreviousStates = []; this._timeInState = 0; this._timeInStateBefore = 0; this.findParameter = (name)=>{ return this._findParameter(name); }; this._animEvaluator = animEvaluator; this._eventHandler = eventHandler; this._findParameter = findParameter; this._consumeTrigger = consumeTrigger; for(let i = 0; i < states.length; i++){ this._states[states[i].name] = new AnimState$1(this, states[i].name, states[i].speed, states[i].loop, states[i].blendTree); this._stateNames.push(states[i].name); } this._transitions = transitions.map((transition)=>{ return new AnimTransition({ ...transition }); }); this._activate = activate; } }; const v2 = new Vec2(); const v3 = new Vec3(); const v4 = new Vec4(); const c = new Color(); const q = new Quat(); class AnimComponentBinder extends DefaultAnimBinder { static _packFloat(values) { return values[0]; } static _packBoolean(values) { return !!values[0]; } static _packVec2(values) { v2.x = values[0]; v2.y = values[1]; return v2; } static _packVec3(values) { v3.x = values[0]; v3.y = values[1]; v3.z = values[2]; return v3; } static _packVec4(values) { v4.x = values[0]; v4.y = values[1]; v4.z = values[2]; v4.w = values[3]; return v4; } static _packColor(values) { c.r = values[0]; c.g = values[1]; c.b = values[2]; c.a = values[3]; return c; } static _packQuat(values) { q.x = values[0]; q.y = values[1]; q.z = values[2]; q.w = values[3]; return q; } resolve(path) { const encodedPath = AnimBinder.encode(path.entityPath, path.component, path.propertyPath); let target = this.targetCache[encodedPath]; if (target) return target; let entity; let propertyComponent; let targetPath; switch(path.component){ case 'entity': entity = this._getEntityFromHierarchy(path.entityPath); targetPath = AnimBinder.encode(entity.path, 'entity', path.propertyPath); propertyComponent = entity; break; case 'graph': propertyComponent = this.findNode(path); if (!propertyComponent) return null; targetPath = AnimBinder.encode(propertyComponent.path, 'graph', path.propertyPath); break; default: entity = this._getEntityFromHierarchy(path.entityPath); propertyComponent = entity.findComponent(path.component); if (!propertyComponent) { return null; } targetPath = AnimBinder.encode(entity.path, path.component, path.propertyPath); break; } target = this._createAnimTargetForProperty(propertyComponent, path.propertyPath, targetPath); this.targetCache[encodedPath] = target; return target; } update(deltaTime) { const activeNodes = this.activeNodes; if (activeNodes) { for(let i = 0; i < activeNodes.length; i++){ activeNodes[i]._dirtifyLocal(); } } } _getEntityFromHierarchy(entityHierarchy) { if (!this.animComponent.entity.name === entityHierarchy[0]) { return null; } const currEntity = this.animComponent.entity; if (entityHierarchy.length === 1) { return currEntity; } return currEntity._parent.findByPath(entityHierarchy); } _resolvePath(object, path, resolveLeaf) { const steps = path.length - (resolveLeaf ? 0 : 1); for(let i = 0; i < steps; i++){ object = object[path[i]]; } return object; } _setter(object, path, packFunc) { const obj = this._resolvePath(object, path); const key = path[path.length - 1]; const setterFuncName = `set${key.substring(0, 1).toUpperCase()}${key.substring(1)}`; if (obj[setterFuncName]) { const getterFunc = obj[`get${key.substring(0, 1).toUpperCase()}${key.substring(1)}`].bind(obj); let baseValues = getterFunc(); baseValues = [ baseValues.x, baseValues.y, baseValues.z, baseValues.w ]; const setterFunc = obj[setterFuncName].bind(obj); return { set: (values)=>{ setterFunc(packFunc(values)); }, get: ()=>baseValues }; } const prop = obj[key]; if (typeof prop === 'object' && prop.hasOwnProperty('copy')) { return function(values) { prop.copy(packFunc(values)); }; } if ([ Vec2, Vec3, Vec4, Color, Quat ].indexOf(obj.constructor) !== -1 && path.length > 1) { const parent = path.length > 2 ? this._resolvePath(object, path.slice(0, -1)) : object; const objKey = path[path.length - 2]; return function(values) { obj[key] = packFunc(values); parent[objKey] = obj; }; } return function(values) { obj[key] = packFunc(values); }; } _createAnimTargetForProperty(propertyComponent, propertyHierarchy, targetPath) { if (this.handlers && propertyHierarchy[0].startsWith('weight.')) { return this.handlers.weight(propertyComponent, propertyHierarchy[0].replace('weight.', '')); } else if (this.handlers && propertyHierarchy[0] === 'material' && propertyHierarchy.length === 2) { const materialPropertyName = propertyHierarchy[1]; if (materialPropertyName.endsWith('Map')) { return this.handlers.materialTexture(propertyComponent, materialPropertyName); } } const property = this._resolvePath(propertyComponent, propertyHierarchy, true); if (typeof property === 'undefined') { return null; } let setter; let animDataType; let animDataComponents; if (typeof property === 'number') { setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packFloat); animDataType = 'vector'; animDataComponents = 1; } else if (typeof property === 'boolean') { setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packBoolean); animDataType = 'vector'; animDataComponents = 1; } else if (typeof property === 'object') { switch(property.constructor){ case Vec2: setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packVec2); animDataType = 'vector'; animDataComponents = 2; break; case Vec3: setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packVec3); animDataType = 'vector'; animDataComponents = 3; break; case Vec4: setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packVec4); animDataType = 'vector'; animDataComponents = 4; break; case Color: setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packColor); animDataType = 'vector'; animDataComponents = 4; break; case Quat: setter = this._setter(propertyComponent, propertyHierarchy, AnimComponentBinder._packQuat); animDataType = 'quaternion'; animDataComponents = 4; break; default: return null; } } if (propertyHierarchy.indexOf('material') !== -1) { return new AnimTarget((values)=>{ setter(values); propertyComponent.material.update(); }, animDataType, animDataComponents, targetPath); } return new AnimTarget(setter, animDataType, animDataComponents, targetPath); } rebind() { this.targetCache = {}; if (this.animComponent.rootBone) { this.graph = this.animComponent.rootBone; } else { this.graph = this.animComponent.entity; } const nodes = {}; const flatten = function(node) { nodes[node.name] = node; for(let i = 0; i < node.children.length; ++i){ flatten(node.children[i]); } }; flatten(this.graph); this.nodes = nodes; } constructor(animComponent, graph, layerName, mask, layerIndex){ super(graph); this.animComponent = animComponent; this._mask = mask; this.layerName = layerName; this.layerIndex = layerIndex; } } class AnimComponentLayer { get name() { return this._name; } set playing(value) { this._controller.playing = value; } get playing() { return this._controller.playing; } get playable() { return this._controller.playable; } get activeState() { return this._controller.activeStateName; } get previousState() { return this._controller.previousStateName; } get activeStateProgress() { return this._controller.activeStateProgress; } get activeStateDuration() { return this._controller.activeStateDuration; } set activeStateCurrentTime(time) { const controller = this._controller; const layerPlaying = controller.playing; controller.playing = true; controller.activeStateCurrentTime = time; if (!layerPlaying) { controller.update(0); } controller.playing = layerPlaying; } get activeStateCurrentTime() { return this._controller.activeStateCurrentTime; } get transitioning() { return this._controller.transitioning; } get transitionProgress() { if (this.transitioning) { return this._controller.transitionProgress; } return null; } get states() { return this._controller.states; } set weight(value) { this._weight = value; this._component.dirtifyTargets(); } get weight() { return this._weight; } set blendType(value) { if (value !== this._blendType) { this._blendType = value; if (this._controller.normalizeWeights) { this._component.rebind(); } } } get blendType() { return this._blendType; } set mask(value) { if (this._controller.assignMask(value)) { this._component.rebind(); } this._mask = value; } get mask() { return this._mask; } play(name) { this._controller.play(name); } pause() { this._controller.pause(); } reset() { this._controller.reset(); } rebind() { this._controller.rebind(); } update(dt) { if (this._blendTime) { if (this._blendTimeElapsed < this._blendTime) { this.weight = math.lerp(this._startingWeight, this._targetWeight, this._blendTimeElapsed / this._blendTime); this._blendTimeElapsed += dt; } else { this.weight = this._targetWeight; this._blendTime = 0; this._blendTimeElapsed = 0; this._startingWeight = 0; this._targetWeight = 0; } } this._controller.update(dt); } blendToWeight(weight, time) { this._startingWeight = this.weight; this._targetWeight = weight; this._blendTime = Math.max(0, time); this._blendTimeElapsed = 0; } assignAnimation(nodePath, animTrack, speed, loop) { if (!(animTrack instanceof AnimTrack)) { return; } this._controller.assignAnimation(nodePath, animTrack, speed, loop); if (this._controller._transitions.length === 0) { this._controller._transitions.push(new AnimTransition({ from: 'START', to: nodePath })); } if (this._component.activate && this._component.playable) { this._component.playing = true; } } removeNodeAnimations(nodeName) { if (this._controller.removeNodeAnimations(nodeName)) { this._component.playing = false; } } getAnimationAsset(stateName) { return this._component.animationAssets[`${this.name}:${stateName}`]; } transition(to, time = 0, transitionOffset = null) { this._controller.updateStateFromTransition(new AnimTransition({ from: this._controller.activeStateName, to, time, transitionOffset })); } constructor(name, controller, component, weight = 1, blendType = ANIM_LAYER_OVERWRITE){ this._mask = null; this._blendTime = 0; this._blendTimeElapsed = 0; this._startingWeight = 0; this._targetWeight = 0; this._name = name; this._controller = controller; this._component = component; this._weight = weight; this._blendType = blendType; } } class AnimStateGraph { get parameters() { return Object.assign({}, this._parameters); } get layers() { return this._layers; } constructor(data){ this._layers = []; this._parameters = {}; if (!Array.isArray(data.layers)) { for(const layerId in data.layers){ const dataLayer = data.layers[layerId]; const layer = { name: dataLayer.name, blendType: dataLayer.blendType, weight: dataLayer.weight, states: [], transitions: [] }; for(let i = 0; i < dataLayer.states.length; i++){ layer.states.push(data.states[dataLayer.states[i]]); } for(let i = 0; i < dataLayer.transitions.length; i++){ const dataLayerTransition = data.transitions[dataLayer.transitions[i]]; if (dataLayerTransition.conditions && !Array.isArray(dataLayerTransition.conditions)) { const conditionKeys = Object.keys(dataLayerTransition.conditions); const conditions = []; for(let j = 0; j < conditionKeys.length; j++){ const condition = dataLayerTransition.conditions[conditionKeys[j]]; if (condition.parameterName) { conditions.push(condition); } } dataLayerTransition.conditions = conditions; } if (Number.isInteger(dataLayerTransition.from)) { dataLayerTransition.from = data.states[dataLayerTransition.from].name; } if (Number.isInteger(dataLayerTransition.to)) { dataLayerTransition.to = data.states[dataLayerTransition.to].name; } layer.transitions.push(dataLayerTransition); } this._layers.push(layer); } } else { this._layers = data.layers; } for(const paramId in data.parameters){ const param = data.parameters[paramId]; this._parameters[param.name] = { type: param.type, value: param.value }; } } } class AnimComponent extends Component { set stateGraphAsset(value) { if (value === null) { this.removeStateGraph(); return; } if (this._stateGraphAsset) { const stateGraphAsset = this.system.app.assets.get(this._stateGraphAsset); stateGraphAsset.off('change', this._onStateGraphAssetChangeEvent, this); } let _id; let _asset; if (value instanceof Asset) { _id = value.id; _asset = this.system.app.assets.get(_id); if (!_asset) { this.system.app.assets.add(value); _asset = this.system.app.assets.get(_id); } } else { _id = value; _asset = this.system.app.assets.get(_id); } if (!_asset || this._stateGraphAsset === _id) { return; } if (_asset.resource) { this._stateGraph = _asset.resource; this.loadStateGraph(this._stateGraph); _asset.on('change', this._onStateGraphAssetChangeEvent, this); } else { _asset.once('load', (asset)=>{ this._stateGraph = asset.resource; this.loadStateGraph(this._stateGraph); }); _asset.on('change', this._onStateGraphAssetChangeEvent, this); this.system.app.assets.load(_asset); } this._stateGraphAsset = _id; } get stateGraphAsset() { return this._stateGraphAsset; } set normalizeWeights(value) { this._normalizeWeights = value; this.unbind(); } get normalizeWeights() { return this._normalizeWeights; } set animationAssets(value) { this._animationAssets = value; this.loadAnimationAssets(); } get animationAssets() { return this._animationAssets; } set speed(value) { this._speed = value; } get speed() { return this._speed; } set activate(value) { this._activate = value; } get activate() { return this._activate; } set playing(value) { this._playing = value; } get playing() { return this._playing; } set rootBone(value) { if (typeof value === 'string') { const entity = this.entity.root.findByGuid(value); this._rootBone = entity; } else if (value instanceof Entity) { this._rootBone = value; } else { this._rootBone = null; } this.rebind(); } get rootBone() { return this._rootBone; } set stateGraph(value) { this._stateGraph = value; } get stateGraph() { return this._stateGraph; } get layers() { return this._layers; } set layerIndices(value) { this._layerIndices = value; } get layerIndices() { return this._layerIndices; } set parameters(value) { this._parameters = value; } get parameters() { return this._parameters; } set targets(value) { this._targets = value; } get targets() { return this._targets; } get playable() { for(let i = 0; i < this._layers.length; i++){ if (!this._layers[i].playable) { return false; } } return true; } get baseLayer() { if (this._layers.length > 0) { return this._layers[0]; } return null; } _onStateGraphAssetChangeEvent(asset) { const prevAnimationAssets = this.animationAssets; const prevMasks = this.layers.map((layer)=>layer.mask); this.removeStateGraph(); this._stateGraph = new AnimStateGraph(asset._data); this.loadStateGraph(this._stateGraph); this.animationAssets = prevAnimationAssets; this.loadAnimationAssets(); this.layers.forEach((layer, i)=>{ layer.mask = prevMasks[i]; }); this.rebind(); } dirtifyTargets() { const targets = Object.values(this._targets); for(let i = 0; i < targets.length; i++){ targets[i].dirty = true; } } _addLayer({ name, states, transitions, weight, mask, blendType }) { let graph; if (this.rootBone) { graph = this.rootBone; } else { graph = this.entity; } const layerIndex = this._layers.length; const animBinder = new AnimComponentBinder(this, graph, name, mask, layerIndex); const animEvaluator = new AnimEvaluator(animBinder); const controller = new AnimController$1(animEvaluator, states, transitions, this._activate, this, this.findParameter, this.consumeTrigger); this._layers.push(new AnimComponentLayer(name, controller, this, weight, blendType)); this._layerIndices[name] = layerIndex; return this._layers[layerIndex]; } addLayer(name, weight, mask, blendType) { const layer = this.findAnimationLayer(name); if (layer) return layer; const states = [ { 'name': 'START', 'speed': 1 } ]; const transitions = []; return this._addLayer({ name, states, transitions, weight, mask, blendType }); } _assignParameters(stateGraph) { this._parameters = {}; const paramKeys = Object.keys(stateGraph.parameters); for(let i = 0; i < paramKeys.length; i++){ const paramKey = paramKeys[i]; this._parameters[paramKey] = { type: stateGraph.parameters[paramKey].type, value: stateGraph.parameters[paramKey].value }; } } loadStateGraph(stateGraph) { this._stateGraph = stateGraph; this._assignParameters(stateGraph); this._layers = []; let containsBlendTree = false; for(let i = 0; i < stateGraph.layers.length; i++){ const layer = stateGraph.layers[i]; this._addLayer({ ...layer }); if (layer.states.some((state)=>state.blendTree)) { containsBlendTree = true; } } if (!containsBlendTree) { this.setupAnimationAssets(); } } setupAnimationAssets() { for(let i = 0; i < this._layers.length; i++){ const layer = this._layers[i]; const layerName = layer.name; for(let j = 0; j < layer.states.length; j++){ const stateName = layer.states[j]; if (ANIM_CONTROL_STATES.indexOf(stateName) === -1) { const stateKey = `${layerName}:${stateName}`; if (!this._animationAssets[stateKey]) { this._animationAssets[stateKey] = { asset: null }; } } } } this.loadAnimationAssets(); } loadAnimationAssets() { for(let i = 0; i < this._layers.length; i++){ const layer = this._layers[i]; for(let j = 0; j < layer.states.length; j++){ const stateName = layer.states[j]; if (ANIM_CONTROL_STATES.indexOf(stateName) !== -1) continue; const animationAsset = this._animationAssets[`${layer.name}:${stateName}`]; if (!animationAsset || !animationAsset.asset) { this.findAnimationLayer(layer.name).assignAnimation(stateName, AnimTrack.EMPTY); continue; } const assetId = animationAsset.asset; const asset = this.system.app.assets.get(assetId); if (asset) { if (asset.resource) { this.onAnimationAssetLoaded(layer.name, stateName, asset); } else { asset.once('load', (function(layerName, stateName) { return (function(asset) { this.onAnimationAssetLoaded(layerName, stateName, asset); }).bind(this); }).bind(this)(layer.name, stateName)); this.system.app.assets.load(asset); } } } } } onAnimationAssetLoaded(layerName, stateName, asset) { this.findAnimationLayer(layerName).assignAnimation(stateName, asset.resource); } removeStateGraph() { this._stateGraph = null; this._stateGraphAsset = null; this._animationAssets = {}; this._layers = []; this._layerIndices = {}; this._parameters = {}; this._playing = false; this.unbind(); this._targets = {}; } reset() { this._assignParameters(this._stateGraph); for(let i = 0; i < this._layers.length; i++){ const layerPlaying = this._layers[i].playing; this._layers[i].reset(); this._layers[i].playing = layerPlaying; } } unbind() { if (!this._normalizeWeights) { Object.keys(this._targets).forEach((targetKey)=>{ this._targets[targetKey].unbind(); }); } } rebind() { this._targets = {}; for(let i = 0; i < this._layers.length; i++){ this._layers[i].rebind(); } } findAnimationLayer(name) { const layerIndex = this._layerIndices[name]; return this._layers[layerIndex] || null; } addAnimationState(nodeName, animTrack, speed = 1, loop = true, layerName = 'Base') { if (!this._stateGraph) { this.loadStateGraph(new AnimStateGraph({ 'layers': [ { 'name': layerName, 'states': [ { 'name': 'START', 'speed': 1 }, { 'name': nodeName, 'speed': speed, 'loop': loop, 'defaultState': true } ], 'transitions': [ { 'from': 'START', 'to': nodeName } ] } ], 'parameters': {} })); } const layer = this.findAnimationLayer(layerName); if (layer) { layer.assignAnimation(nodeName, animTrack, speed, loop); } else { this.addLayer(layerName)?.assignAnimation(nodeName, animTrack, speed, loop); } } assignAnimation(nodePath, animTrack, layerName, speed = 1, loop = true) { if (!this._stateGraph && nodePath.indexOf('.') === -1) { this.loadStateGraph(new AnimStateGraph({ 'layers': [ { 'name': 'Base', 'states': [ { 'name': 'START', 'speed': 1 }, { 'name': nodePath, 'speed': speed, 'loop': loop, 'defaultState': true } ], 'transitions': [ { 'from': 'START', 'to': nodePath } ] } ], 'parameters': {} })); this.baseLayer.assignAnimation(nodePath, animTrack); return; } const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer; if (!layer) { return; } layer.assignAnimation(nodePath, animTrack, speed, loop); } removeNodeAnimations(nodeName, layerName) { const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer; if (!layer) { return; } layer.removeNodeAnimations(nodeName); } getParameterValue(name, type) { const param = this._parameters[name]; if (param && param.type === type) { return param.value; } return undefined; } setParameterValue(name, type, value) { const param = this._parameters[name]; if (param && param.type === type) { param.value = value; return; } } getFloat(name) { return this.getParameterValue(name, ANIM_PARAMETER_FLOAT); } setFloat(name, value) { this.setParameterValue(name, ANIM_PARAMETER_FLOAT, value); } getInteger(name) { return this.getParameterValue(name, ANIM_PARAMETER_INTEGER); } setInteger(name, value) { if (typeof value === 'number' && value % 1 === 0) { this.setParameterValue(name, ANIM_PARAMETER_INTEGER, value); } } getBoolean(name) { return this.getParameterValue(name, ANIM_PARAMETER_BOOLEAN); } setBoolean(name, value) { this.setParameterValue(name, ANIM_PARAMETER_BOOLEAN, !!value); } getTrigger(name) { return this.getParameterValue(name, ANIM_PARAMETER_TRIGGER); } setTrigger(name, singleFrame = false) { this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, true); if (singleFrame) { this._consumedTriggers.add(name); } } resetTrigger(name) { this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, false); } onBeforeRemove() { if (Number.isFinite(this._stateGraphAsset)) { const stateGraphAsset = this.system.app.assets.get(this._stateGraphAsset); stateGraphAsset.off('change', this._onStateGraphAssetChangeEvent, this); } } update(dt) { for(let i = 0; i < this.layers.length; i++){ this.layers[i].update(dt * this.speed); } this._consumedTriggers.forEach((trigger)=>{ this.parameters[trigger].value = false; }); this._consumedTriggers.clear(); } resolveDuplicatedEntityReferenceProperties(oldAnim, duplicatedIdsMap) { if (oldAnim.rootBone && duplicatedIdsMap[oldAnim.rootBone.getGuid()]) { this.rootBone = duplicatedIdsMap[oldAnim.rootBone.getGuid()]; } else { this.rebind(); } } constructor(...args){ super(...args), this._stateGraphAsset = null, this._animationAssets = {}, this._speed = 1, this._activate = true, this._playing = false, this._rootBone = null, this._stateGraph = null, this._layers = [], this._layerIndices = {}, this._parameters = {}, this._targets = {}, this._consumedTriggers = new Set(), this._normalizeWeights = false, this.findParameter = (name)=>{ return this._parameters[name]; }, this.consumeTrigger = (name)=>{ this._consumedTriggers.add(name); }; } } class AnimComponentData { constructor(){ this.enabled = true; } } const _schema$j = [ 'enabled' ]; class AnimComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { super.initializeComponentData(component, data, _schema$j); const complexProperties = [ 'animationAssets', 'stateGraph', 'layers', 'masks' ]; Object.keys(data).forEach((key)=>{ if (complexProperties.includes(key)) return; component[key] = data[key]; }); if (data.stateGraph) { component.stateGraph = data.stateGraph; component.loadStateGraph(component.stateGraph); } if (data.layers) { data.layers.forEach((layer, i)=>{ layer._controller.states.forEach((stateKey)=>{ layer._controller._states[stateKey]._animationList.forEach((node)=>{ if (!node.animTrack || node.animTrack === AnimTrack.EMPTY) { const animationAsset = this.app.assets.get(layer._component._animationAssets[`${layer.name}:${node.name}`].asset); if (animationAsset && !animationAsset.loaded) { animationAsset.once('load', ()=>{ component.layers[i].assignAnimation(node.name, animationAsset.resource); }); } } else { component.layers[i].assignAnimation(node.name, node.animTrack); } }); }); }); } if (data.animationAssets) { component.animationAssets = Object.assign(component.animationAssets, data.animationAssets); } if (data.masks) { Object.keys(data.masks).forEach((key)=>{ if (component.layers[key]) { const maskData = data.masks[key].mask; const mask = {}; Object.keys(maskData).forEach((maskKey)=>{ mask[decodeURI(maskKey)] = maskData[maskKey]; }); component.layers[key].mask = mask; } }); } } onAnimationUpdate(dt) { const components = this.store; for(const id in components){ if (components.hasOwnProperty(id)) { const component = components[id].entity.anim; const componentData = component.data; if (componentData.enabled && component.entity.enabled && component.playing) { component.update(dt); } } } } cloneComponent(entity, clone) { let masks; if (!entity.anim.rootBone || entity.anim.rootBone === entity) { masks = {}; entity.anim.layers.forEach((layer, i)=>{ if (layer.mask) { const mask = {}; Object.keys(layer.mask).forEach((path)=>{ const pathArr = path.split('/'); pathArr.shift(); const clonePath = [ clone.name, ...pathArr ].join('/'); mask[clonePath] = layer.mask[path]; }); masks[i] = { mask }; } }); } const data = { enabled: entity.anim.enabled, stateGraphAsset: entity.anim.stateGraphAsset, animationAssets: entity.anim.animationAssets, speed: entity.anim.speed, activate: entity.anim.activate, playing: entity.anim.playing, rootBone: entity.anim.rootBone, stateGraph: entity.anim.stateGraph, layers: entity.anim.layers, layerIndices: entity.anim.layerIndices, parameters: entity.anim.parameters, normalizeWeights: entity.anim.normalizeWeights, masks }; return this.addComponent(clone, data); } onBeforeRemove(entity, component) { component.onBeforeRemove(); } destroy() { super.destroy(); this.app.systems.off('animationUpdate', this.onAnimationUpdate, this); } constructor(app){ super(app); this.id = 'anim'; this.ComponentType = AnimComponent; this.DataType = AnimComponentData; this.schema = _schema$j; this.on('beforeremove', this.onBeforeRemove, this); this.app.systems.on('animationUpdate', this.onAnimationUpdate, this); } } Component._buildAccessors(AnimComponent.prototype, _schema$j); class AudioListenerComponent extends Component { setCurrentListener() { if (this.enabled && this.entity.audiolistener && this.entity.enabled) { this.system.current = this.entity; const position = this.system.current.getPosition(); this.system.manager.listener.setPosition(position); } } onEnable() { this.setCurrentListener(); } onDisable() { if (this.system.current === this.entity) { this.system.current = null; } } } class AudioListenerComponentData { constructor(){ this.enabled = true; } } const _schema$i = [ 'enabled' ]; class AudioListenerComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { properties = [ 'enabled' ]; super.initializeComponentData(component, data, properties); } onUpdate(dt) { if (this.current) { const position = this.current.getPosition(); this.manager.listener.setPosition(position); const wtm = this.current.getWorldTransform(); this.manager.listener.setOrientation(wtm); } } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); } constructor(app){ super(app); this.id = 'audiolistener'; this.ComponentType = AudioListenerComponent; this.DataType = AudioListenerComponentData; this.schema = _schema$i; this.manager = app.soundManager; this.current = null; this.app.systems.on('update', this.onUpdate, this); } } Component._buildAccessors(AudioListenerComponent.prototype, _schema$i); const BUTTON_TRANSITION_MODE_TINT = 0; const BUTTON_TRANSITION_MODE_SPRITE_CHANGE = 1; const ELEMENTTYPE_GROUP = 'group'; const ELEMENTTYPE_IMAGE = 'image'; const ELEMENTTYPE_TEXT = 'text'; const FITMODE_STRETCH = 'stretch'; const FITMODE_CONTAIN = 'contain'; const FITMODE_COVER = 'cover'; const VisualState = { DEFAULT: 'DEFAULT', HOVER: 'HOVER', PRESSED: 'PRESSED', INACTIVE: 'INACTIVE' }; const STATES_TO_TINT_NAMES = {}; STATES_TO_TINT_NAMES[VisualState.DEFAULT] = '_defaultTint'; STATES_TO_TINT_NAMES[VisualState.HOVER] = 'hoverTint'; STATES_TO_TINT_NAMES[VisualState.PRESSED] = 'pressedTint'; STATES_TO_TINT_NAMES[VisualState.INACTIVE] = 'inactiveTint'; const STATES_TO_SPRITE_ASSET_NAMES = {}; STATES_TO_SPRITE_ASSET_NAMES[VisualState.DEFAULT] = '_defaultSpriteAsset'; STATES_TO_SPRITE_ASSET_NAMES[VisualState.HOVER] = 'hoverSpriteAsset'; STATES_TO_SPRITE_ASSET_NAMES[VisualState.PRESSED] = 'pressedSpriteAsset'; STATES_TO_SPRITE_ASSET_NAMES[VisualState.INACTIVE] = 'inactiveSpriteAsset'; const STATES_TO_SPRITE_FRAME_NAMES = {}; STATES_TO_SPRITE_FRAME_NAMES[VisualState.DEFAULT] = '_defaultSpriteFrame'; STATES_TO_SPRITE_FRAME_NAMES[VisualState.HOVER] = 'hoverSpriteFrame'; STATES_TO_SPRITE_FRAME_NAMES[VisualState.PRESSED] = 'pressedSpriteFrame'; STATES_TO_SPRITE_FRAME_NAMES[VisualState.INACTIVE] = 'inactiveSpriteFrame'; class ButtonComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) { this._setValue('enabled', arg); } get enabled() { return this.data.enabled; } set active(arg) { this._setValue('active', arg); } get active() { return this.data.active; } set imageEntity(arg) { if (this._imageEntity !== arg) { const isString = typeof arg === 'string'; if (this._imageEntity && isString && this._imageEntity.getGuid() === arg) { return; } if (this._imageEntity) { this._imageEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._imageEntity = arg; } else if (isString) { this._imageEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._imageEntity = null; } if (this._imageEntity) { this._imageEntitySubscribe(); } if (this._imageEntity) { this.data.imageEntity = this._imageEntity.getGuid(); } else if (isString && arg) { this.data.imageEntity = arg; } } } get imageEntity() { return this._imageEntity; } set hitPadding(arg) { this._setValue('hitPadding', arg); } get hitPadding() { return this.data.hitPadding; } set transitionMode(arg) { this._setValue('transitionMode', arg); } get transitionMode() { return this.data.transitionMode; } set hoverTint(arg) { this._setValue('hoverTint', arg); } get hoverTint() { return this.data.hoverTint; } set pressedTint(arg) { this._setValue('pressedTint', arg); } get pressedTint() { return this.data.pressedTint; } set inactiveTint(arg) { this._setValue('inactiveTint', arg); } get inactiveTint() { return this.data.inactiveTint; } set fadeDuration(arg) { this._setValue('fadeDuration', arg); } get fadeDuration() { return this.data.fadeDuration; } set hoverSpriteAsset(arg) { this._setValue('hoverSpriteAsset', arg); } get hoverSpriteAsset() { return this.data.hoverSpriteAsset; } set hoverSpriteFrame(arg) { this._setValue('hoverSpriteFrame', arg); } get hoverSpriteFrame() { return this.data.hoverSpriteFrame; } set pressedSpriteAsset(arg) { this._setValue('pressedSpriteAsset', arg); } get pressedSpriteAsset() { return this.data.pressedSpriteAsset; } set pressedSpriteFrame(arg) { this._setValue('pressedSpriteFrame', arg); } get pressedSpriteFrame() { return this.data.pressedSpriteFrame; } set inactiveSpriteAsset(arg) { this._setValue('inactiveSpriteAsset', arg); } get inactiveSpriteAsset() { return this.data.inactiveSpriteAsset; } set inactiveSpriteFrame(arg) { this._setValue('inactiveSpriteFrame', arg); } get inactiveSpriteFrame() { return this.data.inactiveSpriteFrame; } _setValue(name, value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); } _toggleLifecycleListeners(onOrOff, system) { this[onOrOff]('set_active', this._onSetActive, this); this[onOrOff]('set_transitionMode', this._onSetTransitionMode, this); this[onOrOff]('set_hoverTint', this._onSetTransitionValue, this); this[onOrOff]('set_pressedTint', this._onSetTransitionValue, this); this[onOrOff]('set_inactiveTint', this._onSetTransitionValue, this); this[onOrOff]('set_hoverSpriteAsset', this._onSetTransitionValue, this); this[onOrOff]('set_hoverSpriteFrame', this._onSetTransitionValue, this); this[onOrOff]('set_pressedSpriteAsset', this._onSetTransitionValue, this); this[onOrOff]('set_pressedSpriteFrame', this._onSetTransitionValue, this); this[onOrOff]('set_inactiveSpriteAsset', this._onSetTransitionValue, this); this[onOrOff]('set_inactiveSpriteFrame', this._onSetTransitionValue, this); if (onOrOff === 'on') { this._evtElementAdd = this.entity.on('element:add', this._onElementComponentAdd, this); } else { this._evtElementAdd?.off(); this._evtElementAdd = null; } } _onSetActive(name, oldValue, newValue) { if (oldValue !== newValue) { this._updateVisualState(); } } _onSetTransitionMode(name, oldValue, newValue) { if (oldValue !== newValue) { this._cancelTween(); this._resetToDefaultVisualState(oldValue); this._forceReapplyVisualState(); } } _onSetTransitionValue(name, oldValue, newValue) { if (oldValue !== newValue) { this._forceReapplyVisualState(); } } _imageEntitySubscribe() { this._evtImageEntityElementAdd = this._imageEntity.on('element:add', this._onImageElementGain, this); if (this._imageEntity.element) { this._onImageElementGain(); } } _imageEntityUnsubscribe() { this._evtImageEntityElementAdd?.off(); this._evtImageEntityElementAdd = null; if (this._imageEntity?.element) { this._onImageElementLose(); } } _imageEntityElementSubscribe() { const element = this._imageEntity.element; this._evtImageEntityElementRemove = element.once('beforeremove', this._onImageElementLose, this); this._evtImageEntityElementColor = element.on('set:color', this._onSetColor, this); this._evtImageEntityElementOpacity = element.on('set:opacity', this._onSetOpacity, this); this._evtImageEntityElementSpriteAsset = element.on('set:spriteAsset', this._onSetSpriteAsset, this); this._evtImageEntityElementSpriteFrame = element.on('set:spriteFrame', this._onSetSpriteFrame, this); } _imageEntityElementUnsubscribe() { this._evtImageEntityElementRemove?.off(); this._evtImageEntityElementRemove = null; this._evtImageEntityElementColor?.off(); this._evtImageEntityElementColor = null; this._evtImageEntityElementOpacity?.off(); this._evtImageEntityElementOpacity = null; this._evtImageEntityElementSpriteAsset?.off(); this._evtImageEntityElementSpriteAsset = null; this._evtImageEntityElementSpriteFrame?.off(); this._evtImageEntityElementSpriteFrame = null; } _onElementComponentRemove() { this._toggleHitElementListeners('off'); } _onElementComponentAdd() { this._toggleHitElementListeners('on'); } _onImageElementLose() { this._imageEntityElementUnsubscribe(); this._cancelTween(); this._resetToDefaultVisualState(this.transitionMode); } _onImageElementGain() { this._imageEntityElementSubscribe(); this._storeDefaultVisualState(); this._forceReapplyVisualState(); } _toggleHitElementListeners(onOrOff) { if (this.entity.element) { const isAdding = onOrOff === 'on'; if (isAdding && this._hasHitElementListeners) { return; } this.entity.element[onOrOff]('beforeremove', this._onElementComponentRemove, this); this.entity.element[onOrOff]('mouseenter', this._onMouseEnter, this); this.entity.element[onOrOff]('mouseleave', this._onMouseLeave, this); this.entity.element[onOrOff]('mousedown', this._onMouseDown, this); this.entity.element[onOrOff]('mouseup', this._onMouseUp, this); this.entity.element[onOrOff]('touchstart', this._onTouchStart, this); this.entity.element[onOrOff]('touchend', this._onTouchEnd, this); this.entity.element[onOrOff]('touchleave', this._onTouchLeave, this); this.entity.element[onOrOff]('touchcancel', this._onTouchCancel, this); this.entity.element[onOrOff]('selectstart', this._onSelectStart, this); this.entity.element[onOrOff]('selectend', this._onSelectEnd, this); this.entity.element[onOrOff]('selectenter', this._onSelectEnter, this); this.entity.element[onOrOff]('selectleave', this._onSelectLeave, this); this.entity.element[onOrOff]('click', this._onClick, this); this._hasHitElementListeners = isAdding; } } _storeDefaultVisualState() { const element = this._imageEntity?.element; if (!element || element.type === ELEMENTTYPE_GROUP) { return; } this._storeDefaultColor(element.color); this._storeDefaultOpacity(element.opacity); this._storeDefaultSpriteAsset(element.spriteAsset); this._storeDefaultSpriteFrame(element.spriteFrame); } _storeDefaultColor(color) { this._defaultTint.r = color.r; this._defaultTint.g = color.g; this._defaultTint.b = color.b; } _storeDefaultOpacity(opacity) { this._defaultTint.a = opacity; } _storeDefaultSpriteAsset(spriteAsset) { this._defaultSpriteAsset = spriteAsset; } _storeDefaultSpriteFrame(spriteFrame) { this._defaultSpriteFrame = spriteFrame; } _onSetColor(color) { if (!this._isApplyingTint) { this._storeDefaultColor(color); this._forceReapplyVisualState(); } } _onSetOpacity(opacity) { if (!this._isApplyingTint) { this._storeDefaultOpacity(opacity); this._forceReapplyVisualState(); } } _onSetSpriteAsset(spriteAsset) { if (!this._isApplyingSprite) { this._storeDefaultSpriteAsset(spriteAsset); this._forceReapplyVisualState(); } } _onSetSpriteFrame(spriteFrame) { if (!this._isApplyingSprite) { this._storeDefaultSpriteFrame(spriteFrame); this._forceReapplyVisualState(); } } _onMouseEnter(event) { this._isHovering = true; this._updateVisualState(); this._fireIfActive('mouseenter', event); } _onMouseLeave(event) { this._isHovering = false; this._isPressed = false; this._updateVisualState(); this._fireIfActive('mouseleave', event); } _onMouseDown(event) { this._isPressed = true; this._updateVisualState(); this._fireIfActive('mousedown', event); } _onMouseUp(event) { this._isPressed = false; this._updateVisualState(); this._fireIfActive('mouseup', event); } _onTouchStart(event) { this._isPressed = true; this._updateVisualState(); this._fireIfActive('touchstart', event); } _onTouchEnd(event) { event.event.preventDefault(); this._isPressed = false; this._updateVisualState(); this._fireIfActive('touchend', event); } _onTouchLeave(event) { this._isPressed = false; this._updateVisualState(); this._fireIfActive('touchleave', event); } _onTouchCancel(event) { this._isPressed = false; this._updateVisualState(); this._fireIfActive('touchcancel', event); } _onSelectStart(event) { this._isPressed = true; this._updateVisualState(); this._fireIfActive('selectstart', event); } _onSelectEnd(event) { this._isPressed = false; this._updateVisualState(); this._fireIfActive('selectend', event); } _onSelectEnter(event) { this._hoveringCounter++; if (this._hoveringCounter === 1) { this._isHovering = true; this._updateVisualState(); } this._fireIfActive('selectenter', event); } _onSelectLeave(event) { this._hoveringCounter--; if (this._hoveringCounter === 0) { this._isHovering = false; this._isPressed = false; this._updateVisualState(); } this._fireIfActive('selectleave', event); } _onClick(event) { this._fireIfActive('click', event); } _fireIfActive(name, event) { if (this.data.active) { this.fire(name, event); } } _updateVisualState(force) { const oldVisualState = this._visualState; const newVisualState = this._determineVisualState(); if ((oldVisualState !== newVisualState || force) && this.enabled) { this._visualState = newVisualState; if (oldVisualState === VisualState.HOVER) { this._fireIfActive('hoverend'); } if (oldVisualState === VisualState.PRESSED) { this._fireIfActive('pressedend'); } if (newVisualState === VisualState.HOVER) { this._fireIfActive('hoverstart'); } if (newVisualState === VisualState.PRESSED) { this._fireIfActive('pressedstart'); } switch(this.transitionMode){ case BUTTON_TRANSITION_MODE_TINT: { const tintName = STATES_TO_TINT_NAMES[this._visualState]; const tintColor = this[tintName]; this._applyTint(tintColor); break; } case BUTTON_TRANSITION_MODE_SPRITE_CHANGE: { const spriteAssetName = STATES_TO_SPRITE_ASSET_NAMES[this._visualState]; const spriteFrameName = STATES_TO_SPRITE_FRAME_NAMES[this._visualState]; const spriteAsset = this[spriteAssetName]; const spriteFrame = this[spriteFrameName]; this._applySprite(spriteAsset, spriteFrame); break; } } } } _forceReapplyVisualState() { this._updateVisualState(true); } _resetToDefaultVisualState(transitionMode) { if (!this._imageEntity?.element) { return; } switch(transitionMode){ case BUTTON_TRANSITION_MODE_TINT: this._cancelTween(); this._applyTintImmediately(this._defaultTint); break; case BUTTON_TRANSITION_MODE_SPRITE_CHANGE: this._applySprite(this._defaultSpriteAsset, this._defaultSpriteFrame); break; } } _determineVisualState() { if (!this.active) { return VisualState.INACTIVE; } else if (this._isPressed) { return VisualState.PRESSED; } else if (this._isHovering) { return VisualState.HOVER; } return VisualState.DEFAULT; } _applySprite(spriteAsset, spriteFrame) { const element = this._imageEntity?.element; if (!element) { return; } spriteFrame = spriteFrame || 0; this._isApplyingSprite = true; if (element.spriteAsset !== spriteAsset) { element.spriteAsset = spriteAsset; } if (element.spriteFrame !== spriteFrame) { element.spriteFrame = spriteFrame; } this._isApplyingSprite = false; } _applyTint(tintColor) { this._cancelTween(); if (this.fadeDuration === 0) { this._applyTintImmediately(tintColor); } else { this._applyTintWithTween(tintColor); } } _applyTintImmediately(tintColor) { const element = this._imageEntity?.element; if (!tintColor || !element || element.type === ELEMENTTYPE_GROUP) { return; } const color3 = toColor3(tintColor); this._isApplyingTint = true; if (!color3.equals(element.color)) { element.color = color3; } if (element.opacity !== tintColor.a) { element.opacity = tintColor.a; } this._isApplyingTint = false; } _applyTintWithTween(tintColor) { const element = this._imageEntity?.element; if (!tintColor || !element || element.type === ELEMENTTYPE_GROUP) { return; } const color3 = toColor3(tintColor); const color = element.color; const opacity = element.opacity; if (color3.equals(color) && tintColor.a === opacity) return; this._tweenInfo = { startTime: now(), from: new Color(color.r, color.g, color.b, opacity), to: tintColor.clone(), lerpColor: new Color() }; } _updateTintTween() { const elapsedTime = now() - this._tweenInfo.startTime; let elapsedProportion = this.fadeDuration === 0 ? 1 : elapsedTime / this.fadeDuration; elapsedProportion = math.clamp(elapsedProportion, 0, 1); if (Math.abs(elapsedProportion - 1) > 1e-5) { const lerpColor = this._tweenInfo.lerpColor; lerpColor.lerp(this._tweenInfo.from, this._tweenInfo.to, elapsedProportion); this._applyTintImmediately(new Color(lerpColor.r, lerpColor.g, lerpColor.b, lerpColor.a)); } else { this._applyTintImmediately(this._tweenInfo.to); this._cancelTween(); } } _cancelTween() { delete this._tweenInfo; } onUpdate() { if (this._tweenInfo) { this._updateTintTween(); } } onEnable() { this._isHovering = false; this._hoveringCounter = 0; this._isPressed = false; this._toggleHitElementListeners('on'); this._forceReapplyVisualState(); } onDisable() { this._toggleHitElementListeners('off'); this._resetToDefaultVisualState(this.transitionMode); } onRemove() { this._imageEntityUnsubscribe(); this._toggleLifecycleListeners('off', this.system); this.onDisable(); } resolveDuplicatedEntityReferenceProperties(oldButton, duplicatedIdsMap) { if (oldButton.imageEntity) { this.imageEntity = duplicatedIdsMap[oldButton.imageEntity.getGuid()]; } } constructor(system, entity){ super(system, entity), this._visualState = VisualState.DEFAULT, this._isHovering = false, this._hoveringCounter = 0, this._isPressed = false, this._defaultTint = new Color(1, 1, 1, 1), this._defaultSpriteAsset = null, this._defaultSpriteFrame = 0, this._imageEntity = null, this._evtElementAdd = null, this._evtImageEntityElementAdd = null, this._evtImageEntityElementRemove = null, this._evtImageEntityElementColor = null, this._evtImageEntityElementOpacity = null, this._evtImageEntityElementSpriteAsset = null, this._evtImageEntityElementSpriteFrame = null; this._visualState = VisualState.DEFAULT; this._isHovering = false; this._hoveringCounter = 0; this._isPressed = false; this._defaultTint = new Color(1, 1, 1, 1); this._defaultSpriteAsset = null; this._defaultSpriteFrame = 0; this._toggleLifecycleListeners('on', system); } } ButtonComponent.EVENT_MOUSEDOWN = 'mousedown'; ButtonComponent.EVENT_MOUSEUP = 'mouseup'; ButtonComponent.EVENT_MOUSEENTER = 'mouseenter'; ButtonComponent.EVENT_MOUSELEAVE = 'mouseleave'; ButtonComponent.EVENT_CLICK = 'click'; ButtonComponent.EVENT_TOUCHSTART = 'touchstart'; ButtonComponent.EVENT_TOUCHEND = 'touchend'; ButtonComponent.EVENT_TOUCHCANCEL = 'touchcancel'; ButtonComponent.EVENT_TOUCHLEAVE = 'touchleave'; ButtonComponent.EVENT_SELECTSTART = 'selectstart'; ButtonComponent.EVENT_SELECTEND = 'selectend'; ButtonComponent.EVENT_SELECTENTER = 'selectenter'; ButtonComponent.EVENT_SELECTLEAVE = 'selectleave'; ButtonComponent.EVENT_HOVERSTART = 'hoverstart'; ButtonComponent.EVENT_HOVEREND = 'hoverend'; ButtonComponent.EVENT_PRESSEDSTART = 'pressedstart'; ButtonComponent.EVENT_PRESSEDEND = 'pressedend'; function toColor3(color4) { return new Color(color4.r, color4.g, color4.b); } class ButtonComponentData { constructor(){ this.enabled = true; this.active = true; this.imageEntity = null; this.hitPadding = new Vec4(); this.transitionMode = BUTTON_TRANSITION_MODE_TINT; this.hoverTint = new Color(0.75, 0.75, 0.75); this.pressedTint = new Color(0.5, 0.5, 0.5); this.inactiveTint = new Color(0.25, 0.25, 0.25); this.fadeDuration = 0; this.hoverSpriteAsset = null; this.hoverSpriteFrame = 0; this.pressedSpriteAsset = null; this.pressedSpriteFrame = 0; this.inactiveSpriteAsset = null; this.inactiveSpriteFrame = 0; } } const _schema$h = [ 'enabled', 'active', { name: 'hitPadding', type: 'vec4' }, 'transitionMode', { name: 'hoverTint', type: 'rgba' }, { name: 'pressedTint', type: 'rgba' }, { name: 'inactiveTint', type: 'rgba' }, 'fadeDuration', 'hoverSpriteAsset', 'hoverSpriteFrame', 'pressedSpriteAsset', 'pressedSpriteFrame', 'inactiveSpriteAsset', 'inactiveSpriteFrame' ]; class ButtonComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { component.imageEntity = data.imageEntity; super.initializeComponentData(component, data, _schema$h); } onUpdate(dt) { const components = this.store; for(const id in components){ const entity = components[id].entity; const component = entity.button; if (component.enabled && entity.enabled) { component.onUpdate(); } } } _onRemoveComponent(entity, component) { component.onRemove(); } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); } constructor(app){ super(app); this.id = 'button'; this.ComponentType = ButtonComponent; this.DataType = ButtonComponentData; this.schema = _schema$h; this.on('beforeremove', this._onRemoveComponent, this); this.app.systems.on('update', this.onUpdate, this); } } const _vec3$1 = new Vec3(); const _quat = new Quat(); class CollisionComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) { this._setValue('enabled', arg); } get enabled() { return this.data.enabled; } set type(arg) { this._setValue('type', arg); } get type() { return this.data.type; } set halfExtents(arg) { this._setValue('halfExtents', arg); } get halfExtents() { return this.data.halfExtents; } set linearOffset(arg) { this._setValue('linearOffset', arg); } get linearOffset() { return this.data.linearOffset; } set angularOffset(arg) { this._setValue('angularOffset', arg); } get angularOffset() { return this.data.angularOffset; } set radius(arg) { this._setValue('radius', arg); } get radius() { return this.data.radius; } set axis(arg) { this._setValue('axis', arg); } get axis() { return this.data.axis; } set height(arg) { this._setValue('height', arg); } get height() { return this.data.height; } set asset(arg) { this._setValue('asset', arg); } get asset() { return this.data.asset; } set renderAsset(arg) { this._setValue('renderAsset', arg); } get renderAsset() { return this.data.renderAsset; } set convexHull(arg) { this._setValue('convexHull', arg); } get convexHull() { return this.data.convexHull; } set shape(arg) { this._setValue('shape', arg); } get shape() { return this.data.shape; } set model(arg) { this._setValue('model', arg); } get model() { return this.data.model; } set render(arg) { this._setValue('render', arg); } get render() { return this.data.render; } set checkVertexDuplicates(arg) { this._setValue('checkVertexDuplicates', arg); } get checkVertexDuplicates() { return this.data.checkVertexDuplicates; } _setValue(name, value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); } onSetType(name, oldValue, newValue) { if (oldValue !== newValue) { this.system.changeType(this, oldValue, newValue); } } onSetHalfExtents(name, oldValue, newValue) { const t = this.data.type; if (this.data.initialized && t === 'box') { this.system.recreatePhysicalShapes(this); } } onSetOffset(name, oldValue, newValue) { this._hasOffset = !this.data.linearOffset.equals(Vec3.ZERO) || !this.data.angularOffset.equals(Quat.IDENTITY); if (this.data.initialized) { this.system.recreatePhysicalShapes(this); } } onSetRadius(name, oldValue, newValue) { const t = this.data.type; if (this.data.initialized && (t === 'sphere' || t === 'capsule' || t === 'cylinder' || t === 'cone')) { this.system.recreatePhysicalShapes(this); } } onSetHeight(name, oldValue, newValue) { const t = this.data.type; if (this.data.initialized && (t === 'capsule' || t === 'cylinder' || t === 'cone')) { this.system.recreatePhysicalShapes(this); } } onSetAxis(name, oldValue, newValue) { const t = this.data.type; if (this.data.initialized && (t === 'capsule' || t === 'cylinder' || t === 'cone')) { this.system.recreatePhysicalShapes(this); } } onSetAsset(name, oldValue, newValue) { const assets = this.system.app.assets; if (oldValue) { const asset = assets.get(oldValue); if (asset) { asset.off('remove', this.onAssetRemoved, this); } } if (newValue) { if (newValue instanceof Asset) { this.data.asset = newValue.id; } const asset = assets.get(this.data.asset); if (asset) { asset.off('remove', this.onAssetRemoved, this); asset.on('remove', this.onAssetRemoved, this); } } if (this.data.initialized && this.data.type === 'mesh') { if (!newValue) { this.data.model = null; } this.system.recreatePhysicalShapes(this); } } onSetRenderAsset(name, oldValue, newValue) { const assets = this.system.app.assets; if (oldValue) { const asset = assets.get(oldValue); if (asset) { asset.off('remove', this.onRenderAssetRemoved, this); } } if (newValue) { if (newValue instanceof Asset) { this.data.renderAsset = newValue.id; } const asset = assets.get(this.data.renderAsset); if (asset) { asset.off('remove', this.onRenderAssetRemoved, this); asset.on('remove', this.onRenderAssetRemoved, this); } } if (this.data.initialized && this.data.type === 'mesh') { if (!newValue) { this.data.render = null; } this.system.recreatePhysicalShapes(this); } } onSetModel(name, oldValue, newValue) { if (this.data.initialized && this.data.type === 'mesh') { this.system.implementations.mesh.doRecreatePhysicalShape(this); } } onSetRender(name, oldValue, newValue) { this.onSetModel(name, oldValue, newValue); } onAssetRemoved(asset) { asset.off('remove', this.onAssetRemoved, this); if (this.data.asset === asset.id) { this.asset = null; } } onRenderAssetRemoved(asset) { asset.off('remove', this.onRenderAssetRemoved, this); if (this.data.renderAsset === asset.id) { this.renderAsset = null; } } getCompoundChildShapeIndex(shape) { const compound = this.data.shape; const shapes = compound.getNumChildShapes(); for(let i = 0; i < shapes; i++){ const childShape = compound.getChildShape(i); if (Ammo.getPointer(childShape) === Ammo.getPointer(shape)) { return i; } } return null; } _onInsert(parent) { if (typeof Ammo === 'undefined') { return; } if (this._compoundParent) { this.system.recreatePhysicalShapes(this); } else if (!this.entity.rigidbody) { let ancestor = this.entity.parent; while(ancestor){ if (ancestor.collision && ancestor.collision.type === 'compound') { if (ancestor.collision.shape.getNumChildShapes() === 0) { this.system.recreatePhysicalShapes(ancestor.collision); } else { this.system.recreatePhysicalShapes(this); } break; } ancestor = ancestor.parent; } } } _updateCompound() { const entity = this.entity; if (entity._dirtyWorld) { let dirty = entity._dirtyLocal; let parent = entity; while(parent && !dirty){ if (parent.collision && parent.collision === this._compoundParent) { break; } if (parent._dirtyLocal) { dirty = true; } parent = parent.parent; } if (dirty) { entity.forEach(this.system.implementations.compound._updateEachDescendantTransform, entity); const bodyComponent = this._compoundParent.entity.rigidbody; if (bodyComponent) { bodyComponent.activate(); } } } } getShapePosition() { const pos = this.entity.getPosition(); if (this._hasOffset) { const rot = this.entity.getRotation(); const lo = this.data.linearOffset; _quat.copy(rot).transformVector(lo, _vec3$1); return _vec3$1.add(pos); } return pos; } getShapeRotation() { const rot = this.entity.getRotation(); if (this._hasOffset) { return _quat.copy(rot).mul(this.data.angularOffset); } return rot; } onEnable() { if (this.data.type === 'mesh' && (this.data.asset || this.data.renderAsset) && this.data.initialized) { const asset = this.system.app.assets.get(this.data.asset || this.data.renderAsset); if (asset && (!asset.resource || !this.data.shape)) { this.system.recreatePhysicalShapes(this); return; } } if (this.entity.rigidbody) { if (this.entity.rigidbody.enabled) { this.entity.rigidbody.enableSimulation(); } } else if (this._compoundParent && this !== this._compoundParent) { if (this._compoundParent.shape.getNumChildShapes() === 0) { this.system.recreatePhysicalShapes(this._compoundParent); } else { const transform = this.system._getNodeTransform(this.entity, this._compoundParent.entity); this._compoundParent.shape.addChildShape(transform, this.data.shape); Ammo.destroy(transform); if (this._compoundParent.entity.rigidbody) { this._compoundParent.entity.rigidbody.activate(); } } } else if (this.entity.trigger) { this.entity.trigger.enable(); } } onDisable() { if (this.entity.rigidbody) { this.entity.rigidbody.disableSimulation(); } else if (this._compoundParent && this !== this._compoundParent) { if (!this._compoundParent.entity._destroying) { this.system._removeCompoundChild(this._compoundParent, this.data.shape); if (this._compoundParent.entity.rigidbody) { this._compoundParent.entity.rigidbody.activate(); } } } else if (this.entity.trigger) { this.entity.trigger.disable(); } } onBeforeRemove() { if (this.asset) { this.asset = null; } if (this.renderAsset) { this.renderAsset = null; } this.entity.off('insert', this._onInsert, this); this.off(); } constructor(system, entity){ super(system, entity), this._compoundParent = null, this._hasOffset = false; this.entity.on('insert', this._onInsert, this); this.on('set_type', this.onSetType, this); this.on('set_convexHull', this.onSetModel, this); this.on('set_halfExtents', this.onSetHalfExtents, this); this.on('set_linearOffset', this.onSetOffset, this); this.on('set_angularOffset', this.onSetOffset, this); this.on('set_radius', this.onSetRadius, this); this.on('set_height', this.onSetHeight, this); this.on('set_axis', this.onSetAxis, this); this.on('set_asset', this.onSetAsset, this); this.on('set_renderAsset', this.onSetRenderAsset, this); this.on('set_model', this.onSetModel, this); this.on('set_render', this.onSetRender, this); } } CollisionComponent.EVENT_CONTACT = 'contact'; CollisionComponent.EVENT_COLLISIONSTART = 'collisionstart'; CollisionComponent.EVENT_COLLISIONEND = 'collisionend'; CollisionComponent.EVENT_TRIGGERENTER = 'triggerenter'; CollisionComponent.EVENT_TRIGGERLEAVE = 'triggerleave'; class CollisionComponentData { constructor(){ this.enabled = true; this.type = 'box'; this.halfExtents = new Vec3(0.5, 0.5, 0.5); this.linearOffset = new Vec3(); this.angularOffset = new Quat(); this.radius = 0.5; this.axis = 1; this.height = 2; this.convexHull = false; this.asset = null; this.renderAsset = null; this.checkVertexDuplicates = true; this.shape = null; this.model = null; this.render = null; this.initialized = false; } } const BODYTYPE_STATIC = 'static'; const BODYTYPE_DYNAMIC = 'dynamic'; const BODYTYPE_KINEMATIC = 'kinematic'; const BODYFLAG_KINEMATIC_OBJECT = 2; const BODYFLAG_NORESPONSE_OBJECT = 4; const BODYSTATE_ACTIVE_TAG = 1; const BODYSTATE_DISABLE_DEACTIVATION = 4; const BODYSTATE_DISABLE_SIMULATION = 5; const BODYGROUP_DYNAMIC = 1; const BODYGROUP_STATIC = 2; const BODYGROUP_KINEMATIC = 4; const BODYGROUP_TRIGGER = 16; const BODYMASK_ALL = 65535; const BODYMASK_NOT_STATIC = 65535 ^ 2; let _ammoVec1$1, _ammoQuat$1, _ammoTransform$1; class Trigger { initialize(data) { const entity = this.entity; const shape = data.shape; if (shape && typeof Ammo !== 'undefined') { if (entity.trigger) { entity.trigger.destroy(); } const mass = 1; const component = this.component; if (component) { const bodyPos = component.getShapePosition(); const bodyRot = component.getShapeRotation(); _ammoVec1$1.setValue(bodyPos.x, bodyPos.y, bodyPos.z); _ammoQuat$1.setValue(bodyRot.x, bodyRot.y, bodyRot.z, bodyRot.w); } else { const pos = entity.getPosition(); const rot = entity.getRotation(); _ammoVec1$1.setValue(pos.x, pos.y, pos.z); _ammoQuat$1.setValue(rot.x, rot.y, rot.z, rot.w); } _ammoTransform$1.setOrigin(_ammoVec1$1); _ammoTransform$1.setRotation(_ammoQuat$1); const body = this.app.systems.rigidbody.createBody(mass, shape, _ammoTransform$1); body.setRestitution(0); body.setFriction(0); body.setDamping(0, 0); _ammoVec1$1.setValue(0, 0, 0); body.setLinearFactor(_ammoVec1$1); body.setAngularFactor(_ammoVec1$1); body.setCollisionFlags(body.getCollisionFlags() | BODYFLAG_NORESPONSE_OBJECT); body.entity = entity; this.body = body; if (this.component.enabled && entity.enabled) { this.enable(); } } } destroy() { if (!this.body) return; this.disable(); this.app.systems.rigidbody.destroyBody(this.body); this.body = null; } _getEntityTransform(transform) { const component = this.component; if (component) { const bodyPos = component.getShapePosition(); const bodyRot = component.getShapeRotation(); _ammoVec1$1.setValue(bodyPos.x, bodyPos.y, bodyPos.z); _ammoQuat$1.setValue(bodyRot.x, bodyRot.y, bodyRot.z, bodyRot.w); } else { const pos = this.entity.getPosition(); const rot = this.entity.getRotation(); _ammoVec1$1.setValue(pos.x, pos.y, pos.z); _ammoQuat$1.setValue(rot.x, rot.y, rot.z, rot.w); } transform.setOrigin(_ammoVec1$1); transform.setRotation(_ammoQuat$1); } updateTransform() { this._getEntityTransform(_ammoTransform$1); const body = this.body; body.setWorldTransform(_ammoTransform$1); body.activate(); } enable() { const body = this.body; if (!body) return; const system = this.app.systems.rigidbody; const idx = system._triggers.indexOf(this); if (idx < 0) { system.addBody(body, BODYGROUP_TRIGGER, BODYMASK_NOT_STATIC ^ BODYGROUP_TRIGGER); system._triggers.push(this); } body.forceActivationState(BODYSTATE_ACTIVE_TAG); this.updateTransform(); } disable() { const body = this.body; if (!body) return; const system = this.app.systems.rigidbody; const idx = system._triggers.indexOf(this); if (idx > -1) { system.removeBody(body); system._triggers.splice(idx, 1); } body.forceActivationState(BODYSTATE_DISABLE_SIMULATION); } constructor(app, component, data){ this.entity = component.entity; this.component = component; this.app = app; if (typeof Ammo !== 'undefined' && !_ammoVec1$1) { _ammoVec1$1 = new Ammo.btVector3(); _ammoQuat$1 = new Ammo.btQuaternion(); _ammoTransform$1 = new Ammo.btTransform(); } this.initialize(data); } } const mat4 = new Mat4(); const p1 = new Vec3(); const p2 = new Vec3(); const quat$1 = new Quat(); const tempGraphNode = new GraphNode(); const _schema$g = [ 'enabled', 'type', 'halfExtents', 'linearOffset', 'angularOffset', 'radius', 'axis', 'height', 'convexHull', 'asset', 'renderAsset', 'shape', 'model', 'render', 'checkVertexDuplicates' ]; class CollisionSystemImpl { beforeInitialize(component, data) { data.shape = null; data.model = new Model(); data.model.graph = new GraphNode(); } afterInitialize(component, data) { this.recreatePhysicalShapes(component); component.data.initialized = true; } reset(component, data) { this.beforeInitialize(component, data); this.afterInitialize(component, data); } recreatePhysicalShapes(component) { const entity = component.entity; const data = component.data; if (typeof Ammo !== 'undefined') { if (entity.trigger) { entity.trigger.destroy(); delete entity.trigger; } if (data.shape) { if (component._compoundParent) { if (component !== component._compoundParent) { this.system._removeCompoundChild(component._compoundParent, data.shape); } if (component._compoundParent.entity.rigidbody) { component._compoundParent.entity.rigidbody.activate(); } } this.destroyShape(data); } data.shape = this.createPhysicalShape(component.entity, data); const firstCompoundChild = !component._compoundParent; if (data.type === 'compound' && (!component._compoundParent || component === component._compoundParent)) { component._compoundParent = component; entity.forEach(this._addEachDescendant, component); } else if (data.type !== 'compound') { if (!component.rigidbody) { component._compoundParent = null; let parent = entity.parent; while(parent){ if (parent.collision && parent.collision.type === 'compound') { component._compoundParent = parent.collision; break; } parent = parent.parent; } } } if (component._compoundParent) { if (component !== component._compoundParent) { if (firstCompoundChild && component._compoundParent.shape.getNumChildShapes() === 0) { this.system.recreatePhysicalShapes(component._compoundParent); } else { this.system.updateCompoundChildTransform(entity, true); if (component._compoundParent.entity.rigidbody) { component._compoundParent.entity.rigidbody.activate(); } } } } if (entity.rigidbody) { entity.rigidbody.disableSimulation(); entity.rigidbody.createBody(); if (entity.enabled && entity.rigidbody.enabled) { entity.rigidbody.enableSimulation(); } } else if (!component._compoundParent) { if (!entity.trigger) { entity.trigger = new Trigger(this.system.app, component, data); } else { entity.trigger.initialize(data); } } } } createPhysicalShape(entity, data) { return undefined; } updateTransform(component, position, rotation, scale) { if (component.entity.trigger) { component.entity.trigger.updateTransform(); } } destroyShape(data) { if (data.shape) { Ammo.destroy(data.shape); data.shape = null; } } beforeRemove(entity, component) { if (component.data.shape) { if (component._compoundParent && !component._compoundParent.entity._destroying) { this.system._removeCompoundChild(component._compoundParent, component.data.shape); if (component._compoundParent.entity.rigidbody) { component._compoundParent.entity.rigidbody.activate(); } } component._compoundParent = null; this.destroyShape(component.data); } } remove(entity, data) { if (entity.rigidbody && entity.rigidbody.body) { entity.rigidbody.disableSimulation(); } if (entity.trigger) { entity.trigger.destroy(); delete entity.trigger; } } clone(entity, clone) { const src = this.system.store[entity.getGuid()]; const data = { enabled: src.data.enabled, type: src.data.type, halfExtents: [ src.data.halfExtents.x, src.data.halfExtents.y, src.data.halfExtents.z ], linearOffset: [ src.data.linearOffset.x, src.data.linearOffset.y, src.data.linearOffset.z ], angularOffset: [ src.data.angularOffset.x, src.data.angularOffset.y, src.data.angularOffset.z, src.data.angularOffset.w ], radius: src.data.radius, axis: src.data.axis, height: src.data.height, convexHull: src.data.convexHull, asset: src.data.asset, renderAsset: src.data.renderAsset, model: src.data.model, render: src.data.render, checkVertexDuplicates: src.data.checkVertexDuplicates }; return this.system.addComponent(clone, data); } constructor(system){ this.system = system; } } class CollisionBoxSystemImpl extends CollisionSystemImpl { createPhysicalShape(entity, data) { if (typeof Ammo !== 'undefined') { const he = data.halfExtents; const ammoHe = new Ammo.btVector3(he ? he.x : 0.5, he ? he.y : 0.5, he ? he.z : 0.5); const shape = new Ammo.btBoxShape(ammoHe); Ammo.destroy(ammoHe); return shape; } return undefined; } } class CollisionSphereSystemImpl extends CollisionSystemImpl { createPhysicalShape(entity, data) { if (typeof Ammo !== 'undefined') { return new Ammo.btSphereShape(data.radius); } return undefined; } } class CollisionCapsuleSystemImpl extends CollisionSystemImpl { createPhysicalShape(entity, data) { const axis = data.axis ?? 1; const radius = data.radius ?? 0.5; const height = Math.max((data.height ?? 2) - 2 * radius, 0); let shape = null; if (typeof Ammo !== 'undefined') { switch(axis){ case 0: shape = new Ammo.btCapsuleShapeX(radius, height); break; case 1: shape = new Ammo.btCapsuleShape(radius, height); break; case 2: shape = new Ammo.btCapsuleShapeZ(radius, height); break; } } return shape; } } class CollisionCylinderSystemImpl extends CollisionSystemImpl { createPhysicalShape(entity, data) { const axis = data.axis ?? 1; const radius = data.radius ?? 0.5; const height = data.height ?? 1; let halfExtents = null; let shape = null; if (typeof Ammo !== 'undefined') { switch(axis){ case 0: halfExtents = new Ammo.btVector3(height * 0.5, radius, radius); shape = new Ammo.btCylinderShapeX(halfExtents); break; case 1: halfExtents = new Ammo.btVector3(radius, height * 0.5, radius); shape = new Ammo.btCylinderShape(halfExtents); break; case 2: halfExtents = new Ammo.btVector3(radius, radius, height * 0.5); shape = new Ammo.btCylinderShapeZ(halfExtents); break; } } if (halfExtents) { Ammo.destroy(halfExtents); } return shape; } } class CollisionConeSystemImpl extends CollisionSystemImpl { createPhysicalShape(entity, data) { const axis = data.axis ?? 1; const radius = data.radius ?? 0.5; const height = data.height ?? 1; let shape = null; if (typeof Ammo !== 'undefined') { switch(axis){ case 0: shape = new Ammo.btConeShapeX(radius, height); break; case 1: shape = new Ammo.btConeShape(radius, height); break; case 2: shape = new Ammo.btConeShapeZ(radius, height); break; } } return shape; } } class CollisionMeshSystemImpl extends CollisionSystemImpl { beforeInitialize(component, data) {} createAmmoHull(mesh, node, shape, scale) { const hull = new Ammo.btConvexHullShape(); const point = new Ammo.btVector3(); const positions = []; mesh.getPositions(positions); for(let i = 0; i < positions.length; i += 3){ point.setValue(positions[i] * scale.x, positions[i + 1] * scale.y, positions[i + 2] * scale.z); hull.addPoint(point, false); } Ammo.destroy(point); hull.recalcLocalAabb(); hull.setMargin(0.01); const transform = this.system._getNodeTransform(node); shape.addChildShape(transform, hull); Ammo.destroy(transform); } createAmmoMesh(mesh, node, shape, scale, checkDupes = true) { const system = this.system; let triMesh; if (system._triMeshCache[mesh.id]) { triMesh = system._triMeshCache[mesh.id]; } else { const vb = mesh.vertexBuffer; const format = vb.getFormat(); let stride, positions; for(let i = 0; i < format.elements.length; i++){ const element = format.elements[i]; if (element.name === SEMANTIC_POSITION) { positions = new Float32Array(vb.lock(), element.offset); stride = element.stride / 4; break; } } const indices = []; mesh.getIndices(indices); const numTriangles = mesh.primitive[0].count / 3; const v1 = new Ammo.btVector3(); let i1, i2, i3; const base = mesh.primitive[0].base; triMesh = new Ammo.btTriangleMesh(); system._triMeshCache[mesh.id] = triMesh; const vertexCache = new Map(); const indexedArray = triMesh.getIndexedMeshArray(); indexedArray.at(0).m_numTriangles = numTriangles; const sx = scale ? scale.x : 1; const sy = scale ? scale.y : 1; const sz = scale ? scale.z : 1; const addVertex = (index)=>{ const x = positions[index * stride] * sx; const y = positions[index * stride + 1] * sy; const z = positions[index * stride + 2] * sz; let idx; if (checkDupes) { const str = `${x}:${y}:${z}`; idx = vertexCache.get(str); if (idx !== undefined) { return idx; } v1.setValue(x, y, z); idx = triMesh.findOrAddVertex(v1, false); vertexCache.set(str, idx); } else { v1.setValue(x, y, z); idx = triMesh.findOrAddVertex(v1, false); } return idx; }; for(let i = 0; i < numTriangles; i++){ i1 = addVertex(indices[base + i * 3]); i2 = addVertex(indices[base + i * 3 + 1]); i3 = addVertex(indices[base + i * 3 + 2]); triMesh.addIndex(i1); triMesh.addIndex(i2); triMesh.addIndex(i3); } Ammo.destroy(v1); } const triMeshShape = new Ammo.btBvhTriangleMeshShape(triMesh, true); if (!scale) { const scaling = system._getNodeScaling(node); triMeshShape.setLocalScaling(scaling); Ammo.destroy(scaling); } const transform = system._getNodeTransform(node); shape.addChildShape(transform, triMeshShape); Ammo.destroy(transform); } createPhysicalShape(entity, data) { if (typeof Ammo === 'undefined') return undefined; if (data.model || data.render) { const shape = new Ammo.btCompoundShape(); const entityTransform = entity.getWorldTransform(); const scale = entityTransform.getScale(); if (data.render) { const meshes = data.render.meshes; for(let i = 0; i < meshes.length; i++){ if (data.convexHull) { this.createAmmoHull(meshes[i], tempGraphNode, shape, scale); } else { this.createAmmoMesh(meshes[i], tempGraphNode, shape, scale, data.checkVertexDuplicates); } } } else if (data.model) { const meshInstances = data.model.meshInstances; for(let i = 0; i < meshInstances.length; i++){ this.createAmmoMesh(meshInstances[i].mesh, meshInstances[i].node, shape, null, data.checkVertexDuplicates); } const vec = new Ammo.btVector3(scale.x, scale.y, scale.z); shape.setLocalScaling(vec); Ammo.destroy(vec); } return shape; } return undefined; } recreatePhysicalShapes(component) { const data = component.data; if (data.renderAsset || data.asset) { if (component.enabled && component.entity.enabled) { this.loadAsset(component, data.renderAsset || data.asset, data.renderAsset ? 'render' : 'model'); return; } } this.doRecreatePhysicalShape(component); } loadAsset(component, id, property) { const data = component.data; const assets = this.system.app.assets; const previousPropertyValue = data[property]; const onAssetFullyReady = (asset)=>{ if (data[property] !== previousPropertyValue) { return; } data[property] = asset.resource; this.doRecreatePhysicalShape(component); }; const loadAndHandleAsset = (asset)=>{ asset.ready((asset)=>{ if (asset.data.containerAsset) { const containerAsset = assets.get(asset.data.containerAsset); if (containerAsset.loaded) { onAssetFullyReady(asset); } else { containerAsset.ready(()=>{ onAssetFullyReady(asset); }); assets.load(containerAsset); } } else { onAssetFullyReady(asset); } }); assets.load(asset); }; const asset = assets.get(id); if (asset) { loadAndHandleAsset(asset); } else { assets.once(`add:${id}`, loadAndHandleAsset); } } doRecreatePhysicalShape(component) { const entity = component.entity; const data = component.data; if (data.model || data.render) { this.destroyShape(data); data.shape = this.createPhysicalShape(entity, data); if (entity.rigidbody) { entity.rigidbody.disableSimulation(); entity.rigidbody.createBody(); if (entity.enabled && entity.rigidbody.enabled) { entity.rigidbody.enableSimulation(); } } else { if (!entity.trigger) { entity.trigger = new Trigger(this.system.app, component, data); } else { entity.trigger.initialize(data); } } } else { this.beforeRemove(entity, component); this.remove(entity, data); } } updateTransform(component, position, rotation, scale) { if (component.shape) { const entityTransform = component.entity.getWorldTransform(); const worldScale = entityTransform.getScale(); const previousScale = component.shape.getLocalScaling(); if (worldScale.x !== previousScale.x() || worldScale.y !== previousScale.y() || worldScale.z !== previousScale.z()) { this.doRecreatePhysicalShape(component); } } super.updateTransform(component, position, rotation, scale); } destroyShape(data) { if (!data.shape) { return; } const numShapes = data.shape.getNumChildShapes(); for(let i = 0; i < numShapes; i++){ const shape = data.shape.getChildShape(i); Ammo.destroy(shape); } Ammo.destroy(data.shape); data.shape = null; } } class CollisionCompoundSystemImpl extends CollisionSystemImpl { createPhysicalShape(entity, data) { if (typeof Ammo !== 'undefined') { return new Ammo.btCompoundShape(); } return undefined; } _addEachDescendant(entity) { if (!entity.collision || entity.rigidbody) { return; } entity.collision._compoundParent = this; if (entity !== this.entity) { entity.collision.system.recreatePhysicalShapes(entity.collision); } } _updateEachDescendant(entity) { if (!entity.collision) { return; } if (entity.collision._compoundParent !== this) { return; } entity.collision._compoundParent = null; if (entity !== this.entity && !entity.rigidbody) { entity.collision.system.recreatePhysicalShapes(entity.collision); } } _updateEachDescendantTransform(entity) { if (!entity.collision || entity.collision._compoundParent !== this.collision._compoundParent) { return; } this.collision.system.updateCompoundChildTransform(entity, false); } } class CollisionComponentSystem extends ComponentSystem { initializeComponentData(component, _data, properties) { properties = [ 'type', 'halfExtents', 'radius', 'axis', 'height', 'convexHull', 'shape', 'model', 'asset', 'render', 'renderAsset', 'enabled', 'linearOffset', 'angularOffset', 'checkVertexDuplicates' ]; const data = {}; for(let i = 0, len = properties.length; i < len; i++){ const property = properties[i]; data[property] = _data[property]; } let idx; if (_data.hasOwnProperty('asset')) { idx = properties.indexOf('model'); if (idx !== -1) { properties.splice(idx, 1); } idx = properties.indexOf('render'); if (idx !== -1) { properties.splice(idx, 1); } } else if (_data.hasOwnProperty('model')) { idx = properties.indexOf('asset'); if (idx !== -1) { properties.splice(idx, 1); } } if (!data.type) { data.type = component.data.type; } component.data.type = data.type; if (Array.isArray(data.halfExtents)) { data.halfExtents = new Vec3(data.halfExtents); } if (Array.isArray(data.linearOffset)) { data.linearOffset = new Vec3(data.linearOffset); } if (Array.isArray(data.angularOffset)) { const values = data.angularOffset; if (values.length === 3) { data.angularOffset = new Quat().setFromEulerAngles(values[0], values[1], values[2]); } else { data.angularOffset = new Quat(data.angularOffset); } } const impl = this._createImplementation(data.type); impl.beforeInitialize(component, data); super.initializeComponentData(component, data, properties); impl.afterInitialize(component, data); } _createImplementation(type) { if (this.implementations[type] === undefined) { let impl; switch(type){ case 'box': impl = new CollisionBoxSystemImpl(this); break; case 'sphere': impl = new CollisionSphereSystemImpl(this); break; case 'capsule': impl = new CollisionCapsuleSystemImpl(this); break; case 'cylinder': impl = new CollisionCylinderSystemImpl(this); break; case 'cone': impl = new CollisionConeSystemImpl(this); break; case 'mesh': impl = new CollisionMeshSystemImpl(this); break; case 'compound': impl = new CollisionCompoundSystemImpl(this); break; } this.implementations[type] = impl; } return this.implementations[type]; } _getImplementation(entity) { return this.implementations[entity.collision.data.type]; } cloneComponent(entity, clone) { return this._getImplementation(entity).clone(entity, clone); } onBeforeRemove(entity, component) { this.implementations[component.data.type].beforeRemove(entity, component); component.onBeforeRemove(); } onRemove(entity, data) { this.implementations[data.type].remove(entity, data); } updateCompoundChildTransform(entity, forceUpdate) { const parentComponent = entity.collision._compoundParent; if (parentComponent === entity.collision) return; if (entity.enabled && entity.collision.enabled && (entity._dirtyLocal || forceUpdate)) { const transform = this._getNodeTransform(entity, parentComponent.entity); const idx = parentComponent.getCompoundChildShapeIndex(entity.collision.shape); if (idx === null) { parentComponent.shape.addChildShape(transform, entity.collision.data.shape); } else { parentComponent.shape.updateChildTransform(idx, transform, true); } Ammo.destroy(transform); } } _removeCompoundChild(collision, shape) { if (collision.shape.getNumChildShapes() === 0) { return; } if (collision.shape.removeChildShape) { collision.shape.removeChildShape(shape); } else { const ind = collision.getCompoundChildShapeIndex(shape); if (ind !== null) { collision.shape.removeChildShapeByIndex(ind); } } } onTransformChanged(component, position, rotation, scale) { this.implementations[component.data.type].updateTransform(component, position, rotation, scale); } changeType(component, previousType, newType) { this.implementations[previousType].beforeRemove(component.entity, component); this.implementations[previousType].remove(component.entity, component.data); this._createImplementation(newType).reset(component, component.data); } recreatePhysicalShapes(component) { this.implementations[component.data.type].recreatePhysicalShapes(component); } _calculateNodeRelativeTransform(node, relative) { if (node === relative) { const scale = node.getWorldTransform().getScale(); mat4.setScale(scale.x, scale.y, scale.z); } else { this._calculateNodeRelativeTransform(node.parent, relative); mat4.mul(node.getLocalTransform()); } } _getNodeScaling(node) { const wtm = node.getWorldTransform(); const scl = wtm.getScale(); return new Ammo.btVector3(scl.x, scl.y, scl.z); } _getNodeTransform(node, relative) { let pos, rot; if (relative) { this._calculateNodeRelativeTransform(node, relative); pos = p1; rot = quat$1; mat4.getTranslation(pos); rot.setFromMat4(mat4); } else { pos = node.getPosition(); rot = node.getRotation(); } const ammoQuat = new Ammo.btQuaternion(); const transform = new Ammo.btTransform(); transform.setIdentity(); const origin = transform.getOrigin(); const component = node.collision; if (component && component._hasOffset) { const lo = component.data.linearOffset; const ao = component.data.angularOffset; const newOrigin = p2; quat$1.copy(rot).transformVector(lo, newOrigin); newOrigin.add(pos); quat$1.copy(rot).mul(ao); origin.setValue(newOrigin.x, newOrigin.y, newOrigin.z); ammoQuat.setValue(quat$1.x, quat$1.y, quat$1.z, quat$1.w); } else { origin.setValue(pos.x, pos.y, pos.z); ammoQuat.setValue(rot.x, rot.y, rot.z, rot.w); } transform.setRotation(ammoQuat); Ammo.destroy(ammoQuat); return transform; } destroy() { for(const key in this._triMeshCache){ Ammo.destroy(this._triMeshCache[key]); } this._triMeshCache = null; super.destroy(); } constructor(app){ super(app); this.id = 'collision'; this.ComponentType = CollisionComponent; this.DataType = CollisionComponentData; this.schema = _schema$g; this.implementations = {}; this._triMeshCache = {}; this.on('beforeremove', this.onBeforeRemove, this); this.on('remove', this.onRemove, this); } } const _tempColor$1 = new Color(); const _vertexFormatDeviceCache = new DeviceCache(); class ImageRenderable { destroy() { this.setMaterial(null); this._element.removeModelFromLayers(this.model); this.model.destroy(); this.model = null; this.node = null; this.mesh = null; this.meshInstance?.destroy(); this.meshInstance = null; this.unmaskMeshInstance?.destroy(); this.unmaskMeshInstance = null; this._entity = null; this._element = null; } setMesh(mesh) { if (!this.meshInstance) return; this.mesh = mesh; this.meshInstance.mesh = mesh; this.meshInstance.visible = !!mesh; if (this.unmaskMeshInstance) { this.unmaskMeshInstance.mesh = mesh; } this.forceUpdateAabb(); } setMask(mask) { if (!this.meshInstance) return; if (this._entity.enabled && this._element.enabled) { this._element.removeModelFromLayers(this.model); } if (mask) { this.unmaskMeshInstance = new MeshInstance(this.mesh, this.meshInstance.material, this.node); this.unmaskMeshInstance.name = `Unmask: ${this._entity.name}`; this.unmaskMeshInstance.castShadow = false; this.unmaskMeshInstance.receiveShadow = false; this.unmaskMeshInstance.pick = false; this.model.meshInstances.push(this.unmaskMeshInstance); for(const name in this.meshInstance.parameters){ this.unmaskMeshInstance.setParameter(name, this.meshInstance.parameters[name].data); } } else { const idx = this.model.meshInstances.indexOf(this.unmaskMeshInstance); if (idx >= 0) { this.model.meshInstances.splice(idx, 1); } } if (this._entity.enabled && this._element.enabled) { this._element.addModelToLayers(this.model); } if (!mask) { this.unmaskMeshInstance?.destroy(); this.unmaskMeshInstance = null; } } setMaterial(material) { if (!this.meshInstance) return; this.meshInstance.material = material; if (this.unmaskMeshInstance) { this.unmaskMeshInstance.material = material; } } setParameter(name, value) { if (!this.meshInstance) return; this.meshInstance.setParameter(name, value); if (this.unmaskMeshInstance) { this.unmaskMeshInstance.setParameter(name, value); } } deleteParameter(name) { if (!this.meshInstance) return; this.meshInstance.deleteParameter(name); if (this.unmaskMeshInstance) { this.unmaskMeshInstance.deleteParameter(name); } } setUnmaskDrawOrder() { if (!this.meshInstance) return; const getLastChild = function(e) { let last; const c = e.children; const l = c.length; if (l) { for(let i = 0; i < l; i++){ if (c[i].element) { last = c[i]; } } if (!last) return null; const child = getLastChild(last); if (child) { return child; } return last; } return null; }; if (this.unmaskMeshInstance) { const lastChild = getLastChild(this._entity); if (lastChild && lastChild.element) { this.unmaskMeshInstance.drawOrder = lastChild.element.drawOrder + lastChild.element.getMaskOffset(); } else { this.unmaskMeshInstance.drawOrder = this.meshInstance.drawOrder + this._element.getMaskOffset(); } } } setDrawOrder(drawOrder) { if (!this.meshInstance) { return; } this.meshInstance.drawOrder = drawOrder; } setCull(cull) { if (!this.meshInstance) return; const element = this._element; let visibleFn = null; if (cull && element._isScreenSpace()) { visibleFn = function(camera) { return element.isVisibleForCamera(camera); }; } this.meshInstance.cull = cull; this.meshInstance.isVisibleFunc = visibleFn; if (this.unmaskMeshInstance) { this.unmaskMeshInstance.cull = cull; this.unmaskMeshInstance.isVisibleFunc = visibleFn; } } setScreenSpace(screenSpace) { if (!this.meshInstance) return; this.meshInstance.screenSpace = screenSpace; if (this.unmaskMeshInstance) { this.unmaskMeshInstance.screenSpace = screenSpace; } } setLayer(layer) { if (!this.meshInstance) return; this.meshInstance.layer = layer; if (this.unmaskMeshInstance) { this.unmaskMeshInstance.layer = layer; } } forceUpdateAabb(mask) { if (!this.meshInstance) return; this.meshInstance._aabbVer = -1; if (this.unmaskMeshInstance) { this.unmaskMeshInstance._aabbVer = -1; } } setAabbFunc(fn) { if (!this.meshInstance) return; this.meshInstance._updateAabbFunc = fn; if (this.unmaskMeshInstance) { this.unmaskMeshInstance._updateAabbFunc = fn; } } constructor(entity, mesh, material){ this._entity = entity; this._element = entity.element; this.model = new Model(); this.node = new GraphNode(); this.model.graph = this.node; this.mesh = mesh; this.meshInstance = new MeshInstance(this.mesh, material, this.node); this.meshInstance.name = `ImageElement: ${entity.name}`; this.meshInstance.castShadow = false; this.meshInstance.receiveShadow = false; this._meshDirty = false; this.model.meshInstances.push(this.meshInstance); this._entity.addChild(this.model.graph); this.model._entity = this._entity; this.unmaskMeshInstance = null; } } class ImageElement { destroy() { this.textureAsset = null; this.spriteAsset = null; this.materialAsset = null; this._renderable.setMesh(this._defaultMesh); this._renderable.destroy(); this._defaultMesh = null; this._element.off('resize', this._onParentResizeOrPivotChange, this); this._element.off('set:pivot', this._onParentResizeOrPivotChange, this); this._element.off('screen:set:screenspace', this._onScreenSpaceChange, this); this._element.off('set:screen', this._onScreenChange, this); this._element.off('set:draworder', this._onDrawOrderChange, this); this._element.off('screen:set:resolution', this._onResolutionChange, this); } _onResolutionChange(res) {} _onParentResizeOrPivotChange() { if (this._renderable.mesh) { this._updateMesh(this._renderable.mesh); } } _onScreenSpaceChange(value) { this._updateMaterial(value); } _onScreenChange(screen, previous) { if (screen) { this._updateMaterial(screen.screen.screenSpace); } else { this._updateMaterial(false); } } _onDrawOrderChange(order) { this._renderable.setDrawOrder(order); if (this.mask && this._element.screen) { this._element.screen.screen.once('syncdraworder', function() { this._renderable.setUnmaskDrawOrder(); }, this); } } _hasUserMaterial() { return !!this._materialAsset || !!this._material && this._system.defaultImageMaterials.indexOf(this._material) === -1; } _use9Slicing() { return this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === SPRITE_RENDERMODE_TILED); } _updateMaterial(screenSpace) { const mask = !!this._mask; const nineSliced = !!(this.sprite && this.sprite.renderMode === SPRITE_RENDERMODE_SLICED); const nineTiled = !!(this.sprite && this.sprite.renderMode === SPRITE_RENDERMODE_TILED); if (!this._hasUserMaterial()) { this._material = this._system.getImageElementMaterial(screenSpace, mask, nineSliced, nineTiled); } if (this._renderable) { this._renderable.setCull(!this._element._isScreenSpace() || this._element._isScreenCulled()); this._renderable.setMaterial(this._material); this._renderable.setScreenSpace(screenSpace); this._renderable.setLayer(screenSpace ? LAYER_HUD : LAYER_WORLD); } } _createMesh() { const element = this._element; const w = element.calculatedWidth; const h = element.calculatedHeight; const r = this._rect; const device = this._system.app.graphicsDevice; const vertexData = new Float32Array([ w, 0, 0, 0, 0, 1, r.x + r.z, 1.0 - r.y, w, h, 0, 0, 0, 1, r.x + r.z, 1.0 - (r.y + r.w), 0, 0, 0, 0, 0, 1, r.x, 1.0 - r.y, 0, h, 0, 0, 0, 1, r.x, 1.0 - (r.y + r.w) ]); const vertexFormat = _vertexFormatDeviceCache.get(device, ()=>{ return new VertexFormat(device, [ { semantic: SEMANTIC_POSITION, components: 3, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_NORMAL, components: 3, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_TEXCOORD0, components: 2, type: TYPE_FLOAT32 } ]); }); const vertexBuffer = new VertexBuffer(device, vertexFormat, 4, { data: vertexData.buffer }); const mesh = new Mesh(device); mesh.vertexBuffer = vertexBuffer; mesh.primitive[0].type = PRIMITIVE_TRISTRIP; mesh.primitive[0].base = 0; mesh.primitive[0].count = 4; mesh.primitive[0].indexed = false; mesh.aabb.setMinMax(Vec3.ZERO, new Vec3(w, h, 0)); this._updateMesh(mesh); return mesh; } _updateMesh(mesh) { const element = this._element; let w = element.calculatedWidth; let h = element.calculatedHeight; if (element.fitMode !== FITMODE_STRETCH && this._targetAspectRatio > 0) { const actualRatio = element.calculatedWidth / element.calculatedHeight; if (element.fitMode === FITMODE_CONTAIN && actualRatio > this._targetAspectRatio || element.fitMode === FITMODE_COVER && actualRatio < this._targetAspectRatio) { w = element.calculatedHeight * this._targetAspectRatio; } else { h = element.calculatedWidth / this._targetAspectRatio; } } const screenSpace = element._isScreenSpace(); this._updateMaterial(screenSpace); if (this._renderable) this._renderable.forceUpdateAabb(); if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === SPRITE_RENDERMODE_TILED)) { const frameData = this._sprite.atlas.frames[this._sprite.frameKeys[this._spriteFrame]]; const borderWidthScale = 2 / frameData.rect.z; const borderHeightScale = 2 / frameData.rect.w; this._innerOffset.set(frameData.border.x * borderWidthScale, frameData.border.y * borderHeightScale, frameData.border.z * borderWidthScale, frameData.border.w * borderHeightScale); const tex = this.sprite.atlas.texture; this._atlasRect.set(frameData.rect.x / tex.width, frameData.rect.y / tex.height, frameData.rect.z / tex.width, frameData.rect.w / tex.height); const ppu = this._pixelsPerUnit !== null ? this._pixelsPerUnit : this.sprite.pixelsPerUnit; const scaleMulX = frameData.rect.z / ppu; const scaleMulY = frameData.rect.w / ppu; this._outerScale.set(Math.max(w, this._innerOffset.x * scaleMulX), Math.max(h, this._innerOffset.y * scaleMulY)); let scaleX = scaleMulX; let scaleY = scaleMulY; this._outerScale.x /= scaleMulX; this._outerScale.y /= scaleMulY; scaleX *= math.clamp(w / (this._innerOffset.x * scaleMulX), 0.0001, 1); scaleY *= math.clamp(h / (this._innerOffset.y * scaleMulY), 0.0001, 1); if (this._renderable) { this._innerOffsetUniform[0] = this._innerOffset.x; this._innerOffsetUniform[1] = this._innerOffset.y; this._innerOffsetUniform[2] = this._innerOffset.z; this._innerOffsetUniform[3] = this._innerOffset.w; this._renderable.setParameter('innerOffset', this._innerOffsetUniform); this._atlasRectUniform[0] = this._atlasRect.x; this._atlasRectUniform[1] = this._atlasRect.y; this._atlasRectUniform[2] = this._atlasRect.z; this._atlasRectUniform[3] = this._atlasRect.w; this._renderable.setParameter('atlasRect', this._atlasRectUniform); this._outerScaleUniform[0] = this._outerScale.x; this._outerScaleUniform[1] = this._outerScale.y; this._renderable.setParameter('outerScale', this._outerScaleUniform); this._renderable.setAabbFunc(this._updateAabbFunc); this._renderable.node.setLocalScale(scaleX, scaleY, 1); this._renderable.node.setLocalPosition((0.5 - element.pivot.x) * w, (0.5 - element.pivot.y) * h, 0); } } else { const vb = mesh.vertexBuffer; const vertexDataF32 = new Float32Array(vb.lock()); const hp = element.pivot.x; const vp = element.pivot.y; vertexDataF32[0] = w - hp * w; vertexDataF32[1] = 0 - vp * h; vertexDataF32[8] = w - hp * w; vertexDataF32[9] = h - vp * h; vertexDataF32[16] = 0 - hp * w; vertexDataF32[17] = 0 - vp * h; vertexDataF32[24] = 0 - hp * w; vertexDataF32[25] = h - vp * h; let atlasTextureWidth = 1; let atlasTextureHeight = 1; let rect = this._rect; if (this._sprite && this._sprite.frameKeys[this._spriteFrame] && this._sprite.atlas) { const frame = this._sprite.atlas.frames[this._sprite.frameKeys[this._spriteFrame]]; if (frame) { rect = frame.rect; atlasTextureWidth = this._sprite.atlas.texture.width; atlasTextureHeight = this._sprite.atlas.texture.height; } } vertexDataF32[6] = (rect.x + rect.z) / atlasTextureWidth; vertexDataF32[7] = 1.0 - rect.y / atlasTextureHeight; vertexDataF32[14] = (rect.x + rect.z) / atlasTextureWidth; vertexDataF32[15] = 1.0 - (rect.y + rect.w) / atlasTextureHeight; vertexDataF32[22] = rect.x / atlasTextureWidth; vertexDataF32[23] = 1.0 - rect.y / atlasTextureHeight; vertexDataF32[30] = rect.x / atlasTextureWidth; vertexDataF32[31] = 1.0 - (rect.y + rect.w) / atlasTextureHeight; vb.unlock(); const min = new Vec3(0 - hp * w, 0 - vp * h, 0); const max = new Vec3(w - hp * w, h - vp * h, 0); mesh.aabb.setMinMax(min, max); if (this._renderable) { this._renderable.node.setLocalScale(1, 1, 1); this._renderable.node.setLocalPosition(0, 0, 0); this._renderable.setAabbFunc(null); } } this._meshDirty = false; } _updateSprite() { let nineSlice = false; let mesh = null; this._targetAspectRatio = -1; if (this._sprite && this._sprite.atlas) { mesh = this._sprite.meshes[this.spriteFrame]; nineSlice = this._sprite.renderMode === SPRITE_RENDERMODE_SLICED || this._sprite.renderMode === SPRITE_RENDERMODE_TILED; const frameData = this._sprite.atlas.frames[this._sprite.frameKeys[this._spriteFrame]]; if (frameData?.rect.w > 0) { this._targetAspectRatio = frameData.rect.z / frameData.rect.w; } } this.mesh = nineSlice ? mesh : this._defaultMesh; this.refreshMesh(); } refreshMesh() { if (this.mesh) { if (!this._element._beingInitialized) { this._updateMesh(this.mesh); } else { this._meshDirty = true; } } } _updateAabb(aabb) { aabb.center.set(0, 0, 0); aabb.halfExtents.set(this._outerScale.x * 0.5, this._outerScale.y * 0.5, 0.001); aabb.setFromTransformedAabb(aabb, this._renderable.node.getWorldTransform()); return aabb; } _toggleMask() { this._element._dirtifyMask(); const screenSpace = this._element._isScreenSpace(); this._updateMaterial(screenSpace); this._renderable.setMask(!!this._mask); } _onMaterialLoad(asset) { this.material = asset.resource; } _onMaterialAdded(asset) { this._system.app.assets.off(`add:${asset.id}`, this._onMaterialAdded, this); if (this._materialAsset === asset.id) { this._bindMaterialAsset(asset); } } _bindMaterialAsset(asset) { if (!this._entity.enabled) return; asset.on('load', this._onMaterialLoad, this); asset.on('change', this._onMaterialChange, this); asset.on('remove', this._onMaterialRemove, this); if (asset.resource) { this._onMaterialLoad(asset); } else { this._system.app.assets.load(asset); } } _unbindMaterialAsset(asset) { asset.off('load', this._onMaterialLoad, this); asset.off('change', this._onMaterialChange, this); asset.off('remove', this._onMaterialRemove, this); } _onMaterialChange() {} _onMaterialRemove() {} _onTextureAdded(asset) { this._system.app.assets.off(`add:${asset.id}`, this._onTextureAdded, this); if (this._textureAsset === asset.id) { this._bindTextureAsset(asset); } } _bindTextureAsset(asset) { if (!this._entity.enabled) return; asset.on('load', this._onTextureLoad, this); asset.on('change', this._onTextureChange, this); asset.on('remove', this._onTextureRemove, this); if (asset.resource) { this._onTextureLoad(asset); } else { this._system.app.assets.load(asset); } } _unbindTextureAsset(asset) { asset.off('load', this._onTextureLoad, this); asset.off('change', this._onTextureChange, this); asset.off('remove', this._onTextureRemove, this); } _onTextureLoad(asset) { this.texture = asset.resource; } _onTextureChange(asset) {} _onTextureRemove(asset) {} _onSpriteAssetAdded(asset) { this._system.app.assets.off(`add:${asset.id}`, this._onSpriteAssetAdded, this); if (this._spriteAsset === asset.id) { this._bindSpriteAsset(asset); } } _bindSpriteAsset(asset) { if (!this._entity.enabled) return; asset.on('load', this._onSpriteAssetLoad, this); asset.on('change', this._onSpriteAssetChange, this); asset.on('remove', this._onSpriteAssetRemove, this); if (asset.resource) { this._onSpriteAssetLoad(asset); } else { this._system.app.assets.load(asset); } } _unbindSpriteAsset(asset) { asset.off('load', this._onSpriteAssetLoad, this); asset.off('change', this._onSpriteAssetChange, this); asset.off('remove', this._onSpriteAssetRemove, this); if (asset.data.textureAtlasAsset) { this._system.app.assets.off(`load:${asset.data.textureAtlasAsset}`, this._onTextureAtlasLoad, this); } } _onSpriteAssetLoad(asset) { if (!asset || !asset.resource) { this.sprite = null; } else { if (!asset.resource.atlas) { const atlasAssetId = asset.data.textureAtlasAsset; if (atlasAssetId) { const assets = this._system.app.assets; assets.off(`load:${atlasAssetId}`, this._onTextureAtlasLoad, this); assets.once(`load:${atlasAssetId}`, this._onTextureAtlasLoad, this); } } else { this.sprite = asset.resource; } } } _onSpriteAssetChange(asset) { this._onSpriteAssetLoad(asset); } _onSpriteAssetRemove(asset) {} _bindSprite(sprite) { this._evtSetMeshes = sprite.on('set:meshes', this._onSpriteMeshesChange, this); sprite.on('set:pixelsPerUnit', this._onSpritePpuChange, this); sprite.on('set:atlas', this._onAtlasTextureChange, this); if (sprite.atlas) { sprite.atlas.on('set:texture', this._onAtlasTextureChange, this); } } _unbindSprite(sprite) { this._evtSetMeshes?.off(); this._evtSetMeshes = null; sprite.off('set:pixelsPerUnit', this._onSpritePpuChange, this); sprite.off('set:atlas', this._onAtlasTextureChange, this); if (sprite.atlas) { sprite.atlas.off('set:texture', this._onAtlasTextureChange, this); } } _onSpriteMeshesChange() { if (this._sprite) { this._spriteFrame = math.clamp(this._spriteFrame, 0, this._sprite.frameKeys.length - 1); } this._updateSprite(); } _onSpritePpuChange() { if (this.sprite.renderMode !== SPRITE_RENDERMODE_SIMPLE && this._pixelsPerUnit === null) { this._updateSprite(); } } _onAtlasTextureChange() { if (this.sprite && this.sprite.atlas && this.sprite.atlas.texture) { this._renderable.setParameter('texture_emissiveMap', this._sprite.atlas.texture); this._renderable.setParameter('texture_opacityMap', this._sprite.atlas.texture); } else { this._renderable.deleteParameter('texture_emissiveMap'); this._renderable.deleteParameter('texture_opacityMap'); } } _onTextureAtlasLoad(atlasAsset) { const spriteAsset = this._spriteAsset; if (spriteAsset instanceof Asset) { this._onSpriteAssetLoad(spriteAsset); } else { this._onSpriteAssetLoad(this._system.app.assets.get(spriteAsset)); } } onEnable() { if (this._materialAsset) { const asset = this._system.app.assets.get(this._materialAsset); if (asset && asset.resource !== this._material) { this._bindMaterialAsset(asset); } } if (this._textureAsset) { const asset = this._system.app.assets.get(this._textureAsset); if (asset && asset.resource !== this._texture) { this._bindTextureAsset(asset); } } if (this._spriteAsset) { const asset = this._system.app.assets.get(this._spriteAsset); if (asset && asset.resource !== this._sprite) { this._bindSpriteAsset(asset); } } this._element.addModelToLayers(this._renderable.model); } onDisable() { this._element.removeModelFromLayers(this._renderable.model); } _setStencil(stencilParams) { this._renderable.meshInstance.stencilFront = stencilParams; this._renderable.meshInstance.stencilBack = stencilParams; let ref = 0; if (this._element.maskedBy) { ref = this._element.maskedBy.element._image._maskRef; } if (this._renderable.unmaskMeshInstance) { const sp = new StencilParameters({ ref: ref + 1, func: FUNC_EQUAL, zpass: STENCILOP_DECREMENT }); this._renderable.unmaskMeshInstance.stencilFront = sp; this._renderable.unmaskMeshInstance.stencilBack = sp; } } _updateRenderableEmissive() { _tempColor$1.linear(this._color); this._colorUniform[0] = _tempColor$1.r; this._colorUniform[1] = _tempColor$1.g; this._colorUniform[2] = _tempColor$1.b; this._renderable.setParameter('material_emissive', this._colorUniform); } set color(value) { const { r, g, b } = value; if (this._color.r !== r || this._color.g !== g || this._color.b !== b) { this._color.r = r; this._color.g = g; this._color.b = b; this._updateRenderableEmissive(); } if (this._element) { this._element.fire('set:color', this._color); } } get color() { return this._color; } set opacity(value) { if (value !== this._color.a) { this._color.a = value; this._renderable.setParameter('material_opacity', value); } if (this._element) { this._element.fire('set:opacity', value); } } get opacity() { return this._color.a; } set rect(value) { let x, y, z, w; if (value instanceof Vec4) { x = value.x; y = value.y; z = value.z; w = value.w; } else { x = value[0]; y = value[1]; z = value[2]; w = value[3]; } if (x === this._rect.x && y === this._rect.y && z === this._rect.z && w === this._rect.w) { return; } this._rect.set(x, y, z, w); if (this._renderable.mesh) { if (!this._element._beingInitialized) { this._updateMesh(this._renderable.mesh); } else { this._meshDirty = true; } } } get rect() { return this._rect; } _removeMaterialAssetEvents() { if (this._materialAsset) { const assets = this._system.app.assets; assets.off(`add:${this._materialAsset}`, this._onMaterialAdded, this); const asset = assets.get(this._materialAsset); if (asset) { asset.off('load', this._onMaterialLoad, this); asset.off('change', this._onMaterialChange, this); asset.off('remove', this._onMaterialRemove, this); } } } set material(value) { if (this._material === value) return; if (!value) { const screenSpace = this._element._isScreenSpace(); if (this.mask) { value = screenSpace ? this._system.defaultScreenSpaceImageMaskMaterial : this._system.defaultImageMaskMaterial; } else { value = screenSpace ? this._system.defaultScreenSpaceImageMaterial : this._system.defaultImageMaterial; } } this._material = value; if (this._materialAsset) { const asset = this._system.app.assets.get(this._materialAsset); if (!asset || asset.resource !== value) { this._removeMaterialAssetEvents(); this._materialAsset = null; } } if (value) { this._renderable.setMaterial(value); if (this._hasUserMaterial()) { this._renderable.deleteParameter('material_opacity'); this._renderable.deleteParameter('material_emissive'); } else { this._updateRenderableEmissive(); this._renderable.setParameter('material_opacity', this._color.a); } } } get material() { return this._material; } set materialAsset(value) { const assets = this._system.app.assets; let _id = value; if (value instanceof Asset) { _id = value.id; } if (this._materialAsset !== _id) { this._removeMaterialAssetEvents(); this._materialAsset = _id; if (this._materialAsset) { const asset = assets.get(this._materialAsset); if (!asset) { this._materialAsset = null; this.material = null; this._materialAsset = _id; assets.on(`add:${this._materialAsset}`, this._onMaterialAdded, this); } else { this._bindMaterialAsset(asset); } } else { this._materialAsset = null; this.material = null; this._materialAsset = _id; } } } get materialAsset() { return this._materialAsset; } set texture(value) { if (this._texture === value) return; if (this._textureAsset) { const textureAsset = this._system.app.assets.get(this._textureAsset); if (textureAsset && textureAsset.resource !== value) { this.textureAsset = null; } } this._texture = value; if (value) { if (this._spriteAsset) { this.spriteAsset = null; } this._renderable.setParameter('texture_emissiveMap', this._texture); this._renderable.setParameter('texture_opacityMap', this._texture); this._updateRenderableEmissive(); this._renderable.setParameter('material_opacity', this._color.a); const newAspectRatio = this._texture.width / this._texture.height; if (newAspectRatio !== this._targetAspectRatio) { this._targetAspectRatio = newAspectRatio; if (this._element.fitMode !== FITMODE_STRETCH) { this.refreshMesh(); } } } else { this._renderable.deleteParameter('texture_emissiveMap'); this._renderable.deleteParameter('texture_opacityMap'); this._targetAspectRatio = -1; if (this._element.fitMode !== FITMODE_STRETCH) { this.refreshMesh(); } } } get texture() { return this._texture; } set textureAsset(value) { const assets = this._system.app.assets; let _id = value; if (value instanceof Asset) { _id = value.id; } if (this._textureAsset !== _id) { if (this._textureAsset) { assets.off(`add:${this._textureAsset}`, this._onTextureAdded, this); const _prev = assets.get(this._textureAsset); if (_prev) { _prev.off('load', this._onTextureLoad, this); _prev.off('change', this._onTextureChange, this); _prev.off('remove', this._onTextureRemove, this); } } this._textureAsset = _id; if (this._textureAsset) { const asset = assets.get(this._textureAsset); if (!asset) { this.texture = null; assets.on(`add:${this._textureAsset}`, this._onTextureAdded, this); } else { this._bindTextureAsset(asset); } } else { this.texture = null; } } } get textureAsset() { return this._textureAsset; } set spriteAsset(value) { const assets = this._system.app.assets; let _id = value; if (value instanceof Asset) { _id = value.id; } if (this._spriteAsset !== _id) { if (this._spriteAsset) { assets.off(`add:${this._spriteAsset}`, this._onSpriteAssetAdded, this); const _prev = assets.get(this._spriteAsset); if (_prev) { this._unbindSpriteAsset(_prev); } } this._spriteAsset = _id; if (this._spriteAsset) { const asset = assets.get(this._spriteAsset); if (!asset) { this.sprite = null; assets.on(`add:${this._spriteAsset}`, this._onSpriteAssetAdded, this); } else { this._bindSpriteAsset(asset); } } else { this.sprite = null; } } if (this._element) { this._element.fire('set:spriteAsset', _id); } } get spriteAsset() { return this._spriteAsset; } set sprite(value) { if (this._sprite === value) return; if (this._sprite) { this._unbindSprite(this._sprite); } if (this._spriteAsset) { const spriteAsset = this._system.app.assets.get(this._spriteAsset); if (spriteAsset && spriteAsset.resource !== value) { this.spriteAsset = null; } } this._sprite = value; if (this._sprite) { this._bindSprite(this._sprite); if (this._textureAsset) { this.textureAsset = null; } } if (this._sprite && this._sprite.atlas && this._sprite.atlas.texture) { this._renderable.setParameter('texture_emissiveMap', this._sprite.atlas.texture); this._renderable.setParameter('texture_opacityMap', this._sprite.atlas.texture); } else { this._renderable.deleteParameter('texture_emissiveMap'); this._renderable.deleteParameter('texture_opacityMap'); } if (this._sprite) { this._spriteFrame = math.clamp(this._spriteFrame, 0, this._sprite.frameKeys.length - 1); } this._updateSprite(); } get sprite() { return this._sprite; } set spriteFrame(value) { const oldValue = this._spriteFrame; if (this._sprite) { this._spriteFrame = math.clamp(value, 0, this._sprite.frameKeys.length - 1); } else { this._spriteFrame = value; } if (this._spriteFrame !== oldValue) { this._updateSprite(); } if (this._element) { this._element.fire('set:spriteFrame', value); } } get spriteFrame() { return this._spriteFrame; } set mesh(value) { this._renderable.setMesh(value); if (this._defaultMesh === value) { this._renderable.setAabbFunc(null); } else { this._renderable.setAabbFunc(this._updateAabbFunc); } } get mesh() { return this._renderable.mesh; } set mask(value) { if (this._mask !== value) { this._mask = value; this._toggleMask(); } } get mask() { return this._mask; } set pixelsPerUnit(value) { if (this._pixelsPerUnit === value) return; this._pixelsPerUnit = value; if (this._sprite && (this._sprite.renderMode === SPRITE_RENDERMODE_SLICED || this._sprite.renderMode === SPRITE_RENDERMODE_TILED)) { this._updateSprite(); } } get pixelsPerUnit() { return this._pixelsPerUnit; } get aabb() { if (this._renderable.meshInstance) { return this._renderable.meshInstance.aabb; } return null; } constructor(element){ this._evtSetMeshes = null; this._element = element; this._entity = element.entity; this._system = element.system; this._textureAsset = null; this._texture = null; this._materialAsset = null; this._material = null; this._spriteAsset = null; this._sprite = null; this._spriteFrame = 0; this._pixelsPerUnit = null; this._targetAspectRatio = -1; this._rect = new Vec4(0, 0, 1, 1); this._mask = false; this._maskRef = 0; this._outerScale = new Vec2(); this._outerScaleUniform = new Float32Array(2); this._innerOffset = new Vec4(); this._innerOffsetUniform = new Float32Array(4); this._atlasRect = new Vec4(); this._atlasRectUniform = new Float32Array(4); this._defaultMesh = this._createMesh(); this._renderable = new ImageRenderable(this._entity, this._defaultMesh, this._material); this._color = new Color(1, 1, 1, 1); this._colorUniform = new Float32Array([ 1, 1, 1 ]); this._updateRenderableEmissive(); this._renderable.setParameter('material_opacity', 1); this._updateAabbFunc = this._updateAabb.bind(this); this._onScreenChange(this._element.screen); this._element.on('resize', this._onParentResizeOrPivotChange, this); this._element.on('set:pivot', this._onParentResizeOrPivotChange, this); this._element.on('screen:set:screenspace', this._onScreenSpaceChange, this); this._element.on('set:screen', this._onScreenChange, this); this._element.on('set:draworder', this._onDrawOrderChange, this); this._element.on('screen:set:resolution', this._onResolutionChange, this); if (!element._beingInitialized && element.enabled && element.entity.enabled) { this.onEnable(); } } } class LocalizedAsset extends EventHandler { set defaultAsset(value) { const id = value instanceof Asset ? value.id : value; if (this._defaultAsset === id) return; if (this._defaultAsset) { this._unbindDefaultAsset(); } this._defaultAsset = id; if (this._defaultAsset) { this._bindDefaultAsset(); } this._onSetLocale(this._app.i18n.locale); } get defaultAsset() { return this._defaultAsset; } set localizedAsset(value) { const id = value instanceof Asset ? value.id : value; if (this._localizedAsset === id) { return; } if (this._localizedAsset) { this._app.assets.off(`add:${this._localizedAsset}`, this._onLocalizedAssetAdd, this); this._unbindLocalizedAsset(); } this._localizedAsset = id; if (this._localizedAsset) { const asset = this._app.assets.get(this._localizedAsset); if (!asset) { this._app.assets.once(`add:${this._localizedAsset}`, this._onLocalizedAssetAdd, this); } else { this._bindLocalizedAsset(); } } } get localizedAsset() { return this._localizedAsset; } set autoLoad(value) { if (this._autoLoad === value) return; this._autoLoad = value; if (this._autoLoad && this._localizedAsset) { this._unbindLocalizedAsset(); this._bindLocalizedAsset(); } } get autoLoad() { return this._autoLoad; } set disableLocalization(value) { if (this._disableLocalization === value) return; this._disableLocalization = value; this._onSetLocale(this._app.i18n.locale); } get disableLocalization() { return this._disableLocalization; } _bindDefaultAsset() { const asset = this._app.assets.get(this._defaultAsset); if (!asset) { this._app.assets.once(`add:${this._defaultAsset}`, this._onDefaultAssetAdd, this); } else { this._onDefaultAssetAdd(asset); } } _unbindDefaultAsset() { if (!this._defaultAsset) return; this._app.assets.off(`add:${this._defaultAsset}`, this._onDefaultAssetAdd, this); const asset = this._app.assets.get(this._defaultAsset); if (!asset) return; asset.off('add:localized', this._onLocaleAdd, this); asset.off('remove:localized', this._onLocaleRemove, this); asset.off('remove', this._onDefaultAssetRemove, this); } _onDefaultAssetAdd(asset) { if (this._defaultAsset !== asset.id) return; asset.on('add:localized', this._onLocaleAdd, this); asset.on('remove:localized', this._onLocaleRemove, this); asset.once('remove', this._onDefaultAssetRemove, this); } _onDefaultAssetRemove(asset) { if (this._defaultAsset !== asset.id) return; asset.off('add:localized', this._onLocaleAdd, this); asset.off('remove:localized', this._onLocaleAdd, this); this._app.assets.once(`add:${this._defaultAsset}`, this._onDefaultAssetAdd, this); } _bindLocalizedAsset() { if (!this._autoLoad) return; const asset = this._app.assets.get(this._localizedAsset); if (!asset) return; asset.on('load', this._onLocalizedAssetLoad, this); asset.on('change', this._onLocalizedAssetChange, this); asset.on('remove', this._onLocalizedAssetRemove, this); if (asset.resource) { this._onLocalizedAssetLoad(asset); } else { this._app.assets.load(asset); } } _unbindLocalizedAsset() { const asset = this._app.assets.get(this._localizedAsset); if (!asset) return; asset.off('load', this._onLocalizedAssetLoad, this); asset.off('change', this._onLocalizedAssetChange, this); asset.off('remove', this._onLocalizedAssetRemove, this); } _onLocalizedAssetAdd(asset) { if (this._localizedAsset !== asset.id) return; this._bindLocalizedAsset(); } _onLocalizedAssetLoad(asset) { this.fire('load', asset); } _onLocalizedAssetChange(asset, name, newValue, oldValue) { this.fire('change', asset, name, newValue, oldValue); } _onLocalizedAssetRemove(asset) { if (this._localizedAsset === asset.id) { this.localizedAsset = this._defaultAsset; } this.fire('remove', asset); } _onLocaleAdd(locale, assetId) { if (this._app.i18n.locale !== locale) return; this._onSetLocale(locale); } _onLocaleRemove(locale, assetId) { if (this._app.i18n.locale !== locale) return; this._onSetLocale(locale); } _onSetLocale(locale) { if (!this._defaultAsset) { this.localizedAsset = null; return; } const asset = this._app.assets.get(this._defaultAsset); if (!asset || this._disableLocalization) { this.localizedAsset = this._defaultAsset; return; } const localizedAssetId = asset.getLocalizedAssetId(locale); if (!localizedAssetId) { this.localizedAsset = this._defaultAsset; return; } this.localizedAsset = localizedAssetId; } destroy() { this.defaultAsset = null; this._app.i18n.off(I18n.EVENT_CHANGE, this._onSetLocale, this); this.off(); } constructor(app){ super(); this._app = app; app.i18n.on(I18n.EVENT_CHANGE, this._onSetLocale, this); this._autoLoad = false; this._disableLocalization = false; this._defaultAsset = null; this._localizedAsset = null; } } const FONT_MSDF = 'msdf'; const FONT_BITMAP = 'bitmap'; const EOF_TOKEN = 0; const ERROR_TOKEN = 1; const TEXT_TOKEN = 2; const OPEN_BRACKET_TOKEN = 3; const CLOSE_BRACKET_TOKEN = 4; const EQUALS_TOKEN = 5; const STRING_TOKEN = 6; const IDENTIFIER_TOKEN = 7; const WHITESPACE_TOKEN = 8; const WHITESPACE_CHARS = ' \t\n\r\v\f'; const IDENTIFIER_REGEX = /[\w|/]/; class Scanner { read() { let token = this._read(); while(token === WHITESPACE_TOKEN){ token = this._read(); } if (token !== EOF_TOKEN && token !== ERROR_TOKEN) { this._last = this._index; } return token; } buf() { return this._buf; } last() { return this._last; } error() { return this._error; } debugPrint() { const tokenStrings = [ 'EOF', 'ERROR', 'TEXT', 'OPEN_BRACKET', 'CLOSE_BRACKET', 'EQUALS', 'STRING', 'IDENTIFIER', 'WHITESPACE' ]; let token = this.read(); let result = ''; while(true){ result += `${(result.length > 0 ? '\n' : '') + tokenStrings[token]} '${this.buf().join('')}'`; if (token === EOF_TOKEN || token === ERROR_TOKEN) { break; } token = this.read(); } return result; } _read() { this._buf = []; if (this._eof()) { return EOF_TOKEN; } return this._mode === 'text' ? this._text() : this._tag(); } _text() { while(true){ switch(this._cur){ case null: return this._buf.length > 0 ? TEXT_TOKEN : EOF_TOKEN; case '[': this._mode = 'tag'; return this._buf.length > 0 ? TEXT_TOKEN : this._tag(); case '\\': this._next(); switch(this._cur){ case '[': this._store(); break; default: this._output('\\'); break; } break; default: this._store(); break; } } } _tag() { switch(this._cur){ case null: this._error = 'unexpected end of input reading tag'; return ERROR_TOKEN; case '[': this._store(); return OPEN_BRACKET_TOKEN; case ']': this._store(); this._mode = 'text'; return CLOSE_BRACKET_TOKEN; case '=': this._store(); return EQUALS_TOKEN; case ' ': case '\t': case '\n': case '\r': case '\v': case '\f': return this._whitespace(); case '"': return this._string(); default: if (!this._isIdentifierSymbol(this._cur)) { this._error = 'unrecognized character'; return ERROR_TOKEN; } return this._identifier(); } } _whitespace() { this._store(); while(WHITESPACE_CHARS.indexOf(this._cur) !== -1){ this._store(); } return WHITESPACE_TOKEN; } _string() { this._next(); while(true){ switch(this._cur){ case null: this._error = 'unexpected end of input reading string'; return ERROR_TOKEN; case '"': this._next(); return STRING_TOKEN; default: this._store(); break; } } } _identifier() { this._store(); while(this._cur !== null && this._isIdentifierSymbol(this._cur)){ this._store(); } return IDENTIFIER_TOKEN; } _isIdentifierSymbol(s) { return s.length === 1 && s.match(IDENTIFIER_REGEX) !== null; } _eof() { return this._cur === null; } _next() { if (!this._eof()) { this._index++; this._cur = this._index < this._symbols.length ? this._symbols[this._index] : null; } return this._cur; } _store() { this._buf.push(this._cur); return this._next(); } _output(c) { this._buf.push(c); } constructor(symbols){ this._symbols = symbols; this._index = 0; this._last = 0; this._cur = this._symbols.length > 0 ? this._symbols[0] : null; this._buf = []; this._mode = 'text'; this._error = null; } } class Parser { parse(symbols, tags) { while(true){ const token = this._scanner.read(); switch(token){ case EOF_TOKEN: return true; case ERROR_TOKEN: return false; case TEXT_TOKEN: Array.prototype.push.apply(symbols, this._scanner.buf()); break; case OPEN_BRACKET_TOKEN: if (!this._parseTag(symbols, tags)) { return false; } break; default: return false; } } } error() { return `Error evaluating markup at #${this._scanner.last().toString()} (${this._scanner.error() || this._error})`; } _parseTag(symbols, tags) { let token = this._scanner.read(); if (token !== IDENTIFIER_TOKEN) { this._error = 'expected identifier'; return false; } const name = this._scanner.buf().join(''); if (name[0] === '/') { for(let index = tags.length - 1; index >= 0; --index){ if (name === `/${tags[index].name}` && tags[index].end === null) { tags[index].end = symbols.length; token = this._scanner.read(); if (token !== CLOSE_BRACKET_TOKEN) { this._error = 'expected close bracket'; return false; } return true; } } this._error = 'failed to find matching tag'; return false; } const tag = { name: name, value: null, attributes: {}, start: symbols.length, end: null }; token = this._scanner.read(); if (token === EQUALS_TOKEN) { token = this._scanner.read(); if (token !== STRING_TOKEN) { this._error = 'expected string'; return false; } tag.value = this._scanner.buf().join(''); token = this._scanner.read(); } while(true){ switch(token){ case CLOSE_BRACKET_TOKEN: tags.push(tag); return true; case IDENTIFIER_TOKEN: { const identifier = this._scanner.buf().join(''); token = this._scanner.read(); if (token !== EQUALS_TOKEN) { this._error = 'expected equals'; return false; } token = this._scanner.read(); if (token !== STRING_TOKEN) { this._error = 'expected string'; return false; } const value = this._scanner.buf().join(''); tag.attributes[identifier] = value; break; } default: this._error = 'expected close bracket or identifier'; return false; } token = this._scanner.read(); } } constructor(symbols){ this._scanner = new Scanner(symbols); this._error = null; } } function merge(target, source) { for(const key in source){ if (!source.hasOwnProperty(key)) { continue; } const value = source[key]; if (value instanceof Object) { if (!target.hasOwnProperty(key)) { target[key] = {}; } merge(target[key], source[key]); } else { target[key] = value; } } } function combineTags(tags) { if (tags.length === 0) { return null; } const result = {}; for(let index = 0; index < tags.length; ++index){ const tag = tags[index]; const tmp = {}; tmp[tag.name] = { value: tag.value, attributes: tag.attributes }; merge(result, tmp); } return result; } function resolveMarkupTags(tags, numSymbols) { if (tags.length === 0) { return null; } const edges = {}; for(let index = 0; index < tags.length; ++index){ const tag = tags[index]; if (!edges.hasOwnProperty(tag.start)) { edges[tag.start] = { open: [ tag ], close: null }; } else { if (edges[tag.start].open === null) { edges[tag.start].open = [ tag ]; } else { edges[tag.start].open.push(tag); } } if (!edges.hasOwnProperty(tag.end)) { edges[tag.end] = { open: null, close: [ tag ] }; } else { if (edges[tag.end].close === null) { edges[tag.end].close = [ tag ]; } else { edges[tag.end].close.push(tag); } } } let tagStack = []; function removeTags(tags) { tagStack = tagStack.filter((tag)=>{ return tags.find((t)=>{ return t === tag; }) === undefined; }); } function addTags(tags) { for(let index = 0; index < tags.length; ++index){ tagStack.push(tags[index]); } } const edgeKeys = Object.keys(edges).sort((a, b)=>{ return a - b; }); const resolvedTags = []; for(let index = 0; index < edgeKeys.length; ++index){ const edge = edges[edgeKeys[index]]; if (edge.close !== null) { removeTags(edge.close); } if (edge.open !== null) { addTags(edge.open); } resolvedTags.push({ start: edgeKeys[index], tags: combineTags(tagStack) }); } const result = []; let prevTag = null; for(let index = 0; index < resolvedTags.length; ++index){ const resolvedTag = resolvedTags[index]; while(result.length < resolvedTag.start){ result.push(prevTag ? prevTag.tags : null); } prevTag = resolvedTag; } while(result.length < numSymbols){ result.push(null); } return result; } function evaluateMarkup(symbols) { const parser = new Parser(symbols); const stripped_symbols = []; const tags = []; if (!parser.parse(stripped_symbols, tags)) { console.warn(parser.error()); return { symbols: symbols, tags: null }; } const invalidTag = tags.find((t)=>{ return t.end === null; }); if (invalidTag) { console.warn(`Markup error: found unclosed tag='${invalidTag.name}'`); return { symbols: symbols, tags: null }; } const resolved_tags = resolveMarkupTags(tags, stripped_symbols.length); return { symbols: stripped_symbols, tags: resolved_tags }; } class Markup { static evaluate(symbols) { return evaluateMarkup(symbols); } } class MeshInfo { constructor(){ this.count = 0; this.quad = 0; this.lines = {}; this.positions = []; this.normals = []; this.uvs = []; this.colors = []; this.indices = []; this.outlines = []; this.shadows = []; this.meshInstance = null; } } function createTextMesh(device, meshInfo) { const mesh = new Mesh(device); mesh.setPositions(meshInfo.positions); mesh.setNormals(meshInfo.normals); mesh.setColors32(meshInfo.colors); mesh.setUvs(0, meshInfo.uvs); mesh.setIndices(meshInfo.indices); mesh.setVertexStream(SEMANTIC_ATTR8, meshInfo.outlines, 3, undefined, TYPE_FLOAT32, false); mesh.setVertexStream(SEMANTIC_ATTR9, meshInfo.shadows, 3, undefined, TYPE_FLOAT32, false); mesh.update(); return mesh; } const LINE_BREAK_CHAR = /^[\r\n]$/; const WHITESPACE_CHAR = /^[ \t]$/; const WORD_BOUNDARY_CHAR = /^[ \t\-]|\u200b$/; const ALPHANUMERIC_CHAR = /^[a-z0-9]$/i; const CJK_CHAR = /^[\u1100-\u11ff]|[\u3000-\u9fff\ua960-\ua97f]|[\uac00-\ud7ff]$/; const NO_LINE_BREAK_CJK_CHAR = /^[〕〉》」』】〙〗〟ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻]$/; const CONTROL_CHARS = [ '\u200B', '\u061C', '\u200E', '\u200F', '\u202A', '\u202B', '\u202C', '\u202D', '\u202E', '\u2066', '\u2067', '\u2068', '\u2069' ]; const CONTROL_GLYPH_DATA = { width: 0, height: 0, xadvance: 0, xoffset: 0, yoffset: 0 }; const colorTmp = new Color(); const vec2Tmp = new Vec2(); const _tempColor = new Color(); class TextElement { destroy() { this._setMaterial(null); if (this._model) { this._element.removeModelFromLayers(this._model); this._model.destroy(); this._model = null; } this._fontAsset.destroy(); this.font = null; this._element.off('resize', this._onParentResize, this); this._element.off('set:screen', this._onScreenChange, this); this._element.off('screen:set:screenspace', this._onScreenSpaceChange, this); this._element.off('set:draworder', this._onDrawOrderChange, this); this._element.off('set:pivot', this._onPivotChange, this); this._system.app.i18n.off(I18n.EVENT_CHANGE, this._onLocaleSet, this); this._system.app.i18n.off('data:add', this._onLocalizationData, this); this._system.app.i18n.off('data:remove', this._onLocalizationData, this); } _onParentResize(width, height) { if (this._noResize) return; if (this._font) this._updateText(); } _onScreenChange(screen) { if (screen) { this._updateMaterial(screen.screen.screenSpace); } else { this._updateMaterial(false); } } _onScreenSpaceChange(value) { this._updateMaterial(value); } _onDrawOrderChange(order) { this._drawOrder = order; if (this._model) { for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ this._model.meshInstances[i].drawOrder = order; } } } _onPivotChange(pivot) { if (this._font) { this._updateText(); } } _onLocaleSet(locale) { if (!this._i18nKey) return; if (this.fontAsset) { const asset = this._system.app.assets.get(this.fontAsset); if (!asset || !asset.resource || asset.resource !== this._font) { this.font = null; } } this._resetLocalizedText(); } _onLocalizationData(locale, messages) { if (this._i18nKey && messages[this._i18nKey]) { this._resetLocalizedText(); } } _resetLocalizedText() { this._setText(this._system.app.i18n.getText(this._i18nKey)); } _setText(text) { if (this.unicodeConverter) { const unicodeConverterFunc = this._system.getUnicodeConverter(); if (unicodeConverterFunc) { text = unicodeConverterFunc(text); } else { console.warn('Element created with unicodeConverter option but no unicodeConverter function registered'); } } if (this._text !== text) { if (this._font) { this._updateText(text); } this._text = text; } } _updateText(text) { let tags; if (text === undefined) text = this._text; this._symbols = string.getSymbols(text.normalize ? text.normalize('NFC') : text); if (this._symbols.length === 0) { this._symbols = [ ' ' ]; } if (this._enableMarkup) { const results = Markup.evaluate(this._symbols); this._symbols = results.symbols; tags = results.tags || []; } if (this._rtlReorder) { const rtlReorderFunc = this._system.app.systems.element.getRtlReorder(); if (rtlReorderFunc) { const results = rtlReorderFunc(this._symbols); this._rtl = results.rtl; this._symbols = results.mapping.map(function(v) { return this._symbols[v]; }, this); if (tags) { tags = results.mapping.map((v)=>{ return tags[v]; }); } } else { console.warn('Element created with rtlReorder option but no rtlReorder function registered'); } } else { this._rtl = false; } const getColorThicknessHash = (color, thickness)=>{ return `${color.toString(true).toLowerCase()}:${thickness.toFixed(2)}`; }; const getColorOffsetHash = (color, offset)=>{ return `${color.toString(true).toLowerCase()}:${offset.x.toFixed(2)}:${offset.y.toFixed(2)}`; }; if (tags) { const paletteMap = {}; const outlinePaletteMap = {}; const shadowPaletteMap = {}; this._colorPalette = [ Math.round(this._color.r * 255), Math.round(this._color.g * 255), Math.round(this._color.b * 255) ]; this._outlinePalette = [ Math.round(this._outlineColor.r * 255), Math.round(this._outlineColor.g * 255), Math.round(this._outlineColor.b * 255), Math.round(this._outlineColor.a * 255), Math.round(this._outlineThickness * 255) ]; this._shadowPalette = [ Math.round(this._shadowColor.r * 255), Math.round(this._shadowColor.g * 255), Math.round(this._shadowColor.b * 255), Math.round(this._shadowColor.a * 255), Math.round(this._shadowOffset.x * 127), Math.round(this._shadowOffset.y * 127) ]; this._symbolColors = []; this._symbolOutlineParams = []; this._symbolShadowParams = []; paletteMap[this._color.toString(false).toLowerCase()] = 0; outlinePaletteMap[getColorThicknessHash(this._outlineColor, this._outlineThickness)] = 0; shadowPaletteMap[getColorOffsetHash(this._shadowColor, this._shadowOffset)] = 0; for(let i = 0, len = this._symbols.length; i < len; ++i){ const tag = tags[i]; let color = 0; if (tag && tag.color && tag.color.value) { const c = tag.color.value; if (c.length === 7 && c[0] === '#') { const hex = c.substring(1).toLowerCase(); if (paletteMap.hasOwnProperty(hex)) { color = paletteMap[hex]; } else { if (/^[0-9a-f]{6}$/.test(hex)) { color = this._colorPalette.length / 3; paletteMap[hex] = color; this._colorPalette.push(parseInt(hex.substring(0, 2), 16)); this._colorPalette.push(parseInt(hex.substring(2, 4), 16)); this._colorPalette.push(parseInt(hex.substring(4, 6), 16)); } } } } this._symbolColors.push(color); let outline = 0; if (tag && tag.outline && (tag.outline.attributes.color || tag.outline.attributes.thickness)) { let color = tag.outline.attributes.color ? colorTmp.fromString(tag.outline.attributes.color) : this._outlineColor; let thickness = Number(tag.outline.attributes.thickness); if (Number.isNaN(color.r) || Number.isNaN(color.g) || Number.isNaN(color.b) || Number.isNaN(color.a)) { color = this._outlineColor; } if (Number.isNaN(thickness)) { thickness = this._outlineThickness; } const outlineHash = getColorThicknessHash(color, thickness); if (outlinePaletteMap.hasOwnProperty(outlineHash)) { outline = outlinePaletteMap[outlineHash]; } else { outline = this._outlinePalette.length / 5; outlinePaletteMap[outlineHash] = outline; this._outlinePalette.push(Math.round(color.r * 255), Math.round(color.g * 255), Math.round(color.b * 255), Math.round(color.a * 255), Math.round(thickness * 255)); } } this._symbolOutlineParams.push(outline); let shadow = 0; if (tag && tag.shadow && (tag.shadow.attributes.color || tag.shadow.attributes.offset || tag.shadow.attributes.offsetX || tag.shadow.attributes.offsetY)) { let color = tag.shadow.attributes.color ? colorTmp.fromString(tag.shadow.attributes.color) : this._shadowColor; const off = Number(tag.shadow.attributes.offset); const offX = Number(tag.shadow.attributes.offsetX); const offY = Number(tag.shadow.attributes.offsetY); if (Number.isNaN(color.r) || Number.isNaN(color.g) || Number.isNaN(color.b) || Number.isNaN(color.a)) { color = this._shadowColor; } const offset = vec2Tmp.set(!Number.isNaN(offX) ? offX : !Number.isNaN(off) ? off : this._shadowOffset.x, !Number.isNaN(offY) ? offY : !Number.isNaN(off) ? off : this._shadowOffset.y); const shadowHash = getColorOffsetHash(color, offset); if (shadowPaletteMap.hasOwnProperty(shadowHash)) { shadow = shadowPaletteMap[shadowHash]; } else { shadow = this._shadowPalette.length / 6; shadowPaletteMap[shadowHash] = shadow; this._shadowPalette.push(Math.round(color.r * 255), Math.round(color.g * 255), Math.round(color.b * 255), Math.round(color.a * 255), Math.round(offset.x * 127), Math.round(offset.y * 127)); } } this._symbolShadowParams.push(shadow); } } else { this._colorPalette = []; this._symbolColors = null; this._symbolOutlineParams = null; this._symbolShadowParams = null; } this._updateMaterialEmissive(); this._updateMaterialOutline(); this._updateMaterialShadow(); const charactersPerTexture = this._calculateCharsPerTexture(); let removedModel = false; const element = this._element; const screenSpace = element._isScreenSpace(); const screenCulled = element._isScreenCulled(); const visibleFn = function(camera) { return element.isVisibleForCamera(camera); }; for(let i = 0, len = this._meshInfo.length; i < len; i++){ const l = charactersPerTexture[i] || 0; const meshInfo = this._meshInfo[i]; if (meshInfo.count !== l) { if (!removedModel) { element.removeModelFromLayers(this._model); removedModel = true; } meshInfo.count = l; meshInfo.positions.length = meshInfo.normals.length = l * 3 * 4; meshInfo.indices.length = l * 3 * 2; meshInfo.uvs.length = l * 2 * 4; meshInfo.colors.length = l * 4 * 4; meshInfo.outlines.length = l * 4 * 3; meshInfo.shadows.length = l * 4 * 3; if (meshInfo.meshInstance) { this._removeMeshInstance(meshInfo.meshInstance); } if (l === 0) { meshInfo.meshInstance = null; continue; } for(let v = 0; v < l; v++){ meshInfo.indices[v * 3 * 2 + 0] = v * 4; meshInfo.indices[v * 3 * 2 + 1] = v * 4 + 1; meshInfo.indices[v * 3 * 2 + 2] = v * 4 + 3; meshInfo.indices[v * 3 * 2 + 3] = v * 4 + 2; meshInfo.indices[v * 3 * 2 + 4] = v * 4 + 3; meshInfo.indices[v * 3 * 2 + 5] = v * 4 + 1; meshInfo.normals[v * 4 * 3 + 0] = 0; meshInfo.normals[v * 4 * 3 + 1] = 0; meshInfo.normals[v * 4 * 3 + 2] = -1; meshInfo.normals[v * 4 * 3 + 3] = 0; meshInfo.normals[v * 4 * 3 + 4] = 0; meshInfo.normals[v * 4 * 3 + 5] = -1; meshInfo.normals[v * 4 * 3 + 6] = 0; meshInfo.normals[v * 4 * 3 + 7] = 0; meshInfo.normals[v * 4 * 3 + 8] = -1; meshInfo.normals[v * 4 * 3 + 9] = 0; meshInfo.normals[v * 4 * 3 + 10] = 0; meshInfo.normals[v * 4 * 3 + 11] = -1; } const mesh = createTextMesh(this._system.app.graphicsDevice, meshInfo); const mi = new MeshInstance(mesh, this._material, this._node); mi.name = `Text Element: ${this._entity.name}`; mi.castShadow = false; mi.receiveShadow = false; mi.cull = !screenSpace; mi.screenSpace = screenSpace; mi.drawOrder = this._drawOrder; if (screenCulled) { mi.cull = true; mi.isVisibleFunc = visibleFn; } this._setTextureParams(mi, this._font.textures[i]); mi.setParameter('material_emissive', this._colorUniform); mi.setParameter('material_opacity', this._color.a); mi.setParameter('font_sdfIntensity', this._font.intensity); mi.setParameter('font_pxrange', this._getPxRange(this._font)); mi.setParameter('font_textureWidth', this._font.data.info.maps[i].width); mi.setParameter('outline_color', this._outlineColorUniform); mi.setParameter('outline_thickness', this._outlineThicknessScale * this._outlineThickness); mi.setParameter('shadow_color', this._shadowColorUniform); if (this._symbolShadowParams) { this._shadowOffsetUniform[0] = 0; this._shadowOffsetUniform[1] = 0; } else { const ratio = -this._font.data.info.maps[i].width / this._font.data.info.maps[i].height; this._shadowOffsetUniform[0] = this._shadowOffsetScale * this._shadowOffset.x; this._shadowOffsetUniform[1] = ratio * this._shadowOffsetScale * this._shadowOffset.y; } mi.setParameter('shadow_offset', this._shadowOffsetUniform); meshInfo.meshInstance = mi; this._model.meshInstances.push(mi); } } if (this._element.maskedBy) { this._element._setMaskedBy(this._element.maskedBy); } if (removedModel && this._element.enabled && this._entity.enabled) { this._element.addModelToLayers(this._model); } this._updateMeshes(); this._rangeStart = 0; this._rangeEnd = this._symbols.length; this._updateRenderRange(); } _removeMeshInstance(meshInstance) { meshInstance.destroy(); const idx = this._model.meshInstances.indexOf(meshInstance); if (idx !== -1) { this._model.meshInstances.splice(idx, 1); } } _setMaterial(material) { this._material = material; if (this._model) { for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.material = material; } } } _updateMaterial(screenSpace) { const element = this._element; const screenCulled = element._isScreenCulled(); const visibleFn = function(camera) { return element.isVisibleForCamera(camera); }; const msdf = this._font && this._font.type === FONT_MSDF; this._material = this._system.getTextElementMaterial(screenSpace, msdf, this._enableMarkup); if (this._model) { for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.cull = !screenSpace; mi.material = this._material; mi.screenSpace = screenSpace; if (screenCulled) { mi.cull = true; mi.isVisibleFunc = visibleFn; } else { mi.isVisibleFunc = null; } } } } _updateMaterialEmissive() { if (this._symbolColors) { this._colorUniform[0] = 1; this._colorUniform[1] = 1; this._colorUniform[2] = 1; } else { _tempColor.linear(this._color); this._colorUniform[0] = _tempColor.r; this._colorUniform[1] = _tempColor.g; this._colorUniform[2] = _tempColor.b; } } _updateMaterialOutline() { if (this._symbolOutlineParams) { this._outlineColorUniform[0] = 0; this._outlineColorUniform[1] = 0; this._outlineColorUniform[2] = 0; this._outlineColorUniform[3] = 1; } else { _tempColor.linear(this._outlineColor); this._outlineColorUniform[0] = _tempColor.r; this._outlineColorUniform[1] = _tempColor.g; this._outlineColorUniform[2] = _tempColor.b; this._outlineColorUniform[3] = _tempColor.a; } } _updateMaterialShadow() { if (this._symbolOutlineParams) { this._shadowColorUniform[0] = 0; this._shadowColorUniform[1] = 0; this._shadowColorUniform[2] = 0; this._shadowColorUniform[3] = 0; } else { _tempColor.linear(this._shadowColor); this._shadowColorUniform[0] = _tempColor.r; this._shadowColorUniform[1] = _tempColor.g; this._shadowColorUniform[2] = _tempColor.b; this._shadowColorUniform[3] = _tempColor.a; } } _isWordBoundary(char) { return WORD_BOUNDARY_CHAR.test(char); } _isValidNextChar(nextchar) { return nextchar !== null && !NO_LINE_BREAK_CJK_CHAR.test(nextchar); } _isNextCJKBoundary(char, nextchar) { return CJK_CHAR.test(char) && (WORD_BOUNDARY_CHAR.test(nextchar) || ALPHANUMERIC_CHAR.test(nextchar)); } _isNextCJKWholeWord(nextchar) { return CJK_CHAR.test(nextchar); } _updateMeshes() { const json = this._font.data; const self = this; const minFont = Math.min(this._minFontSize, this._maxFontSize); const maxFont = this._maxFontSize; const autoFit = this._shouldAutoFit(); if (autoFit) { this._fontSize = this._maxFontSize; } const MAGIC = 32; const l = this._symbols.length; let _x = 0; let _y = 0; let _z = 0; let _xMinusTrailingWhitespace = 0; let lines = 1; let wordStartX = 0; let wordStartIndex = 0; let lineStartIndex = 0; let numWordsThisLine = 0; let numCharsThisLine = 0; let numBreaksThisLine = 0; const splitHorizontalAnchors = Math.abs(this._element.anchor.x - this._element.anchor.z) >= 0.0001; let maxLineWidth = this._element.calculatedWidth; if (this.autoWidth && !splitHorizontalAnchors || !this._wrapLines) { maxLineWidth = Number.POSITIVE_INFINITY; } let fontMinY = 0; let fontMaxY = 0; let char, data, quad, nextchar; function breakLine(symbols, lineBreakIndex, lineBreakX) { self._lineWidths.push(Math.abs(lineBreakX)); const sliceStart = lineStartIndex > lineBreakIndex ? lineBreakIndex + 1 : lineStartIndex; const sliceEnd = lineStartIndex > lineBreakIndex ? lineStartIndex + 1 : lineBreakIndex; const chars = symbols.slice(sliceStart, sliceEnd); if (numBreaksThisLine) { let i = chars.length; while(i-- && numBreaksThisLine > 0){ if (LINE_BREAK_CHAR.test(chars[i])) { chars.splice(i, 1); numBreaksThisLine--; } } } self._lineContents.push(chars.join('')); _x = 0; _y -= self._scaledLineHeight; lines++; numWordsThisLine = 0; numCharsThisLine = 0; numBreaksThisLine = 0; wordStartX = 0; lineStartIndex = lineBreakIndex; } let retryUpdateMeshes = true; while(retryUpdateMeshes){ retryUpdateMeshes = false; if (autoFit) { this._scaledLineHeight = this._lineHeight * this._fontSize / (this._maxFontSize || 0.0001); } else { this._scaledLineHeight = this._lineHeight; } this.width = 0; this.height = 0; this._lineWidths = []; this._lineContents = []; _x = 0; _y = 0; _z = 0; _xMinusTrailingWhitespace = 0; lines = 1; wordStartX = 0; wordStartIndex = 0; lineStartIndex = 0; numWordsThisLine = 0; numCharsThisLine = 0; numBreaksThisLine = 0; const scale = this._fontSize / MAGIC; fontMinY = this._fontMinY * scale; fontMaxY = this._fontMaxY * scale; for(let i = 0; i < this._meshInfo.length; i++){ this._meshInfo[i].quad = 0; this._meshInfo[i].lines = {}; } let color_r = 255; let color_g = 255; let color_b = 255; let outline_color_rg = 255 + 255 * 256; let outline_color_ba = 255 + 255 * 256; let outline_thickness = 0; let shadow_color_rg = 255 + 255 * 256; let shadow_color_ba = 255 + 255 * 256; let shadow_offset_xy = 127 + 127 * 256; for(let i = 0; i < l; i++){ char = this._symbols[i]; nextchar = i + 1 >= l ? null : this._symbols[i + 1]; const isLineBreak = LINE_BREAK_CHAR.test(char); if (isLineBreak) { numBreaksThisLine++; if (!this._wrapLines || this._maxLines < 0 || lines < this._maxLines) { breakLine(this._symbols, i, _xMinusTrailingWhitespace); wordStartIndex = i + 1; lineStartIndex = i + 1; } continue; } let x = 0; let y = 0; let advance = 0; let quadsize = 1; let dataScale, size; data = json.chars[char]; if (!data) { if (CONTROL_CHARS.indexOf(char) !== -1) { data = CONTROL_GLYPH_DATA; } else { if (json.chars[' ']) { data = json.chars[' ']; } else { for(const key in json.chars){ data = json.chars[key]; break; } } } } if (data) { let kerning = 0; if (numCharsThisLine > 0) { const kernTable = this._font.data.kerning; if (kernTable) { const kernLeft = kernTable[string.getCodePoint(this._symbols[i - 1]) || 0]; if (kernLeft) { kerning = kernLeft[string.getCodePoint(this._symbols[i]) || 0] || 0; } } } dataScale = data.scale || 1; size = (data.width + data.height) / 2; quadsize = scale * size / dataScale; advance = (data.xadvance + kerning) * scale; x = (data.xoffset - kerning) * scale; y = data.yoffset * scale; } else { console.error(`Couldn't substitute missing character: '${char}'`); } const isWhitespace = WHITESPACE_CHAR.test(char); const meshInfoId = data && data.map || 0; const ratio = -this._font.data.info.maps[meshInfoId].width / this._font.data.info.maps[meshInfoId].height; const meshInfo = this._meshInfo[meshInfoId]; const candidateLineWidth = _x + this._spacing * advance; if (candidateLineWidth > maxLineWidth && numCharsThisLine > 0 && !isWhitespace) { if (this._maxLines < 0 || lines < this._maxLines) { if (numWordsThisLine === 0) { wordStartIndex = i; breakLine(this._symbols, i, _xMinusTrailingWhitespace); } else { const backtrack = Math.max(i - wordStartIndex, 0); if (this._meshInfo.length <= 1) { meshInfo.lines[lines - 1] -= backtrack; meshInfo.quad -= backtrack; } else { const backtrackStart = wordStartIndex; const backtrackEnd = i; for(let j = backtrackStart; j < backtrackEnd; j++){ const backChar = this._symbols[j]; const backCharData = json.chars[backChar]; const backMeshInfo = this._meshInfo[backCharData && backCharData.map || 0]; backMeshInfo.lines[lines - 1] -= 1; backMeshInfo.quad -= 1; } } i -= backtrack + 1; breakLine(this._symbols, wordStartIndex, wordStartX); continue; } } } quad = meshInfo.quad; meshInfo.lines[lines - 1] = quad; let left = _x - x; let right = left + quadsize; const bottom = _y - y; const top = bottom + quadsize; if (this._rtl) { const shift = quadsize - x - this._spacing * advance - x; left -= shift; right -= shift; } meshInfo.positions[quad * 4 * 3 + 0] = left; meshInfo.positions[quad * 4 * 3 + 1] = bottom; meshInfo.positions[quad * 4 * 3 + 2] = _z; meshInfo.positions[quad * 4 * 3 + 3] = right; meshInfo.positions[quad * 4 * 3 + 4] = bottom; meshInfo.positions[quad * 4 * 3 + 5] = _z; meshInfo.positions[quad * 4 * 3 + 6] = right; meshInfo.positions[quad * 4 * 3 + 7] = top; meshInfo.positions[quad * 4 * 3 + 8] = _z; meshInfo.positions[quad * 4 * 3 + 9] = left; meshInfo.positions[quad * 4 * 3 + 10] = top; meshInfo.positions[quad * 4 * 3 + 11] = _z; this.width = Math.max(this.width, candidateLineWidth); let fontSize; if (this._shouldAutoFitWidth() && this.width > this._element.calculatedWidth) { fontSize = Math.floor(this._element.fontSize * this._element.calculatedWidth / (this.width || 0.0001)); fontSize = math.clamp(fontSize, minFont, maxFont); if (fontSize !== this._element.fontSize) { this._fontSize = fontSize; retryUpdateMeshes = true; break; } } this.height = Math.max(this.height, fontMaxY - (_y + fontMinY)); if (this._shouldAutoFitHeight() && this.height > this._element.calculatedHeight) { fontSize = math.clamp(this._fontSize - 1, minFont, maxFont); if (fontSize !== this._element.fontSize) { this._fontSize = fontSize; retryUpdateMeshes = true; break; } } _x += this._spacing * advance; if (!isWhitespace) { _xMinusTrailingWhitespace = _x; } if (this._isWordBoundary(char) || this._isValidNextChar(nextchar) && (this._isNextCJKBoundary(char, nextchar) || this._isNextCJKWholeWord(nextchar))) { numWordsThisLine++; wordStartX = _xMinusTrailingWhitespace; wordStartIndex = i + 1; } numCharsThisLine++; const uv = this._getUv(char); meshInfo.uvs[quad * 4 * 2 + 0] = uv[0]; meshInfo.uvs[quad * 4 * 2 + 1] = 1.0 - uv[1]; meshInfo.uvs[quad * 4 * 2 + 2] = uv[2]; meshInfo.uvs[quad * 4 * 2 + 3] = 1.0 - uv[1]; meshInfo.uvs[quad * 4 * 2 + 4] = uv[2]; meshInfo.uvs[quad * 4 * 2 + 5] = 1.0 - uv[3]; meshInfo.uvs[quad * 4 * 2 + 6] = uv[0]; meshInfo.uvs[quad * 4 * 2 + 7] = 1.0 - uv[3]; if (this._symbolColors) { const colorIdx = this._symbolColors[i] * 3; color_r = this._colorPalette[colorIdx]; color_g = this._colorPalette[colorIdx + 1]; color_b = this._colorPalette[colorIdx + 2]; } meshInfo.colors[quad * 4 * 4 + 0] = color_r; meshInfo.colors[quad * 4 * 4 + 1] = color_g; meshInfo.colors[quad * 4 * 4 + 2] = color_b; meshInfo.colors[quad * 4 * 4 + 3] = 255; meshInfo.colors[quad * 4 * 4 + 4] = color_r; meshInfo.colors[quad * 4 * 4 + 5] = color_g; meshInfo.colors[quad * 4 * 4 + 6] = color_b; meshInfo.colors[quad * 4 * 4 + 7] = 255; meshInfo.colors[quad * 4 * 4 + 8] = color_r; meshInfo.colors[quad * 4 * 4 + 9] = color_g; meshInfo.colors[quad * 4 * 4 + 10] = color_b; meshInfo.colors[quad * 4 * 4 + 11] = 255; meshInfo.colors[quad * 4 * 4 + 12] = color_r; meshInfo.colors[quad * 4 * 4 + 13] = color_g; meshInfo.colors[quad * 4 * 4 + 14] = color_b; meshInfo.colors[quad * 4 * 4 + 15] = 255; if (this._symbolOutlineParams) { const outlineIdx = this._symbolOutlineParams[i] * 5; outline_color_rg = this._outlinePalette[outlineIdx] + this._outlinePalette[outlineIdx + 1] * 256; outline_color_ba = this._outlinePalette[outlineIdx + 2] + this._outlinePalette[outlineIdx + 3] * 256; outline_thickness = this._outlinePalette[outlineIdx + 4]; } meshInfo.outlines[quad * 4 * 3 + 0] = outline_color_rg; meshInfo.outlines[quad * 4 * 3 + 1] = outline_color_ba; meshInfo.outlines[quad * 4 * 3 + 2] = outline_thickness; meshInfo.outlines[quad * 4 * 3 + 3] = outline_color_rg; meshInfo.outlines[quad * 4 * 3 + 4] = outline_color_ba; meshInfo.outlines[quad * 4 * 3 + 5] = outline_thickness; meshInfo.outlines[quad * 4 * 3 + 6] = outline_color_rg; meshInfo.outlines[quad * 4 * 3 + 7] = outline_color_ba; meshInfo.outlines[quad * 4 * 3 + 8] = outline_thickness; meshInfo.outlines[quad * 4 * 3 + 9] = outline_color_rg; meshInfo.outlines[quad * 4 * 3 + 10] = outline_color_ba; meshInfo.outlines[quad * 4 * 3 + 11] = outline_thickness; if (this._symbolShadowParams) { const shadowIdx = this._symbolShadowParams[i] * 6; shadow_color_rg = this._shadowPalette[shadowIdx] + this._shadowPalette[shadowIdx + 1] * 256; shadow_color_ba = this._shadowPalette[shadowIdx + 2] + this._shadowPalette[shadowIdx + 3] * 256; shadow_offset_xy = this._shadowPalette[shadowIdx + 4] + 127 + Math.round(ratio * this._shadowPalette[shadowIdx + 5] + 127) * 256; } meshInfo.shadows[quad * 4 * 3 + 0] = shadow_color_rg; meshInfo.shadows[quad * 4 * 3 + 1] = shadow_color_ba; meshInfo.shadows[quad * 4 * 3 + 2] = shadow_offset_xy; meshInfo.shadows[quad * 4 * 3 + 3] = shadow_color_rg; meshInfo.shadows[quad * 4 * 3 + 4] = shadow_color_ba; meshInfo.shadows[quad * 4 * 3 + 5] = shadow_offset_xy; meshInfo.shadows[quad * 4 * 3 + 6] = shadow_color_rg; meshInfo.shadows[quad * 4 * 3 + 7] = shadow_color_ba; meshInfo.shadows[quad * 4 * 3 + 8] = shadow_offset_xy; meshInfo.shadows[quad * 4 * 3 + 9] = shadow_color_rg; meshInfo.shadows[quad * 4 * 3 + 10] = shadow_color_ba; meshInfo.shadows[quad * 4 * 3 + 11] = shadow_offset_xy; meshInfo.quad++; } if (retryUpdateMeshes) { continue; } if (lineStartIndex < l) { breakLine(this._symbols, l, _x); } } this._noResize = true; this.autoWidth = this._autoWidth; this.autoHeight = this._autoHeight; this._noResize = false; const hp = this._element.pivot.x; const vp = this._element.pivot.y; const ha = this._alignment.x; const va = this._alignment.y; for(let i = 0; i < this._meshInfo.length; i++){ if (this._meshInfo[i].count === 0) continue; let prevQuad = 0; for(const line in this._meshInfo[i].lines){ const index = this._meshInfo[i].lines[line]; const lw = this._lineWidths[parseInt(line, 10)]; const hoffset = -hp * this._element.calculatedWidth + ha * (this._element.calculatedWidth - lw) * (this._rtl ? -1 : 1); const voffset = (1 - vp) * this._element.calculatedHeight - fontMaxY - (1 - va) * (this._element.calculatedHeight - this.height); for(let quad = prevQuad; quad <= index; quad++){ this._meshInfo[i].positions[quad * 4 * 3] += hoffset; this._meshInfo[i].positions[quad * 4 * 3 + 3] += hoffset; this._meshInfo[i].positions[quad * 4 * 3 + 6] += hoffset; this._meshInfo[i].positions[quad * 4 * 3 + 9] += hoffset; this._meshInfo[i].positions[quad * 4 * 3 + 1] += voffset; this._meshInfo[i].positions[quad * 4 * 3 + 4] += voffset; this._meshInfo[i].positions[quad * 4 * 3 + 7] += voffset; this._meshInfo[i].positions[quad * 4 * 3 + 10] += voffset; } if (this._rtl) { for(let quad = prevQuad; quad <= index; quad++){ const idx = quad * 4 * 3; for(let vert = 0; vert < 4; ++vert){ this._meshInfo[i].positions[idx + vert * 3] = this._element.calculatedWidth - this._meshInfo[i].positions[idx + vert * 3] + hoffset * 2; } const tmp0 = this._meshInfo[i].positions[idx + 3]; const tmp1 = this._meshInfo[i].positions[idx + 6]; this._meshInfo[i].positions[idx + 3] = this._meshInfo[i].positions[idx + 0]; this._meshInfo[i].positions[idx + 6] = this._meshInfo[i].positions[idx + 9]; this._meshInfo[i].positions[idx + 0] = tmp0; this._meshInfo[i].positions[idx + 9] = tmp1; } } prevQuad = index + 1; } const numVertices = this._meshInfo[i].count * 4; const vertMax = this._meshInfo[i].quad * 4; const it = new VertexIterator(this._meshInfo[i].meshInstance.mesh.vertexBuffer); for(let v = 0; v < numVertices; v++){ if (v >= vertMax) { it.element[SEMANTIC_POSITION].set(0, 0, 0); it.element[SEMANTIC_TEXCOORD0].set(0, 0); it.element[SEMANTIC_COLOR].set(0, 0, 0, 0); it.element[SEMANTIC_ATTR8].set(0, 0, 0, 0); it.element[SEMANTIC_ATTR9].set(0, 0, 0, 0); } else { it.element[SEMANTIC_POSITION].set(this._meshInfo[i].positions[v * 3 + 0], this._meshInfo[i].positions[v * 3 + 1], this._meshInfo[i].positions[v * 3 + 2]); it.element[SEMANTIC_TEXCOORD0].set(this._meshInfo[i].uvs[v * 2 + 0], this._meshInfo[i].uvs[v * 2 + 1]); it.element[SEMANTIC_COLOR].set(this._meshInfo[i].colors[v * 4 + 0], this._meshInfo[i].colors[v * 4 + 1], this._meshInfo[i].colors[v * 4 + 2], this._meshInfo[i].colors[v * 4 + 3]); it.element[SEMANTIC_ATTR8].set(this._meshInfo[i].outlines[v * 3 + 0], this._meshInfo[i].outlines[v * 3 + 1], this._meshInfo[i].outlines[v * 3 + 2]); it.element[SEMANTIC_ATTR9].set(this._meshInfo[i].shadows[v * 3 + 0], this._meshInfo[i].shadows[v * 3 + 1], this._meshInfo[i].shadows[v * 3 + 2]); } it.next(); } it.end(); this._meshInfo[i].meshInstance.mesh.aabb.compute(this._meshInfo[i].positions); this._meshInfo[i].meshInstance._aabbVer = -1; } this._aabbDirty = true; } _onFontRender() { this.font = this._font; } _onFontLoad(asset) { if (this.font !== asset.resource) { this.font = asset.resource; } } _onFontChange(asset, name, _new, _old) { if (name === 'data') { this._font.data = _new; const maps = this._font.data.info.maps.length; for(let i = 0; i < maps; i++){ if (!this._meshInfo[i]) continue; const mi = this._meshInfo[i].meshInstance; if (mi) { mi.setParameter('font_sdfIntensity', this._font.intensity); mi.setParameter('font_pxrange', this._getPxRange(this._font)); mi.setParameter('font_textureWidth', this._font.data.info.maps[i].width); } } } } _onFontRemove(asset) {} _setTextureParams(mi, texture) { if (this._font) { if (this._font.type === FONT_MSDF) { mi.deleteParameter('texture_emissiveMap'); mi.deleteParameter('texture_opacityMap'); mi.setParameter('texture_msdfMap', texture); } else if (this._font.type === FONT_BITMAP) { mi.deleteParameter('texture_msdfMap'); mi.setParameter('texture_emissiveMap', texture); mi.setParameter('texture_opacityMap', texture); } } } _getPxRange(font) { const keys = Object.keys(this._font.data.chars); for(let i = 0; i < keys.length; i++){ const char = this._font.data.chars[keys[i]]; if (char.range) { return (char.scale || 1) * char.range; } } return 2; } _getUv(char) { const data = this._font.data; if (!data.chars[char]) { const space = ' '; if (data.chars[space]) { return this._getUv(space); } return [ 0, 0, 0, 0 ]; } const map = data.chars[char].map; const width = data.info.maps[map].width; const height = data.info.maps[map].height; const x = data.chars[char].x; const y = data.chars[char].y; const x1 = x; const y1 = y; const x2 = x + data.chars[char].width; const y2 = y - data.chars[char].height; const edge = 1 - data.chars[char].height / height; return [ x1 / width, edge - y1 / height, x2 / width, edge - y2 / height ]; } onEnable() { this._fontAsset.autoLoad = true; if (this._model) { this._element.addModelToLayers(this._model); } } onDisable() { this._fontAsset.autoLoad = false; if (this._model) { this._element.removeModelFromLayers(this._model); } } _setStencil(stencilParams) { if (this._model) { const instances = this._model.meshInstances; for(let i = 0; i < instances.length; i++){ instances[i].stencilFront = stencilParams; instances[i].stencilBack = stencilParams; } } } _shouldAutoFitWidth() { return this._autoFitWidth && !this._autoWidth; } _shouldAutoFitHeight() { return this._autoFitHeight && !this._autoHeight; } _shouldAutoFit() { return this._autoFitWidth && !this._autoWidth || this._autoFitHeight && !this._autoHeight; } _calculateCharsPerTexture(symbolIndex) { const charactersPerTexture = {}; if (symbolIndex === undefined) { symbolIndex = this._symbols.length; } for(let i = 0, len = symbolIndex; i < len; i++){ const char = this._symbols[i]; let info = this._font.data.chars[char]; if (!info) { info = this._font.data.chars[' ']; if (!info) { info = this._font.data.chars[Object.keys(this._font.data.chars)[0]]; } } const map = info.map; if (!charactersPerTexture[map]) { charactersPerTexture[map] = 1; } else { charactersPerTexture[map]++; } } return charactersPerTexture; } _updateRenderRange() { const startChars = this._rangeStart === 0 ? 0 : this._calculateCharsPerTexture(this._rangeStart); const endChars = this._rangeEnd === 0 ? 0 : this._calculateCharsPerTexture(this._rangeEnd); for(let i = 0, len = this._meshInfo.length; i < len; i++){ const start = startChars[i] || 0; const end = endChars[i] || 0; const instance = this._meshInfo[i].meshInstance; if (instance) { const mesh = instance.mesh; if (mesh) { mesh.primitive[0].base = start * 3 * 2; mesh.primitive[0].count = (end - start) * 3 * 2; } } } } set text(value) { this._i18nKey = null; const str = value != null && value.toString() || ''; this._setText(str); } get text() { return this._text; } set key(value) { const str = value !== null ? value.toString() : null; if (this._i18nKey === str) { return; } this._i18nKey = str; if (str) { this._fontAsset.disableLocalization = false; this._resetLocalizedText(); } else { this._fontAsset.disableLocalization = true; } } get key() { return this._i18nKey; } set color(value) { const r = value.r; const g = value.g; const b = value.b; if (this._color.r === r && this._color.g === g && this._color.b === b) { return; } this._color.r = r; this._color.g = g; this._color.b = b; if (!this._model) { return; } if (this._symbolColors) { if (this._font) { this._updateText(); } } else { _tempColor.linear(this._color); this._colorUniform[0] = _tempColor.r; this._colorUniform[1] = _tempColor.g; this._colorUniform[2] = _tempColor.b; for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.setParameter('material_emissive', this._colorUniform); } } if (this._element) { this._element.fire('set:color', this._color); } } get color() { return this._color; } set opacity(value) { if (this._color.a !== value) { this._color.a = value; if (this._model) { for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.setParameter('material_opacity', value); } } } if (this._element) { this._element.fire('set:opacity', value); } } get opacity() { return this._color.a; } set lineHeight(value) { const _prev = this._lineHeight; this._lineHeight = value; this._scaledLineHeight = value; if (_prev !== value && this._font) { this._updateText(); } } get lineHeight() { return this._lineHeight; } set wrapLines(value) { const _prev = this._wrapLines; this._wrapLines = value; if (_prev !== value && this._font) { this._updateText(); } } get wrapLines() { return this._wrapLines; } get lines() { return this._lineContents; } set spacing(value) { const _prev = this._spacing; this._spacing = value; if (_prev !== value && this._font) { this._updateText(); } } get spacing() { return this._spacing; } set fontSize(value) { const _prev = this._fontSize; this._fontSize = value; this._originalFontSize = value; if (_prev !== value && this._font) { this._updateText(); } } get fontSize() { return this._fontSize; } set fontAsset(value) { this._fontAsset.defaultAsset = value; } get fontAsset() { return this._fontAsset.localizedAsset; } set font(value) { let previousFontType; if (this._font) { previousFontType = this._font.type; if (this._font.off) this._font.off('render', this._onFontRender, this); } this._font = value; this._fontMinY = 0; this._fontMaxY = 0; if (!value) return; const json = this._font.data; for(const charId in json.chars){ const data = json.chars[charId]; if (data.bounds) { this._fontMinY = Math.min(this._fontMinY, data.bounds[1]); this._fontMaxY = Math.max(this._fontMaxY, data.bounds[3]); } } if (this._font.on) this._font.on('render', this._onFontRender, this); if (this._fontAsset.localizedAsset) { const asset = this._system.app.assets.get(this._fontAsset.localizedAsset); if (asset.resource !== this._font) { this._fontAsset.defaultAsset = null; } } if (value.type !== previousFontType) { const screenSpace = this._element._isScreenSpace(); this._updateMaterial(screenSpace); } for(let i = 0, len = this._font.textures.length; i < len; i++){ if (!this._meshInfo[i]) { this._meshInfo[i] = new MeshInfo(); } else { const mi = this._meshInfo[i].meshInstance; if (mi) { mi.setParameter('font_sdfIntensity', this._font.intensity); mi.setParameter('font_pxrange', this._getPxRange(this._font)); mi.setParameter('font_textureWidth', this._font.data.info.maps[i].width); this._setTextureParams(mi, this._font.textures[i]); } } } let removedModel = false; for(let i = this._font.textures.length; i < this._meshInfo.length; i++){ if (this._meshInfo[i].meshInstance) { if (!removedModel) { this._element.removeModelFromLayers(this._model); removedModel = true; } this._removeMeshInstance(this._meshInfo[i].meshInstance); } } if (this._meshInfo.length > this._font.textures.length) { this._meshInfo.length = this._font.textures.length; } this._updateText(); } get font() { return this._font; } set alignment(value) { if (value instanceof Vec2) { this._alignment.set(value.x, value.y); } else { this._alignment.set(value[0], value[1]); } if (this._font) { this._updateText(); } } get alignment() { return this._alignment; } set autoWidth(value) { const old = this._autoWidth; this._autoWidth = value; if (value && Math.abs(this._element.anchor.x - this._element.anchor.z) < 0.0001) { this._element.width = this.width; } if (old !== value) { const newFontSize = this._shouldAutoFit() ? this._maxFontSize : this._originalFontSize; if (newFontSize !== this._fontSize) { this._fontSize = newFontSize; if (this._font) { this._updateText(); } } } } get autoWidth() { return this._autoWidth; } set autoHeight(value) { const old = this._autoHeight; this._autoHeight = value; if (value && Math.abs(this._element.anchor.y - this._element.anchor.w) < 0.0001) { this._element.height = this.height; } if (old !== value) { const newFontSize = this._shouldAutoFit() ? this._maxFontSize : this._originalFontSize; if (newFontSize !== this._fontSize) { this._fontSize = newFontSize; if (this._font) { this._updateText(); } } } } get autoHeight() { return this._autoHeight; } set rtlReorder(value) { if (this._rtlReorder !== value) { this._rtlReorder = value; if (this._font) { this._updateText(); } } } get rtlReorder() { return this._rtlReorder; } set unicodeConverter(value) { if (this._unicodeConverter !== value) { this._unicodeConverter = value; this._setText(this._text); } } get unicodeConverter() { return this._unicodeConverter; } get aabb() { if (this._aabbDirty) { let initialized = false; for(let i = 0; i < this._meshInfo.length; i++){ if (!this._meshInfo[i].meshInstance) continue; if (!initialized) { this._aabb.copy(this._meshInfo[i].meshInstance.aabb); initialized = true; } else { this._aabb.add(this._meshInfo[i].meshInstance.aabb); } } this._aabbDirty = false; } return this._aabb; } set outlineColor(value) { const r = value instanceof Color ? value.r : value[0]; const g = value instanceof Color ? value.g : value[1]; const b = value instanceof Color ? value.b : value[2]; const a = value instanceof Color ? value.a : value[3]; if (this._outlineColor.r === r && this._outlineColor.g === g && this._outlineColor.b === b && this._outlineColor.a === a) { return; } this._outlineColor.r = r; this._outlineColor.g = g; this._outlineColor.b = b; this._outlineColor.a = a; if (!this._model) { return; } if (this._symbolOutlineParams) { if (this._font) { this._updateText(); } } else { _tempColor.linear(this._outlineColor); this._outlineColorUniform[0] = _tempColor.r; this._outlineColorUniform[1] = _tempColor.g; this._outlineColorUniform[2] = _tempColor.b; this._outlineColorUniform[3] = _tempColor.a; for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.setParameter('outline_color', this._outlineColorUniform); } } if (this._element) { this._element.fire('set:outline', this._color); } } get outlineColor() { return this._outlineColor; } set outlineThickness(value) { const _prev = this._outlineThickness; this._outlineThickness = value; if (_prev !== value && this._font) { if (!this._model) { return; } if (this._symbolOutlineParams) { if (this._font) { this._updateText(); } } else { for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.setParameter('outline_thickness', this._outlineThicknessScale * this._outlineThickness); } } } } get outlineThickness() { return this._outlineThickness; } set shadowColor(value) { const r = value instanceof Color ? value.r : value[0]; const g = value instanceof Color ? value.g : value[1]; const b = value instanceof Color ? value.b : value[2]; const a = value instanceof Color ? value.a : value[3]; if (this._shadowColor.r === r && this._shadowColor.g === g && this._shadowColor.b === b && this._shadowColor.a === a) { return; } this._shadowColor.r = r; this._shadowColor.g = g; this._shadowColor.b = b; this._shadowColor.a = a; if (!this._model) { return; } if (this._symbolShadowParams) { if (this._font) { this._updateText(); } } else { _tempColor.linear(this._shadowColor); this._shadowColorUniform[0] = _tempColor.r; this._shadowColorUniform[1] = _tempColor.g; this._shadowColorUniform[2] = _tempColor.b; this._shadowColorUniform[3] = _tempColor.a; for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const mi = this._model.meshInstances[i]; mi.setParameter('shadow_color', this._shadowColorUniform); } } } get shadowColor() { return this._shadowColor; } set shadowOffset(value) { const x = value instanceof Vec2 ? value.x : value[0], y = value instanceof Vec2 ? value.y : value[1]; if (this._shadowOffset.x === x && this._shadowOffset.y === y) { return; } this._shadowOffset.set(x, y); if (this._font && this._model) { if (this._symbolShadowParams) { this._updateText(); } else { for(let i = 0, len = this._model.meshInstances.length; i < len; i++){ const ratio = -this._font.data.info.maps[i].width / this._font.data.info.maps[i].height; this._shadowOffsetUniform[0] = this._shadowOffsetScale * this._shadowOffset.x; this._shadowOffsetUniform[1] = ratio * this._shadowOffsetScale * this._shadowOffset.y; const mi = this._model.meshInstances[i]; mi.setParameter('shadow_offset', this._shadowOffsetUniform); } } } } get shadowOffset() { return this._shadowOffset; } set minFontSize(value) { if (this._minFontSize === value) return; this._minFontSize = value; if (this.font && this._shouldAutoFit()) { this._updateText(); } } get minFontSize() { return this._minFontSize; } set maxFontSize(value) { if (this._maxFontSize === value) return; this._maxFontSize = value; if (this.font && this._shouldAutoFit()) { this._updateText(); } } get maxFontSize() { return this._maxFontSize; } set autoFitWidth(value) { if (this._autoFitWidth === value) return; this._autoFitWidth = value; this._fontSize = this._shouldAutoFit() ? this._maxFontSize : this._originalFontSize; if (this.font) { this._updateText(); } } get autoFitWidth() { return this._autoFitWidth; } set autoFitHeight(value) { if (this._autoFitHeight === value) return; this._autoFitHeight = value; this._fontSize = this._shouldAutoFit() ? this._maxFontSize : this._originalFontSize; if (this.font) { this._updateText(); } } get autoFitHeight() { return this._autoFitHeight; } set maxLines(value) { if (this._maxLines === value) return; if (value === null && this._maxLines === -1) return; this._maxLines = value === null ? -1 : value; if (this.font && this._wrapLines) { this._updateText(); } } get maxLines() { return this._maxLines; } set enableMarkup(value) { value = !!value; if (this._enableMarkup === value) return; this._enableMarkup = value; if (this.font) { this._updateText(); } const screenSpace = this._element._isScreenSpace(); this._updateMaterial(screenSpace); } get enableMarkup() { return this._enableMarkup; } get symbols() { return this._symbols; } get symbolColors() { if (this._symbolColors === null) { return null; } return this._symbolColors.map(function(c) { return this._colorPalette.slice(c * 3, c * 3 + 3); }, this); } get symbolOutlineParams() { if (this._symbolOutlineParams === null) { return null; } return this._symbolOutlineParams.map(function(paramId) { return this._outlinePalette.slice(paramId * 5, paramId * 5 + 5); }, this); } get symbolShadowParams() { if (this._symbolShadowParams === null) { return null; } return this._symbolShadowParams.map(function(paramId) { return this._shadowPalette.slice(paramId * 6, paramId * 6 + 6); }, this); } get rtl() { return this._rtl; } set rangeStart(rangeStart) { rangeStart = Math.max(0, Math.min(rangeStart, this._symbols.length)); if (rangeStart !== this._rangeStart) { this._rangeStart = rangeStart; this._updateRenderRange(); } } get rangeStart() { return this._rangeStart; } set rangeEnd(rangeEnd) { rangeEnd = Math.max(this._rangeStart, Math.min(rangeEnd, this._symbols.length)); if (rangeEnd !== this._rangeEnd) { this._rangeEnd = rangeEnd; this._updateRenderRange(); } } get rangeEnd() { return this._rangeEnd; } constructor(element){ this._element = element; this._system = element.system; this._entity = element.entity; this._text = ''; this._symbols = []; this._colorPalette = []; this._outlinePalette = []; this._shadowPalette = []; this._symbolColors = null; this._symbolOutlineParams = null; this._symbolShadowParams = null; this._i18nKey = null; this._fontAsset = new LocalizedAsset(this._system.app); this._fontAsset.disableLocalization = true; this._fontAsset.on('load', this._onFontLoad, this); this._fontAsset.on('change', this._onFontChange, this); this._fontAsset.on('remove', this._onFontRemove, this); this._font = null; this._color = new Color(1, 1, 1, 1); this._colorUniform = new Float32Array(3); this._spacing = 1; this._fontSize = 32; this._fontMinY = 0; this._fontMaxY = 0; this._originalFontSize = 32; this._maxFontSize = 32; this._minFontSize = 8; this._autoFitWidth = false; this._autoFitHeight = false; this._maxLines = -1; this._lineHeight = 32; this._scaledLineHeight = 32; this._wrapLines = false; this._drawOrder = 0; this._alignment = new Vec2(0.5, 0.5); this._autoWidth = true; this._autoHeight = true; this.width = 0; this.height = 0; this._node = new GraphNode(); this._model = new Model(); this._model.graph = this._node; this._entity.addChild(this._node); this._meshInfo = []; this._material = null; this._aabbDirty = true; this._aabb = new BoundingBox(); this._noResize = false; this._currentMaterialType = null; this._maskedMaterialSrc = null; this._rtlReorder = false; this._unicodeConverter = false; this._rtl = false; this._outlineColor = new Color(0, 0, 0, 1); this._outlineColorUniform = new Float32Array(4); this._outlineThicknessScale = 0.2; this._outlineThickness = 0.0; this._shadowColor = new Color(0, 0, 0, 1); this._shadowColorUniform = new Float32Array(4); this._shadowOffsetScale = 0.005; this._shadowOffset = new Vec2(0, 0); this._shadowOffsetUniform = new Float32Array(2); this._enableMarkup = false; this._onScreenChange(this._element.screen); element.on('resize', this._onParentResize, this); element.on('set:screen', this._onScreenChange, this); element.on('screen:set:screenspace', this._onScreenSpaceChange, this); element.on('set:draworder', this._onDrawOrderChange, this); element.on('set:pivot', this._onPivotChange, this); this._system.app.i18n.on(I18n.EVENT_CHANGE, this._onLocaleSet, this); this._system.app.i18n.on('data:add', this._onLocalizationData, this); this._system.app.i18n.on('data:remove', this._onLocalizationData, this); this._rangeStart = 0; this._rangeEnd = 0; if (!element._beingInitialized && element.enabled && element.entity.enabled) { this.onEnable(); } } } const position = new Vec3(); const invParentWtm = new Mat4(); const vecA$1 = new Vec3(); const vecB$1 = new Vec3(); const matA = new Mat4(); const matB = new Mat4(); const matC = new Mat4(); const matD = new Mat4(); class ElementComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(value) { const data = this.data; const oldValue = data.enabled; data.enabled = value; this.fire('set', 'enabled', oldValue, value); } get enabled() { return this.data.enabled; } get _absLeft() { return this._localAnchor.x + this._margin.x; } get _absRight() { return this._localAnchor.z - this._margin.z; } get _absTop() { return this._localAnchor.w - this._margin.w; } get _absBottom() { return this._localAnchor.y + this._margin.y; } get _hasSplitAnchorsX() { return Math.abs(this._anchor.x - this._anchor.z) > 0.001; } get _hasSplitAnchorsY() { return Math.abs(this._anchor.y - this._anchor.w) > 0.001; } get aabb() { if (this._image) { return this._image.aabb; } if (this._text) { return this._text.aabb; } return null; } set anchor(value) { if (value instanceof Vec4) { this._anchor.copy(value); } else { this._anchor.set(...value); } if (!this.entity._parent && !this.screen) { this._calculateLocalAnchors(); } else { this._calculateSize(this._hasSplitAnchorsX, this._hasSplitAnchorsY); } this._anchorDirty = true; if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:anchor', this._anchor); } get anchor() { return this._anchor; } set batchGroupId(value) { if (this._batchGroupId === value) { return; } if (this.entity.enabled && this._batchGroupId >= 0) { this.system.app.batcher?.remove(BatchGroup.ELEMENT, this.batchGroupId, this.entity); } if (this.entity.enabled && value >= 0) { this.system.app.batcher?.insert(BatchGroup.ELEMENT, value, this.entity); } if (value < 0 && this._batchGroupId >= 0 && this.enabled && this.entity.enabled) { if (this._image && this._image._renderable.model) { this.addModelToLayers(this._image._renderable.model); } else if (this._text && this._text._model) { this.addModelToLayers(this._text._model); } } this._batchGroupId = value; } get batchGroupId() { return this._batchGroupId; } set bottom(value) { this._margin.y = value; const p = this.entity.getLocalPosition(); const wt = this._absTop; const wb = this._localAnchor.y + value; this._setHeight(wt - wb); p.y = value + this._calculatedHeight * this._pivot.y; this.entity.setLocalPosition(p); } get bottom() { return this._margin.y; } set calculatedWidth(value) { this._setCalculatedWidth(value, true); } get calculatedWidth() { return this._calculatedWidth; } set calculatedHeight(value) { this._setCalculatedHeight(value, true); } get calculatedHeight() { return this._calculatedHeight; } get canvasCorners() { if (!this._canvasCornersDirty || !this.screen || !this.screen.screen.screenSpace) { return this._canvasCorners; } const device = this.system.app.graphicsDevice; const screenCorners = this.screenCorners; const sx = device.canvas.clientWidth / device.width; const sy = device.canvas.clientHeight / device.height; for(let i = 0; i < 4; i++){ this._canvasCorners[i].set(screenCorners[i].x * sx, (device.height - screenCorners[i].y) * sy); } this._canvasCornersDirty = false; return this._canvasCorners; } set drawOrder(value) { let priority = 0; if (this.screen) { priority = this.screen.screen.priority; } if (value > 0xFFFFFF) { value = 0xFFFFFF; } this._drawOrder = (priority << 24) + value; this.fire('set:draworder', this._drawOrder); } get drawOrder() { return this._drawOrder; } set height(value) { this._height = value; if (!this._hasSplitAnchorsY) { this._setCalculatedHeight(value, true); } this.fire('set:height', this._height); } get height() { return this._height; } set layers(value) { if (this._addedModels.length) { for(let i = 0; i < this._layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this._layers[i]); if (layer) { for(let j = 0; j < this._addedModels.length; j++){ layer.removeMeshInstances(this._addedModels[j].meshInstances); } } } } this._layers = value; if (!this.enabled || !this.entity.enabled || !this._addedModels.length) { return; } for(let i = 0; i < this._layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this._layers[i]); if (layer) { for(let j = 0; j < this._addedModels.length; j++){ layer.addMeshInstances(this._addedModels[j].meshInstances); } } } } get layers() { return this._layers; } set left(value) { this._margin.x = value; const p = this.entity.getLocalPosition(); const wr = this._absRight; const wl = this._localAnchor.x + value; this._setWidth(wr - wl); p.x = value + this._calculatedWidth * this._pivot.x; this.entity.setLocalPosition(p); } get left() { return this._margin.x; } set margin(value) { this._margin.copy(value); this._calculateSize(true, true); this.fire('set:margin', this._margin); } get margin() { return this._margin; } get maskedBy() { return this._maskedBy; } set pivot(value) { const { pivot, margin } = this; const prevX = pivot.x; const prevY = pivot.y; if (value instanceof Vec2) { pivot.copy(value); } else { pivot.set(...value); } const mx = margin.x + margin.z; const dx = pivot.x - prevX; margin.x += mx * dx; margin.z -= mx * dx; const my = margin.y + margin.w; const dy = pivot.y - prevY; margin.y += my * dy; margin.w -= my * dy; this._anchorDirty = true; this._cornersDirty = true; this._worldCornersDirty = true; this._calculateSize(false, false); this._flagChildrenAsDirty(); this.fire('set:pivot', pivot); } get pivot() { return this._pivot; } set right(value) { this._margin.z = value; const p = this.entity.getLocalPosition(); const wl = this._absLeft; const wr = this._localAnchor.z - value; this._setWidth(wr - wl); p.x = this._localAnchor.z - this._localAnchor.x - value - this._calculatedWidth * (1 - this._pivot.x); this.entity.setLocalPosition(p); } get right() { return this._margin.z; } get screenCorners() { if (!this._cornersDirty || !this.screen) { return this._screenCorners; } const parentBottomLeft = this.entity.parent && this.entity.parent.element && this.entity.parent.element.screenCorners[0]; this._screenCorners[0].set(this._absLeft, this._absBottom, 0); this._screenCorners[1].set(this._absRight, this._absBottom, 0); this._screenCorners[2].set(this._absRight, this._absTop, 0); this._screenCorners[3].set(this._absLeft, this._absTop, 0); const screenSpace = this.screen.screen.screenSpace; for(let i = 0; i < 4; i++){ this._screenTransform.transformPoint(this._screenCorners[i], this._screenCorners[i]); if (screenSpace) { this._screenCorners[i].mulScalar(this.screen.screen.scale); } if (parentBottomLeft) { this._screenCorners[i].add(parentBottomLeft); } } this._cornersDirty = false; this._canvasCornersDirty = true; this._worldCornersDirty = true; return this._screenCorners; } get textWidth() { return this._text ? this._text.width : 0; } get textHeight() { return this._text ? this._text.height : 0; } set top(value) { this._margin.w = value; const p = this.entity.getLocalPosition(); const wb = this._absBottom; const wt = this._localAnchor.w - value; this._setHeight(wt - wb); p.y = this._localAnchor.w - this._localAnchor.y - value - this._calculatedHeight * (1 - this._pivot.y); this.entity.setLocalPosition(p); } get top() { return this._margin.w; } set type(value) { if (value !== this._type) { this._type = value; if (this._image) { this._image.destroy(); this._image = null; } if (this._text) { this._text.destroy(); this._text = null; } if (value === ELEMENTTYPE_IMAGE) { this._image = new ImageElement(this); } else if (value === ELEMENTTYPE_TEXT) { this._text = new TextElement(this); } } } get type() { return this._type; } set useInput(value) { if (this._useInput === value) { return; } this._useInput = value; if (this.system.app.elementInput) { if (value) { if (this.enabled && this.entity.enabled) { this.system.app.elementInput.addElement(this); } } else { this.system.app.elementInput.removeElement(this); } } else { if (this._useInput === true) ; } this.fire('set:useInput', value); } get useInput() { return this._useInput; } set fitMode(value) { this._fitMode = value; this._calculateSize(true, true); if (this._image) { this._image.refreshMesh(); } } get fitMode() { return this._fitMode; } set width(value) { this._width = value; if (!this._hasSplitAnchorsX) { this._setCalculatedWidth(value, true); } this.fire('set:width', this._width); } get width() { return this._width; } get worldCorners() { if (!this._worldCornersDirty) { return this._worldCorners; } if (this.screen) { const screenCorners = this.screenCorners; if (!this.screen.screen.screenSpace) { matA.copy(this.screen.screen._screenMatrix); matA.data[13] = -matA.data[13]; matA.mul2(this.screen.getWorldTransform(), matA); for(let i = 0; i < 4; i++){ matA.transformPoint(screenCorners[i], this._worldCorners[i]); } } } else { const localPos = this.entity.getLocalPosition(); matA.setTranslate(-localPos.x, -localPos.y, -localPos.z); matB.setTRS(Vec3.ZERO, this.entity.getLocalRotation(), this.entity.getLocalScale()); matC.setTranslate(localPos.x, localPos.y, localPos.z); const entity = this.entity.parent ? this.entity.parent : this.entity; matD.copy(entity.getWorldTransform()); matD.mul(matC).mul(matB).mul(matA); vecA$1.set(localPos.x - this.pivot.x * this.calculatedWidth, localPos.y - this.pivot.y * this.calculatedHeight, localPos.z); matD.transformPoint(vecA$1, this._worldCorners[0]); vecA$1.set(localPos.x + (1 - this.pivot.x) * this.calculatedWidth, localPos.y - this.pivot.y * this.calculatedHeight, localPos.z); matD.transformPoint(vecA$1, this._worldCorners[1]); vecA$1.set(localPos.x + (1 - this.pivot.x) * this.calculatedWidth, localPos.y + (1 - this.pivot.y) * this.calculatedHeight, localPos.z); matD.transformPoint(vecA$1, this._worldCorners[2]); vecA$1.set(localPos.x - this.pivot.x * this.calculatedWidth, localPos.y + (1 - this.pivot.y) * this.calculatedHeight, localPos.z); matD.transformPoint(vecA$1, this._worldCorners[3]); } this._worldCornersDirty = false; return this._worldCorners; } set fontSize(arg) { this._setValue('fontSize', arg); } get fontSize() { if (this._text) { return this._text.fontSize; } return null; } set minFontSize(arg) { this._setValue('minFontSize', arg); } get minFontSize() { if (this._text) { return this._text.minFontSize; } return null; } set maxFontSize(arg) { this._setValue('maxFontSize', arg); } get maxFontSize() { if (this._text) { return this._text.maxFontSize; } return null; } set maxLines(arg) { this._setValue('maxLines', arg); } get maxLines() { if (this._text) { return this._text.maxLines; } return null; } set autoFitWidth(arg) { this._setValue('autoFitWidth', arg); } get autoFitWidth() { if (this._text) { return this._text.autoFitWidth; } return null; } set autoFitHeight(arg) { this._setValue('autoFitHeight', arg); } get autoFitHeight() { if (this._text) { return this._text.autoFitHeight; } return null; } set color(arg) { this._setValue('color', arg); } get color() { if (this._text) { return this._text.color; } if (this._image) { return this._image.color; } return null; } set font(arg) { this._setValue('font', arg); } get font() { if (this._text) { return this._text.font; } return null; } set fontAsset(arg) { this._setValue('fontAsset', arg); } get fontAsset() { if (this._text && typeof this._text.fontAsset === 'number') { return this._text.fontAsset; } return null; } set spacing(arg) { this._setValue('spacing', arg); } get spacing() { if (this._text) { return this._text.spacing; } return null; } set lineHeight(arg) { this._setValue('lineHeight', arg); } get lineHeight() { if (this._text) { return this._text.lineHeight; } return null; } set wrapLines(arg) { this._setValue('wrapLines', arg); } get wrapLines() { if (this._text) { return this._text.wrapLines; } return null; } set lines(arg) { this._setValue('lines', arg); } get lines() { if (this._text) { return this._text.lines; } return null; } set alignment(arg) { this._setValue('alignment', arg); } get alignment() { if (this._text) { return this._text.alignment; } return null; } set autoWidth(arg) { this._setValue('autoWidth', arg); } get autoWidth() { if (this._text) { return this._text.autoWidth; } return null; } set autoHeight(arg) { this._setValue('autoHeight', arg); } get autoHeight() { if (this._text) { return this._text.autoHeight; } return null; } set rtlReorder(arg) { this._setValue('rtlReorder', arg); } get rtlReorder() { if (this._text) { return this._text.rtlReorder; } return null; } set unicodeConverter(arg) { this._setValue('unicodeConverter', arg); } get unicodeConverter() { if (this._text) { return this._text.unicodeConverter; } return null; } set text(arg) { this._setValue('text', arg); } get text() { if (this._text) { return this._text.text; } return null; } set key(arg) { this._setValue('key', arg); } get key() { if (this._text) { return this._text.key; } return null; } set texture(arg) { this._setValue('texture', arg); } get texture() { if (this._image) { return this._image.texture; } return null; } set textureAsset(arg) { this._setValue('textureAsset', arg); } get textureAsset() { if (this._image) { return this._image.textureAsset; } return null; } set material(arg) { this._setValue('material', arg); } get material() { if (this._image) { return this._image.material; } return null; } set materialAsset(arg) { this._setValue('materialAsset', arg); } get materialAsset() { if (this._image) { return this._image.materialAsset; } return null; } set sprite(arg) { this._setValue('sprite', arg); } get sprite() { if (this._image) { return this._image.sprite; } return null; } set spriteAsset(arg) { this._setValue('spriteAsset', arg); } get spriteAsset() { if (this._image) { return this._image.spriteAsset; } return null; } set spriteFrame(arg) { this._setValue('spriteFrame', arg); } get spriteFrame() { if (this._image) { return this._image.spriteFrame; } return null; } set pixelsPerUnit(arg) { this._setValue('pixelsPerUnit', arg); } get pixelsPerUnit() { if (this._image) { return this._image.pixelsPerUnit; } return null; } set opacity(arg) { this._setValue('opacity', arg); } get opacity() { if (this._text) { return this._text.opacity; } if (this._image) { return this._image.opacity; } return null; } set rect(arg) { this._setValue('rect', arg); } get rect() { if (this._image) { return this._image.rect; } return null; } set mask(arg) { this._setValue('mask', arg); } get mask() { if (this._image) { return this._image.mask; } return null; } set outlineColor(arg) { this._setValue('outlineColor', arg); } get outlineColor() { if (this._text) { return this._text.outlineColor; } return null; } set outlineThickness(arg) { this._setValue('outlineThickness', arg); } get outlineThickness() { if (this._text) { return this._text.outlineThickness; } return null; } set shadowColor(arg) { this._setValue('shadowColor', arg); } get shadowColor() { if (this._text) { return this._text.shadowColor; } return null; } set shadowOffset(arg) { this._setValue('shadowOffset', arg); } get shadowOffset() { if (this._text) { return this._text.shadowOffset; } return null; } set enableMarkup(arg) { this._setValue('enableMarkup', arg); } get enableMarkup() { if (this._text) { return this._text.enableMarkup; } return null; } set rangeStart(arg) { this._setValue('rangeStart', arg); } get rangeStart() { if (this._text) { return this._text.rangeStart; } return null; } set rangeEnd(arg) { this._setValue('rangeEnd', arg); } get rangeEnd() { if (this._text) { return this._text.rangeEnd; } return null; } _setValue(name, value) { if (this._text) { if (this._text[name] !== value) { this._dirtyBatch(); } this._text[name] = value; } else if (this._image) { if (this._image[name] !== value) { this._dirtyBatch(); } this._image[name] = value; } } _patch() { this.entity._sync = this._sync; this.entity.setPosition = this._setPosition; this.entity.setLocalPosition = this._setLocalPosition; } _unpatch() { this.entity._sync = Entity.prototype._sync; this.entity.setPosition = Entity.prototype.setPosition; this.entity.setLocalPosition = Entity.prototype.setLocalPosition; } _setPosition(x, y, z) { if (!this.element.screen) { Entity.prototype.setPosition.call(this, x, y, z); return; } if (x instanceof Vec3) { position.copy(x); } else { position.set(x, y, z); } this.getWorldTransform(); invParentWtm.copy(this.element._screenToWorld).invert(); invParentWtm.transformPoint(position, this.localPosition); if (!this._dirtyLocal) { this._dirtifyLocal(); } } _setLocalPosition(x, y, z) { if (x instanceof Vec3) { this.localPosition.copy(x); } else { this.localPosition.set(x, y, z); } const element = this.element; const p = this.localPosition; const pvt = element._pivot; element._margin.x = p.x - element._calculatedWidth * pvt.x; element._margin.z = element._localAnchor.z - element._localAnchor.x - element._calculatedWidth - element._margin.x; element._margin.y = p.y - element._calculatedHeight * pvt.y; element._margin.w = element._localAnchor.w - element._localAnchor.y - element._calculatedHeight - element._margin.y; if (!this._dirtyLocal) { this._dirtifyLocal(); } } _sync() { const element = this.element; const screen = element.screen; if (screen) { if (element._anchorDirty) { let resx = 0; let resy = 0; let px = 0; let py = 1; if (this._parent && this._parent.element) { resx = this._parent.element.calculatedWidth; resy = this._parent.element.calculatedHeight; px = this._parent.element.pivot.x; py = this._parent.element.pivot.y; } else { const resolution = screen.screen.resolution; resx = resolution.x / screen.screen.scale; resy = resolution.y / screen.screen.scale; } element._anchorTransform.setTranslate(resx * (element.anchor.x - px), -(resy * (py - element.anchor.y)), 0); element._anchorDirty = false; element._calculateLocalAnchors(); } if (element._sizeDirty) { element._calculateSize(false, false); } } if (this._dirtyLocal) { this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale); const p = this.localPosition; const pvt = element._pivot; element._margin.x = p.x - element._calculatedWidth * pvt.x; element._margin.z = element._localAnchor.z - element._localAnchor.x - element._calculatedWidth - element._margin.x; element._margin.y = p.y - element._calculatedHeight * pvt.y; element._margin.w = element._localAnchor.w - element._localAnchor.y - element._calculatedHeight - element._margin.y; this._dirtyLocal = false; } if (!screen) { if (this._dirtyWorld) { element._cornersDirty = true; element._canvasCornersDirty = true; element._worldCornersDirty = true; } Entity.prototype._sync.call(this); return; } if (this._dirtyWorld) { if (this._parent === null) { this.worldTransform.copy(this.localTransform); } else { if (this._parent.element) { element._screenToWorld.mul2(this._parent.element._modelTransform, element._anchorTransform); } else { element._screenToWorld.copy(element._anchorTransform); } element._modelTransform.mul2(element._screenToWorld, this.localTransform); if (screen) { element._screenToWorld.mul2(screen.screen._screenMatrix, element._screenToWorld); if (!screen.screen.screenSpace) { element._screenToWorld.mul2(screen.worldTransform, element._screenToWorld); } this.worldTransform.mul2(element._screenToWorld, this.localTransform); const parentWorldTransform = element._parentWorldTransform; parentWorldTransform.setIdentity(); const parent = this._parent; if (parent && parent.element && parent !== screen) { matA.setTRS(Vec3.ZERO, parent.getLocalRotation(), parent.getLocalScale()); parentWorldTransform.mul2(parent.element._parentWorldTransform, matA); } const depthOffset = vecA$1; depthOffset.set(0, 0, this.localPosition.z); const pivotOffset = vecB$1; pivotOffset.set(element._absLeft + element._pivot.x * element.calculatedWidth, element._absBottom + element._pivot.y * element.calculatedHeight, 0); matA.setTranslate(-pivotOffset.x, -pivotOffset.y, -pivotOffset.z); matB.setTRS(depthOffset, this.getLocalRotation(), this.getLocalScale()); matC.setTranslate(pivotOffset.x, pivotOffset.y, pivotOffset.z); element._screenTransform.mul2(element._parentWorldTransform, matC).mul(matB).mul(matA); element._cornersDirty = true; element._canvasCornersDirty = true; element._worldCornersDirty = true; } else { this.worldTransform.copy(element._modelTransform); } } this._dirtyWorld = false; } } _onInsert(parent) { const result = this._parseUpToScreen(); this.entity._dirtifyWorld(); this._updateScreen(result.screen); this._dirtifyMask(); } _dirtifyMask() { let current = this.entity; while(current){ const next = current.parent; if ((next === null || next.screen) && current.element) { if (!this.system._prerender || !this.system._prerender.length) { this.system._prerender = []; this.system.app.once('prerender', this._onPrerender, this); } const i = this.system._prerender.indexOf(this.entity); if (i >= 0) { this.system._prerender.splice(i, 1); } const j = this.system._prerender.indexOf(current); if (j < 0) { this.system._prerender.push(current); } } current = next; } } _onPrerender() { for(let i = 0; i < this.system._prerender.length; i++){ const mask = this.system._prerender[i]; if (mask.element) { const depth = 1; mask.element.syncMask(depth); } } this.system._prerender.length = 0; } _bindScreen(screen) { screen._bindElement(this); } _unbindScreen(screen) { screen._unbindElement(this); } _updateScreen(screen) { if (this.screen && this.screen !== screen) { this._unbindScreen(this.screen.screen); } const previousScreen = this.screen; this.screen = screen; if (this.screen) { this._bindScreen(this.screen.screen); } this._calculateSize(this._hasSplitAnchorsX, this._hasSplitAnchorsY); this.fire('set:screen', this.screen, previousScreen); this._anchorDirty = true; const children = this.entity.children; for(let i = 0, l = children.length; i < l; i++){ if (children[i].element) { children[i].element._updateScreen(screen); } } if (this.screen) { this.screen.screen.syncDrawOrder(); } } syncMask(depth) { const result = this._parseUpToScreen(); this._updateMask(result.mask, depth); } _setMaskedBy(mask) { const renderableElement = this._image || this._text; if (mask) { const ref = mask.element._image._maskRef; renderableElement?._setStencil(new StencilParameters({ ref: ref, func: FUNC_EQUAL })); this._maskedBy = mask; } else { renderableElement?._setStencil(null); this._maskedBy = null; } } _updateMask(currentMask, depth) { if (currentMask) { this._setMaskedBy(currentMask); if (this.mask) { const ref = currentMask.element._image._maskRef; const sp = new StencilParameters({ ref: ref, func: FUNC_EQUAL, zpass: STENCILOP_INCREMENT }); this._image._setStencil(sp); this._image._maskRef = depth; depth++; currentMask = this.entity; } const children = this.entity.children; for(let i = 0, l = children.length; i < l; i++){ children[i].element?._updateMask(currentMask, depth); } if (this.mask) depth--; } else { this._setMaskedBy(null); if (this.mask) { const sp = new StencilParameters({ ref: depth, func: FUNC_ALWAYS, zpass: STENCILOP_REPLACE }); this._image._setStencil(sp); this._image._maskRef = depth; depth++; currentMask = this.entity; } const children = this.entity.children; for(let i = 0, l = children.length; i < l; i++){ children[i].element?._updateMask(currentMask, depth); } if (this.mask) { depth--; } } } _parseUpToScreen() { const result = { screen: null, mask: null }; let parent = this.entity._parent; while(parent && !parent.screen){ if (parent.element && parent.element.mask) { if (!result.mask) result.mask = parent; } parent = parent.parent; } if (parent && parent.screen) { result.screen = parent; } return result; } _onScreenResize(res) { this._anchorDirty = true; this._cornersDirty = true; this._worldCornersDirty = true; this._calculateSize(this._hasSplitAnchorsX, this._hasSplitAnchorsY); this.fire('screen:set:resolution', res); } _onScreenSpaceChange() { this.fire('screen:set:screenspace', this.screen.screen.screenSpace); } _onScreenRemove() { if (this.screen) { if (this.screen._destroying) { this.screen = null; } else { this._updateScreen(null); } } } _calculateLocalAnchors() { let resx = 1000; let resy = 1000; const parent = this.entity._parent; if (parent && parent.element) { resx = parent.element.calculatedWidth; resy = parent.element.calculatedHeight; } else if (this.screen) { const res = this.screen.screen.resolution; const scale = this.screen.screen.scale; resx = res.x / scale; resy = res.y / scale; } this._localAnchor.set(this._anchor.x * resx, this._anchor.y * resy, this._anchor.z * resx, this._anchor.w * resy); } getOffsetPosition(x, y) { const p = this.entity.getLocalPosition().clone(); p.x += x; p.y += y; this._screenToWorld.transformPoint(p, p); return p; } onLayersChanged(oldComp, newComp) { this.addModelToLayers(this._image ? this._image._renderable.model : this._text._model); oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; if (this._image) { layer.addMeshInstances(this._image._renderable.model.meshInstances); } else if (this._text) { layer.addMeshInstances(this._text._model.meshInstances); } } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; if (this._image) { layer.removeMeshInstances(this._image._renderable.model.meshInstances); } else if (this._text) { layer.removeMeshInstances(this._text._model.meshInstances); } } onEnable() { const scene = this.system.app.scene; const layers = scene.layers; if (this._image) { this._image.onEnable(); } if (this._text) { this._text.onEnable(); } if (this._group) { this._group.onEnable(); } if (this.useInput && this.system.app.elementInput) { this.system.app.elementInput.addElement(this); } this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this._batchGroupId >= 0) { this.system.app.batcher?.insert(BatchGroup.ELEMENT, this.batchGroupId, this.entity); } this.fire('enableelement'); } onDisable() { const scene = this.system.app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } if (this._image) this._image.onDisable(); if (this._text) this._text.onDisable(); if (this._group) this._group.onDisable(); if (this.system.app.elementInput && this.useInput) { this.system.app.elementInput.removeElement(this); } if (this._batchGroupId >= 0) { this.system.app.batcher?.remove(BatchGroup.ELEMENT, this.batchGroupId, this.entity); } this.fire('disableelement'); } onRemove() { this.entity.off('insert', this._onInsert, this); this._unpatch(); if (this._image) { this._image.destroy(); } if (this._text) { this._text.destroy(); } if (this.system.app.elementInput && this.useInput) { this.system.app.elementInput.removeElement(this); } if (this.screen && this.screen.screen) { this._unbindScreen(this.screen.screen); this.screen.screen.syncDrawOrder(); } this.off(); } _calculateSize(propagateCalculatedWidth, propagateCalculatedHeight) { if (!this.entity._parent && !this.screen) { return; } this._calculateLocalAnchors(); const newWidth = this._absRight - this._absLeft; const newHeight = this._absTop - this._absBottom; if (propagateCalculatedWidth) { this._setWidth(newWidth); } else { this._setCalculatedWidth(newWidth, false); } if (propagateCalculatedHeight) { this._setHeight(newHeight); } else { this._setCalculatedHeight(newHeight, false); } const p = this.entity.getLocalPosition(); p.x = this._margin.x + this._calculatedWidth * this._pivot.x; p.y = this._margin.y + this._calculatedHeight * this._pivot.y; this.entity.setLocalPosition(p); this._sizeDirty = false; } _setWidth(w) { this._width = w; this._setCalculatedWidth(w, false); this.fire('set:width', this._width); } _setHeight(h) { this._height = h; this._setCalculatedHeight(h, false); this.fire('set:height', this._height); } _setCalculatedWidth(value, updateMargins) { if (Math.abs(value - this._calculatedWidth) <= 1e-4) { return; } this._calculatedWidth = value; this.entity._dirtifyLocal(); if (updateMargins) { const p = this.entity.getLocalPosition(); const pvt = this._pivot; this._margin.x = p.x - this._calculatedWidth * pvt.x; this._margin.z = this._localAnchor.z - this._localAnchor.x - this._calculatedWidth - this._margin.x; } this._flagChildrenAsDirty(); this.fire('set:calculatedWidth', this._calculatedWidth); this.fire('resize', this._calculatedWidth, this._calculatedHeight); } _setCalculatedHeight(value, updateMargins) { if (Math.abs(value - this._calculatedHeight) <= 1e-4) { return; } this._calculatedHeight = value; this.entity._dirtifyLocal(); if (updateMargins) { const p = this.entity.getLocalPosition(); const pvt = this._pivot; this._margin.y = p.y - this._calculatedHeight * pvt.y; this._margin.w = this._localAnchor.w - this._localAnchor.y - this._calculatedHeight - this._margin.y; } this._flagChildrenAsDirty(); this.fire('set:calculatedHeight', this._calculatedHeight); this.fire('resize', this._calculatedWidth, this._calculatedHeight); } _flagChildrenAsDirty() { const c = this.entity._children; for(let i = 0, l = c.length; i < l; i++){ if (c[i].element) { c[i].element._anchorDirty = true; c[i].element._sizeDirty = true; } } } addModelToLayers(model) { this._addedModels.push(model); for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (!layer) continue; layer.addMeshInstances(model.meshInstances); } } removeModelFromLayers(model) { const idx = this._addedModels.indexOf(model); if (idx >= 0) { this._addedModels.splice(idx, 1); } for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (!layer) { continue; } layer.removeMeshInstances(model.meshInstances); } } getMaskOffset() { const frame = this.system.app.frame; if (this._offsetReadAt !== frame) { this._maskOffset = 0.5; this._offsetReadAt = frame; } const mo = this._maskOffset; this._maskOffset -= 0.001; return mo; } isVisibleForCamera(camera) { let clipL, clipR, clipT, clipB; if (this.maskedBy) { const corners = this.maskedBy.element.screenCorners; clipL = Math.min(Math.min(corners[0].x, corners[1].x), Math.min(corners[2].x, corners[3].x)); clipR = Math.max(Math.max(corners[0].x, corners[1].x), Math.max(corners[2].x, corners[3].x)); clipB = Math.min(Math.min(corners[0].y, corners[1].y), Math.min(corners[2].y, corners[3].y)); clipT = Math.max(Math.max(corners[0].y, corners[1].y), Math.max(corners[2].y, corners[3].y)); } else { const sw = this.system.app.graphicsDevice.width; const sh = this.system.app.graphicsDevice.height; const cameraWidth = camera._rect.z * sw; const cameraHeight = camera._rect.w * sh; clipL = camera._rect.x * sw; clipR = clipL + cameraWidth; clipT = (1 - camera._rect.y) * sh; clipB = clipT - cameraHeight; } const hitCorners = this.screenCorners; const left = Math.min(Math.min(hitCorners[0].x, hitCorners[1].x), Math.min(hitCorners[2].x, hitCorners[3].x)); const right = Math.max(Math.max(hitCorners[0].x, hitCorners[1].x), Math.max(hitCorners[2].x, hitCorners[3].x)); const bottom = Math.min(Math.min(hitCorners[0].y, hitCorners[1].y), Math.min(hitCorners[2].y, hitCorners[3].y)); const top = Math.max(Math.max(hitCorners[0].y, hitCorners[1].y), Math.max(hitCorners[2].y, hitCorners[3].y)); if (right < clipL || left > clipR || bottom > clipT || top < clipB) { return false; } return true; } _isScreenSpace() { if (this.screen && this.screen.screen) { return this.screen.screen.screenSpace; } return false; } _isScreenCulled() { if (this.screen && this.screen.screen) { return this.screen.screen.cull; } return false; } _dirtyBatch() { if (this.batchGroupId !== -1) { this.system.app.batcher?.markGroupDirty(this.batchGroupId); } } constructor(system, entity){ super(system, entity), this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null; this._beingInitialized = false; this._anchor = new Vec4(); this._localAnchor = new Vec4(); this._pivot = new Vec2(); this._width = this._calculatedWidth = 32; this._height = this._calculatedHeight = 32; this._margin = new Vec4(0, 0, -32, -32); this._modelTransform = new Mat4(); this._screenToWorld = new Mat4(); this._anchorTransform = new Mat4(); this._anchorDirty = true; this._parentWorldTransform = new Mat4(); this._screenTransform = new Mat4(); this._screenCorners = [ new Vec3(), new Vec3(), new Vec3(), new Vec3() ]; this._canvasCorners = [ new Vec2(), new Vec2(), new Vec2(), new Vec2() ]; this._worldCorners = [ new Vec3(), new Vec3(), new Vec3(), new Vec3() ]; this._cornersDirty = true; this._canvasCornersDirty = true; this._worldCornersDirty = true; this.entity.on('insert', this._onInsert, this); this._patch(); this.screen = null; this._type = ELEMENTTYPE_GROUP; this._image = null; this._text = null; this._group = null; this._drawOrder = 0; this._fitMode = FITMODE_STRETCH; this._useInput = false; this._layers = [ LAYERID_UI ]; this._addedModels = []; this._batchGroupId = -1; this._offsetReadAt = 0; this._maskOffset = 0.5; this._maskedBy = null; } } ElementComponent.EVENT_MOUSEDOWN = 'mousedown'; ElementComponent.EVENT_MOUSEUP = 'mouseup'; ElementComponent.EVENT_MOUSEENTER = 'mouseenter'; ElementComponent.EVENT_MOUSELEAVE = 'mouseleave'; ElementComponent.EVENT_MOUSEMOVE = 'mousemove'; ElementComponent.EVENT_MOUSEWHEEL = 'mousewheel'; ElementComponent.EVENT_CLICK = 'click'; ElementComponent.EVENT_TOUCHSTART = 'touchstart'; ElementComponent.EVENT_TOUCHEND = 'touchend'; ElementComponent.EVENT_TOUCHMOVE = 'touchmove'; ElementComponent.EVENT_TOUCHCANCEL = 'touchcancel'; class ElementComponentData { constructor(){ this.enabled = true; } } const _schema$f = [ 'enabled' ]; class ElementComponentSystem extends ComponentSystem { destroy() { super.destroy(); this._defaultTexture.destroy(); } initializeComponentData(component, data, properties) { component._beingInitialized = true; if (data.anchor !== undefined) { if (data.anchor instanceof Vec4) { component.anchor.copy(data.anchor); } else { component.anchor.set(data.anchor[0], data.anchor[1], data.anchor[2], data.anchor[3]); } } if (data.pivot !== undefined) { if (data.pivot instanceof Vec2) { component.pivot.copy(data.pivot); } else { component.pivot.set(data.pivot[0], data.pivot[1]); } } const splitHorAnchors = Math.abs(component.anchor.x - component.anchor.z) > 0.001; const splitVerAnchors = Math.abs(component.anchor.y - component.anchor.w) > 0.001; let _marginChange = false; let color; if (data.margin !== undefined) { if (data.margin instanceof Vec4) { component.margin.copy(data.margin); } else { component._margin.set(data.margin[0], data.margin[1], data.margin[2], data.margin[3]); } _marginChange = true; } if (data.left !== undefined) { component._margin.x = data.left; _marginChange = true; } if (data.bottom !== undefined) { component._margin.y = data.bottom; _marginChange = true; } if (data.right !== undefined) { component._margin.z = data.right; _marginChange = true; } if (data.top !== undefined) { component._margin.w = data.top; _marginChange = true; } if (_marginChange) { component.margin = component._margin; } let shouldForceSetAnchor = false; if (data.width !== undefined && !splitHorAnchors) { component.width = data.width; } else if (splitHorAnchors) { shouldForceSetAnchor = true; } if (data.height !== undefined && !splitVerAnchors) { component.height = data.height; } else if (splitVerAnchors) { shouldForceSetAnchor = true; } if (shouldForceSetAnchor) { component.anchor = component.anchor; } if (data.enabled !== undefined) { component.enabled = data.enabled; } if (data.useInput !== undefined) { component.useInput = data.useInput; } if (data.fitMode !== undefined) { component.fitMode = data.fitMode; } component.batchGroupId = data.batchGroupId === undefined || data.batchGroupId === null ? -1 : data.batchGroupId; if (data.layers && Array.isArray(data.layers)) { component.layers = data.layers.slice(0); } if (data.type !== undefined) { component.type = data.type; } if (component.type === ELEMENTTYPE_IMAGE) { if (data.rect !== undefined) { component.rect = data.rect; } if (data.color !== undefined) { color = data.color; if (!(color instanceof Color)) { color = new Color(data.color[0], data.color[1], data.color[2]); } component.color = color; } if (data.opacity !== undefined) component.opacity = data.opacity; if (data.textureAsset !== undefined) component.textureAsset = data.textureAsset; if (data.texture) component.texture = data.texture; if (data.spriteAsset !== undefined) component.spriteAsset = data.spriteAsset; if (data.sprite) component.sprite = data.sprite; if (data.spriteFrame !== undefined) component.spriteFrame = data.spriteFrame; if (data.pixelsPerUnit !== undefined && data.pixelsPerUnit !== null) component.pixelsPerUnit = data.pixelsPerUnit; if (data.materialAsset !== undefined) component.materialAsset = data.materialAsset; if (data.material) component.material = data.material; if (data.mask !== undefined) { component.mask = data.mask; } } else if (component.type === ELEMENTTYPE_TEXT) { if (data.autoWidth !== undefined) component.autoWidth = data.autoWidth; if (data.autoHeight !== undefined) component.autoHeight = data.autoHeight; if (data.rtlReorder !== undefined) component.rtlReorder = data.rtlReorder; if (data.unicodeConverter !== undefined) component.unicodeConverter = data.unicodeConverter; if (data.text !== null && data.text !== undefined) { component.text = data.text; } else if (data.key !== null && data.key !== undefined) { component.key = data.key; } if (data.color !== undefined) { color = data.color; if (!(color instanceof Color)) { color = new Color(color[0], color[1], color[2]); } component.color = color; } if (data.opacity !== undefined) { component.opacity = data.opacity; } if (data.spacing !== undefined) component.spacing = data.spacing; if (data.fontSize !== undefined) { component.fontSize = data.fontSize; if (!data.lineHeight) component.lineHeight = data.fontSize; } if (data.lineHeight !== undefined) component.lineHeight = data.lineHeight; if (data.maxLines !== undefined) component.maxLines = data.maxLines; if (data.wrapLines !== undefined) component.wrapLines = data.wrapLines; if (data.minFontSize !== undefined) component.minFontSize = data.minFontSize; if (data.maxFontSize !== undefined) component.maxFontSize = data.maxFontSize; if (data.autoFitWidth) component.autoFitWidth = data.autoFitWidth; if (data.autoFitHeight) component.autoFitHeight = data.autoFitHeight; if (data.fontAsset !== undefined) component.fontAsset = data.fontAsset; if (data.font !== undefined) component.font = data.font; if (data.alignment !== undefined) component.alignment = data.alignment; if (data.outlineColor !== undefined) component.outlineColor = data.outlineColor; if (data.outlineThickness !== undefined) component.outlineThickness = data.outlineThickness; if (data.shadowColor !== undefined) component.shadowColor = data.shadowColor; if (data.shadowOffset !== undefined) component.shadowOffset = data.shadowOffset; if (data.enableMarkup !== undefined) component.enableMarkup = data.enableMarkup; } const result = component._parseUpToScreen(); if (result.screen) { component._updateScreen(result.screen); } super.initializeComponentData(component, data, properties); component._beingInitialized = false; if (component.type === ELEMENTTYPE_IMAGE && component._image._meshDirty) { component._image._updateMesh(component._image.mesh); } } onAddComponent(entity, component) { entity.fire('element:add'); } onRemoveComponent(entity, component) { component.onRemove(); } cloneComponent(entity, clone) { const source = entity.element; const data = { enabled: source.enabled, width: source.width, height: source.height, anchor: source.anchor.clone(), pivot: source.pivot.clone(), margin: source.margin.clone(), alignment: source.alignment && source.alignment.clone() || source.alignment, autoWidth: source.autoWidth, autoHeight: source.autoHeight, type: source.type, rect: source.rect && source.rect.clone() || source.rect, rtlReorder: source.rtlReorder, unicodeConverter: source.unicodeConverter, materialAsset: source.materialAsset, material: source.material, color: source.color && source.color.clone() || source.color, opacity: source.opacity, textureAsset: source.textureAsset, texture: source.texture, spriteAsset: source.spriteAsset, sprite: source.sprite, spriteFrame: source.spriteFrame, pixelsPerUnit: source.pixelsPerUnit, spacing: source.spacing, lineHeight: source.lineHeight, wrapLines: source.wrapLines, layers: source.layers, fontSize: source.fontSize, minFontSize: source.minFontSize, maxFontSize: source.maxFontSize, autoFitWidth: source.autoFitWidth, autoFitHeight: source.autoFitHeight, maxLines: source.maxLines, fontAsset: source.fontAsset, font: source.font, useInput: source.useInput, fitMode: source.fitMode, batchGroupId: source.batchGroupId, mask: source.mask, outlineColor: source.outlineColor && source.outlineColor.clone() || source.outlineColor, outlineThickness: source.outlineThickness, shadowColor: source.shadowColor && source.shadowColor.clone() || source.shadowColor, shadowOffset: source.shadowOffset && source.shadowOffset.clone() || source.shadowOffset, enableMarkup: source.enableMarkup }; if (source.key !== undefined && source.key !== null) { data.key = source.key; } else { data.text = source.text; } return this.addComponent(clone, data); } getTextElementMaterial(screenSpace, msdf, textAttibutes) { const hash = (screenSpace && 1 << 0) | (msdf && 1 << 1) | (textAttibutes && 1 << 2); let material = this._defaultTextMaterials[hash]; if (material) { return material; } let name = 'TextMaterial'; material = new StandardMaterial(); if (msdf) { material.msdfMap = this._defaultTexture; material.msdfTextAttribute = textAttibutes; material.emissive.set(1, 1, 1); } else { name = `Bitmap${name}`; material.emissive.set(1, 1, 1); material.emissiveMap = this._defaultTexture; material.opacityMap = this._defaultTexture; material.opacityMapChannel = 'a'; } if (screenSpace) { name = `ScreenSpace${name}`; material.depthTest = false; } material.name = `default${name}`; material.useLighting = false; material.useTonemap = false; material.useFog = false; material.useSkybox = false; material.diffuse.set(0, 0, 0); material.opacity = 0.5; material.blendType = BLEND_PREMULTIPLIED; material.depthWrite = false; material.emissiveVertexColor = true; material.update(); this._defaultTextMaterials[hash] = material; return material; } _createBaseImageMaterial() { const material = new StandardMaterial(); material.diffuse.set(0, 0, 0); material.emissive.set(1, 1, 1); material.emissiveMap = this._defaultTexture; material.opacityMap = this._defaultTexture; material.opacityMapChannel = 'a'; material.useLighting = false; material.useTonemap = false; material.useFog = false; material.useSkybox = false; material.blendType = BLEND_PREMULTIPLIED; material.depthWrite = false; return material; } getImageElementMaterial(screenSpace, mask, nineSliced, nineSliceTiled) { if (screenSpace) { if (mask) { if (nineSliced) { if (!this.defaultScreenSpaceImageMask9SlicedMaterial) { this.defaultScreenSpaceImageMask9SlicedMaterial = this._createBaseImageMaterial(); this.defaultScreenSpaceImageMask9SlicedMaterial.name = 'defaultScreenSpaceImageMask9SlicedMaterial'; this.defaultScreenSpaceImageMask9SlicedMaterial.nineSlicedMode = SPRITE_RENDERMODE_SLICED; this.defaultScreenSpaceImageMask9SlicedMaterial.depthTest = false; this.defaultScreenSpaceImageMask9SlicedMaterial.alphaTest = 1; this.defaultScreenSpaceImageMask9SlicedMaterial.redWrite = false; this.defaultScreenSpaceImageMask9SlicedMaterial.greenWrite = false; this.defaultScreenSpaceImageMask9SlicedMaterial.blueWrite = false; this.defaultScreenSpaceImageMask9SlicedMaterial.alphaWrite = false; this.defaultScreenSpaceImageMask9SlicedMaterial.update(); this.defaultImageMaterials.push(this.defaultScreenSpaceImageMask9SlicedMaterial); } return this.defaultScreenSpaceImageMask9SlicedMaterial; } else if (nineSliceTiled) { if (!this.defaultScreenSpaceImageMask9TiledMaterial) { this.defaultScreenSpaceImageMask9TiledMaterial = this.defaultScreenSpaceImage9TiledMaterial.clone(); this.defaultScreenSpaceImageMask9TiledMaterial.name = 'defaultScreenSpaceImageMask9TiledMaterial'; this.defaultScreenSpaceImageMask9TiledMaterial.nineSlicedMode = SPRITE_RENDERMODE_TILED; this.defaultScreenSpaceImageMask9TiledMaterial.depthTest = false; this.defaultScreenSpaceImageMask9TiledMaterial.alphaTest = 1; this.defaultScreenSpaceImageMask9TiledMaterial.redWrite = false; this.defaultScreenSpaceImageMask9TiledMaterial.greenWrite = false; this.defaultScreenSpaceImageMask9TiledMaterial.blueWrite = false; this.defaultScreenSpaceImageMask9TiledMaterial.alphaWrite = false; this.defaultScreenSpaceImageMask9TiledMaterial.update(); this.defaultImageMaterials.push(this.defaultScreenSpaceImageMask9TiledMaterial); } return this.defaultScreenSpaceImageMask9TiledMaterial; } else { if (!this.defaultScreenSpaceImageMaskMaterial) { this.defaultScreenSpaceImageMaskMaterial = this._createBaseImageMaterial(); this.defaultScreenSpaceImageMaskMaterial.name = 'defaultScreenSpaceImageMaskMaterial'; this.defaultScreenSpaceImageMaskMaterial.depthTest = false; this.defaultScreenSpaceImageMaskMaterial.alphaTest = 1; this.defaultScreenSpaceImageMaskMaterial.redWrite = false; this.defaultScreenSpaceImageMaskMaterial.greenWrite = false; this.defaultScreenSpaceImageMaskMaterial.blueWrite = false; this.defaultScreenSpaceImageMaskMaterial.alphaWrite = false; this.defaultScreenSpaceImageMaskMaterial.update(); this.defaultImageMaterials.push(this.defaultScreenSpaceImageMaskMaterial); } return this.defaultScreenSpaceImageMaskMaterial; } } else { if (nineSliced) { if (!this.defaultScreenSpaceImage9SlicedMaterial) { this.defaultScreenSpaceImage9SlicedMaterial = this._createBaseImageMaterial(); this.defaultScreenSpaceImage9SlicedMaterial.name = 'defaultScreenSpaceImage9SlicedMaterial'; this.defaultScreenSpaceImage9SlicedMaterial.nineSlicedMode = SPRITE_RENDERMODE_SLICED; this.defaultScreenSpaceImage9SlicedMaterial.depthTest = false; this.defaultScreenSpaceImage9SlicedMaterial.update(); this.defaultImageMaterials.push(this.defaultScreenSpaceImage9SlicedMaterial); } return this.defaultScreenSpaceImage9SlicedMaterial; } else if (nineSliceTiled) { if (!this.defaultScreenSpaceImage9TiledMaterial) { this.defaultScreenSpaceImage9TiledMaterial = this._createBaseImageMaterial(); this.defaultScreenSpaceImage9TiledMaterial.name = 'defaultScreenSpaceImage9TiledMaterial'; this.defaultScreenSpaceImage9TiledMaterial.nineSlicedMode = SPRITE_RENDERMODE_TILED; this.defaultScreenSpaceImage9TiledMaterial.depthTest = false; this.defaultScreenSpaceImage9TiledMaterial.update(); this.defaultImageMaterials.push(this.defaultScreenSpaceImage9TiledMaterial); } return this.defaultScreenSpaceImage9TiledMaterial; } else { if (!this.defaultScreenSpaceImageMaterial) { this.defaultScreenSpaceImageMaterial = this._createBaseImageMaterial(); this.defaultScreenSpaceImageMaterial.name = 'defaultScreenSpaceImageMaterial'; this.defaultScreenSpaceImageMaterial.depthTest = false; this.defaultScreenSpaceImageMaterial.update(); this.defaultImageMaterials.push(this.defaultScreenSpaceImageMaterial); } return this.defaultScreenSpaceImageMaterial; } } } else { if (mask) { if (nineSliced) { if (!this.defaultImage9SlicedMaskMaterial) { this.defaultImage9SlicedMaskMaterial = this._createBaseImageMaterial(); this.defaultImage9SlicedMaskMaterial.name = 'defaultImage9SlicedMaskMaterial'; this.defaultImage9SlicedMaskMaterial.nineSlicedMode = SPRITE_RENDERMODE_SLICED; this.defaultImage9SlicedMaskMaterial.alphaTest = 1; this.defaultImage9SlicedMaskMaterial.redWrite = false; this.defaultImage9SlicedMaskMaterial.greenWrite = false; this.defaultImage9SlicedMaskMaterial.blueWrite = false; this.defaultImage9SlicedMaskMaterial.alphaWrite = false; this.defaultImage9SlicedMaskMaterial.update(); this.defaultImageMaterials.push(this.defaultImage9SlicedMaskMaterial); } return this.defaultImage9SlicedMaskMaterial; } else if (nineSliceTiled) { if (!this.defaultImage9TiledMaskMaterial) { this.defaultImage9TiledMaskMaterial = this._createBaseImageMaterial(); this.defaultImage9TiledMaskMaterial.name = 'defaultImage9TiledMaskMaterial'; this.defaultImage9TiledMaskMaterial.nineSlicedMode = SPRITE_RENDERMODE_TILED; this.defaultImage9TiledMaskMaterial.alphaTest = 1; this.defaultImage9TiledMaskMaterial.redWrite = false; this.defaultImage9TiledMaskMaterial.greenWrite = false; this.defaultImage9TiledMaskMaterial.blueWrite = false; this.defaultImage9TiledMaskMaterial.alphaWrite = false; this.defaultImage9TiledMaskMaterial.update(); this.defaultImageMaterials.push(this.defaultImage9TiledMaskMaterial); } return this.defaultImage9TiledMaskMaterial; } else { if (!this.defaultImageMaskMaterial) { this.defaultImageMaskMaterial = this._createBaseImageMaterial(); this.defaultImageMaskMaterial.name = 'defaultImageMaskMaterial'; this.defaultImageMaskMaterial.alphaTest = 1; this.defaultImageMaskMaterial.redWrite = false; this.defaultImageMaskMaterial.greenWrite = false; this.defaultImageMaskMaterial.blueWrite = false; this.defaultImageMaskMaterial.alphaWrite = false; this.defaultImageMaskMaterial.update(); this.defaultImageMaterials.push(this.defaultImageMaskMaterial); } return this.defaultImageMaskMaterial; } } else { if (nineSliced) { if (!this.defaultImage9SlicedMaterial) { this.defaultImage9SlicedMaterial = this._createBaseImageMaterial(); this.defaultImage9SlicedMaterial.name = 'defaultImage9SlicedMaterial'; this.defaultImage9SlicedMaterial.nineSlicedMode = SPRITE_RENDERMODE_SLICED; this.defaultImage9SlicedMaterial.update(); this.defaultImageMaterials.push(this.defaultImage9SlicedMaterial); } return this.defaultImage9SlicedMaterial; } else if (nineSliceTiled) { if (!this.defaultImage9TiledMaterial) { this.defaultImage9TiledMaterial = this._createBaseImageMaterial(); this.defaultImage9TiledMaterial.name = 'defaultImage9TiledMaterial'; this.defaultImage9TiledMaterial.nineSlicedMode = SPRITE_RENDERMODE_TILED; this.defaultImage9TiledMaterial.update(); this.defaultImageMaterials.push(this.defaultImage9TiledMaterial); } return this.defaultImage9TiledMaterial; } else { if (!this.defaultImageMaterial) { this.defaultImageMaterial = this._createBaseImageMaterial(); this.defaultImageMaterial.name = 'defaultImageMaterial'; this.defaultImageMaterial.update(); this.defaultImageMaterials.push(this.defaultImageMaterial); } return this.defaultImageMaterial; } } } } registerUnicodeConverter(func) { this._unicodeConverter = func; } registerRtlReorder(func) { this._rtlReorder = func; } getUnicodeConverter() { return this._unicodeConverter; } getRtlReorder() { return this._rtlReorder; } constructor(app){ super(app); this.id = 'element'; this.ComponentType = ElementComponent; this.DataType = ElementComponentData; this.schema = _schema$f; this._unicodeConverter = null; this._rtlReorder = null; this._defaultTexture = new Texture(app.graphicsDevice, { width: 1, height: 1, format: PIXELFORMAT_SRGBA8, name: 'element-system' }); const pixels = this._defaultTexture.lock(); const pixelData = new Uint8Array(4); pixelData[0] = 255.0; pixelData[1] = 255.0; pixelData[2] = 255.0; pixelData[3] = 255.0; pixels.set(pixelData); this._defaultTexture.unlock(); this.defaultImageMaterial = null; this.defaultImage9SlicedMaterial = null; this.defaultImage9TiledMaterial = null; this.defaultImageMaskMaterial = null; this.defaultImage9SlicedMaskMaterial = null; this.defaultImage9TiledMaskMaterial = null; this.defaultScreenSpaceImageMaterial = null; this.defaultScreenSpaceImage9SlicedMaterial = null; this.defaultScreenSpaceImage9TiledMaterial = null; this.defaultScreenSpaceImageMask9SlicedMaterial = null; this.defaultScreenSpaceImageMask9TiledMaterial = null; this.defaultScreenSpaceImageMaskMaterial = null; this._defaultTextMaterials = {}; this.defaultImageMaterials = []; this.on('add', this.onAddComponent, this); this.on('beforeremove', this.onRemoveComponent, this); } } const MOTION_FREE = 'free'; const MOTION_LIMITED = 'limited'; const MOTION_LOCKED = 'locked'; const properties$1 = [ 'angularDampingX', 'angularDampingY', 'angularDampingZ', 'angularEquilibriumX', 'angularEquilibriumY', 'angularEquilibriumZ', 'angularLimitsX', 'angularLimitsY', 'angularLimitsZ', 'angularMotionX', 'angularMotionY', 'angularMotionZ', 'angularSpringX', 'angularSpringY', 'angularSpringZ', 'angularStiffnessX', 'angularStiffnessY', 'angularStiffnessZ', 'breakForce', 'enableCollision', 'enabled', 'entityA', 'entityB', 'linearDampingX', 'linearDampingY', 'linearDampingZ', 'linearEquilibriumX', 'linearEquilibriumY', 'linearEquilibriumZ', 'linearLimitsX', 'linearLimitsY', 'linearLimitsZ', 'linearMotionX', 'linearMotionY', 'linearMotionZ', 'linearSpringX', 'linearSpringY', 'linearSpringZ', 'linearStiffnessX', 'linearStiffnessY', 'linearStiffnessZ' ]; class JointComponent extends Component { set entityA(body) { this._destroyConstraint(); this._entityA = body; this._createConstraint(); } get entityA() { return this._entityA; } set entityB(body) { this._destroyConstraint(); this._entityB = body; this._createConstraint(); } get entityB() { return this._entityB; } set breakForce(force) { if (this._constraint && this._breakForce !== force) { this._constraint.setBreakingImpulseThreshold(force); this._breakForce = force; } } get breakForce() { return this._breakForce; } set enableCollision(enableCollision) { this._destroyConstraint(); this._enableCollision = enableCollision; this._createConstraint(); } get enableCollision() { return this._enableCollision; } set angularLimitsX(limits) { if (!this._angularLimitsX.equals(limits)) { this._angularLimitsX.copy(limits); this._updateAngularLimits(); } } get angularLimitsX() { return this._angularLimitsX; } set angularMotionX(value) { if (this._angularMotionX !== value) { this._angularMotionX = value; this._updateAngularLimits(); } } get angularMotionX() { return this._angularMotionX; } set angularLimitsY(limits) { if (!this._angularLimitsY.equals(limits)) { this._angularLimitsY.copy(limits); this._updateAngularLimits(); } } get angularLimitsY() { return this._angularLimitsY; } set angularMotionY(value) { if (this._angularMotionY !== value) { this._angularMotionY = value; this._updateAngularLimits(); } } get angularMotionY() { return this._angularMotionY; } set angularLimitsZ(limits) { if (!this._angularLimitsZ.equals(limits)) { this._angularLimitsZ.copy(limits); this._updateAngularLimits(); } } get angularLimitsZ() { return this._angularLimitsZ; } set angularMotionZ(value) { if (this._angularMotionZ !== value) { this._angularMotionZ = value; this._updateAngularLimits(); } } get angularMotionZ() { return this._angularMotionZ; } set linearLimitsX(limits) { if (!this._linearLimitsX.equals(limits)) { this._linearLimitsX.copy(limits); this._updateLinearLimits(); } } get linearLimitsX() { return this._linearLimitsX; } set linearMotionX(value) { if (this._linearMotionX !== value) { this._linearMotionX = value; this._updateLinearLimits(); } } get linearMotionX() { return this._linearMotionX; } set linearLimitsY(limits) { if (!this._linearLimitsY.equals(limits)) { this._linearLimitsY.copy(limits); this._updateLinearLimits(); } } get linearLimitsY() { return this._linearLimitsY; } set linearMotionY(value) { if (this._linearMotionY !== value) { this._linearMotionY = value; this._updateLinearLimits(); } } get linearMotionY() { return this._linearMotionY; } set linearLimitsZ(limits) { if (!this._linearLimitsZ.equals(limits)) { this._linearLimitsZ.copy(limits); this._updateLinearLimits(); } } get linearLimitsZ() { return this._linearLimitsZ; } set linearMotionZ(value) { if (this._linearMotionZ !== value) { this._linearMotionZ = value; this._updateLinearLimits(); } } get linearMotionZ() { return this._linearMotionZ; } _convertTransform(pcTransform, ammoTransform) { const pos = pcTransform.getTranslation(); const rot = new Quat(); rot.setFromMat4(pcTransform); const ammoVec = new Ammo.btVector3(pos.x, pos.y, pos.z); const ammoQuat = new Ammo.btQuaternion(rot.x, rot.y, rot.z, rot.w); ammoTransform.setOrigin(ammoVec); ammoTransform.setRotation(ammoQuat); Ammo.destroy(ammoVec); Ammo.destroy(ammoQuat); } _updateAngularLimits() { const constraint = this._constraint; if (constraint) { let lx, ly, lz, ux, uy, uz; if (this._angularMotionX === MOTION_LIMITED) { lx = this._angularLimitsX.x * math.DEG_TO_RAD; ux = this._angularLimitsX.y * math.DEG_TO_RAD; } else if (this._angularMotionX === MOTION_FREE) { lx = 1; ux = 0; } else { lx = ux = 0; } if (this._angularMotionY === MOTION_LIMITED) { ly = this._angularLimitsY.x * math.DEG_TO_RAD; uy = this._angularLimitsY.y * math.DEG_TO_RAD; } else if (this._angularMotionY === MOTION_FREE) { ly = 1; uy = 0; } else { ly = uy = 0; } if (this._angularMotionZ === MOTION_LIMITED) { lz = this._angularLimitsZ.x * math.DEG_TO_RAD; uz = this._angularLimitsZ.y * math.DEG_TO_RAD; } else if (this._angularMotionZ === MOTION_FREE) { lz = 1; uz = 0; } else { lz = uz = 0; } const limits = new Ammo.btVector3(lx, ly, lz); constraint.setAngularLowerLimit(limits); limits.setValue(ux, uy, uz); constraint.setAngularUpperLimit(limits); Ammo.destroy(limits); } } _updateLinearLimits() { const constraint = this._constraint; if (constraint) { let lx, ly, lz, ux, uy, uz; if (this._linearMotionX === MOTION_LIMITED) { lx = this._linearLimitsX.x; ux = this._linearLimitsX.y; } else if (this._linearMotionX === MOTION_FREE) { lx = 1; ux = 0; } else { lx = ux = 0; } if (this._linearMotionY === MOTION_LIMITED) { ly = this._linearLimitsY.x; uy = this._linearLimitsY.y; } else if (this._linearMotionY === MOTION_FREE) { ly = 1; uy = 0; } else { ly = uy = 0; } if (this._linearMotionZ === MOTION_LIMITED) { lz = this._linearLimitsZ.x; uz = this._linearLimitsZ.y; } else if (this._linearMotionZ === MOTION_FREE) { lz = 1; uz = 0; } else { lz = uz = 0; } const limits = new Ammo.btVector3(lx, ly, lz); constraint.setLinearLowerLimit(limits); limits.setValue(ux, uy, uz); constraint.setLinearUpperLimit(limits); Ammo.destroy(limits); } } _createConstraint() { if (this._entityA && this._entityA.rigidbody) { this._destroyConstraint(); const mat = new Mat4(); const bodyA = this._entityA.rigidbody.body; bodyA.activate(); const jointWtm = this.entity.getWorldTransform(); const entityAWtm = this._entityA.getWorldTransform(); const invEntityAWtm = entityAWtm.clone().invert(); mat.mul2(invEntityAWtm, jointWtm); const frameA = new Ammo.btTransform(); this._convertTransform(mat, frameA); if (this._entityB && this._entityB.rigidbody) { const bodyB = this._entityB.rigidbody.body; bodyB.activate(); const entityBWtm = this._entityB.getWorldTransform(); const invEntityBWtm = entityBWtm.clone().invert(); mat.mul2(invEntityBWtm, jointWtm); const frameB = new Ammo.btTransform(); this._convertTransform(mat, frameB); this._constraint = new Ammo.btGeneric6DofSpringConstraint(bodyA, bodyB, frameA, frameB, !this._enableCollision); Ammo.destroy(frameB); } else { this._constraint = new Ammo.btGeneric6DofSpringConstraint(bodyA, frameA, !this._enableCollision); } Ammo.destroy(frameA); const axis = [ 'X', 'Y', 'Z', 'X', 'Y', 'Z' ]; for(let i = 0; i < 6; i++){ const type = i < 3 ? '_linear' : '_angular'; this._constraint.enableSpring(i, this[`${type}Spring${axis[i]}`]); this._constraint.setDamping(i, this[`${type}Damping${axis[i]}`]); this._constraint.setEquilibriumPoint(i, this[`${type}Equilibrium${axis[i]}`]); this._constraint.setStiffness(i, this[`${type}Stiffness${axis[i]}`]); } this._constraint.setBreakingImpulseThreshold(this._breakForce); this._updateLinearLimits(); this._updateAngularLimits(); const app = this.system.app; const dynamicsWorld = app.systems.rigidbody.dynamicsWorld; dynamicsWorld.addConstraint(this._constraint, !this._enableCollision); } } _destroyConstraint() { if (this._constraint) { const app = this.system.app; const dynamicsWorld = app.systems.rigidbody.dynamicsWorld; dynamicsWorld.removeConstraint(this._constraint); Ammo.destroy(this._constraint); this._constraint = null; } } initFromData(data) { for (const prop of properties$1){ if (data.hasOwnProperty(prop)) { if (data[prop] instanceof Vec2) { this[`_${prop}`].copy(data[prop]); } else { this[`_${prop}`] = data[prop]; } } } this._createConstraint(); } onEnable() { this._createConstraint(); } onDisable() { this._destroyConstraint(); } _onSetEnabled(prop, old, value) {} _onBeforeRemove() { this.fire('remove'); } constructor(system, entity){ super(system, entity); this._constraint = null; this._entityA = null; this._entityB = null; this._breakForce = 3.4e+38; this._enableCollision = true; this._linearMotionX = MOTION_LOCKED; this._linearLimitsX = new Vec2(0, 0); this._linearSpringX = false; this._linearStiffnessX = 0; this._linearDampingX = 1; this._linearEquilibriumX = 0; this._linearMotionY = MOTION_LOCKED; this._linearLimitsY = new Vec2(0, 0); this._linearSpringY = false; this._linearStiffnessY = 0; this._linearDampingY = 1; this._linearEquilibriumY = 0; this._linearMotionZ = MOTION_LOCKED; this._linearLimitsZ = new Vec2(0, 0); this._linearSpringZ = false; this._linearStiffnessZ = 0; this._linearDampingZ = 1; this._linearEquilibriumZ = 0; this._angularMotionX = MOTION_LOCKED; this._angularLimitsX = new Vec2(0, 0); this._angularSpringX = false; this._angularStiffnessX = 0; this._angularDampingX = 1; this._angularEquilibriumX = 0; this._angularMotionY = MOTION_LOCKED; this._angularLimitsY = new Vec2(0, 0); this._angularSpringY = false; this._angularStiffnessY = 0; this._angularDampingY = 1; this._angularEquilibriumY = 0; this._angularMotionZ = MOTION_LOCKED; this._angularLimitsZ = new Vec2(0, 0); this._angularSpringZ = false; this._angularEquilibriumZ = 0; this._angularDampingZ = 1; this._angularStiffnessZ = 0; this.on('set_enabled', this._onSetEnabled, this); } } const functionMap = { Damping: 'setDamping', Equilibrium: 'setEquilibriumPoint', Spring: 'enableSpring', Stiffness: 'setStiffness' }; [ 'linear', 'angular' ].forEach((type)=>{ [ 'Damping', 'Equilibrium', 'Spring', 'Stiffness' ].forEach((name)=>{ [ 'X', 'Y', 'Z' ].forEach((axis)=>{ const prop = type + name + axis; const propInternal = `_${prop}`; let index = type === 'linear' ? 0 : 3; if (axis === 'Y') index += 1; if (axis === 'Z') index += 2; Object.defineProperty(JointComponent.prototype, prop, { get: function() { return this[propInternal]; }, set: function(value) { if (this[propInternal] !== value) { this[propInternal] = value; this._constraint[functionMap[name]](index, value); } } }); }); }); }); class JointComponentData { constructor(){ this.enabled = true; } } const _schema$e = [ 'enabled' ]; class JointComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { component.initFromData(data); super.initializeComponentData(component, data, _schema$e); } constructor(app){ super(app); this.id = 'joint'; this.app = app; this.ComponentType = JointComponent; this.DataType = JointComponentData; this.schema = _schema$e; } } Component._buildAccessors(JointComponent.prototype, _schema$e); class LayoutChildComponent extends Component { set minWidth(value) { if (value !== this._minWidth) { this._minWidth = value; this.fire('resize'); } } get minWidth() { return this._minWidth; } set minHeight(value) { if (value !== this._minHeight) { this._minHeight = value; this.fire('resize'); } } get minHeight() { return this._minHeight; } set maxWidth(value) { if (value !== this._maxWidth) { this._maxWidth = value; this.fire('resize'); } } get maxWidth() { return this._maxWidth; } set maxHeight(value) { if (value !== this._maxHeight) { this._maxHeight = value; this.fire('resize'); } } get maxHeight() { return this._maxHeight; } set fitWidthProportion(value) { if (value !== this._fitWidthProportion) { this._fitWidthProportion = value; this.fire('resize'); } } get fitWidthProportion() { return this._fitWidthProportion; } set fitHeightProportion(value) { if (value !== this._fitHeightProportion) { this._fitHeightProportion = value; this.fire('resize'); } } get fitHeightProportion() { return this._fitHeightProportion; } set excludeFromLayout(value) { if (value !== this._excludeFromLayout) { this._excludeFromLayout = value; this.fire('resize'); } } get excludeFromLayout() { return this._excludeFromLayout; } constructor(...args){ super(...args), this._minWidth = 0, this._minHeight = 0, this._maxWidth = null, this._maxHeight = null, this._fitWidthProportion = 0, this._fitHeightProportion = 0, this._excludeFromLayout = false; } } class LayoutChildComponentData { constructor(){ this.enabled = true; } } const _schema$d = [ 'enabled' ]; class LayoutChildComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { if (data.enabled !== undefined) component.enabled = data.enabled; if (data.minWidth !== undefined) component.minWidth = data.minWidth; if (data.minHeight !== undefined) component.minHeight = data.minHeight; if (data.maxWidth !== undefined) component.maxWidth = data.maxWidth; if (data.maxHeight !== undefined) component.maxHeight = data.maxHeight; if (data.fitWidthProportion !== undefined) component.fitWidthProportion = data.fitWidthProportion; if (data.fitHeightProportion !== undefined) component.fitHeightProportion = data.fitHeightProportion; if (data.excludeFromLayout !== undefined) component.excludeFromLayout = data.excludeFromLayout; super.initializeComponentData(component, data, properties); } cloneComponent(entity, clone) { const layoutChild = entity.layoutchild; return this.addComponent(clone, { enabled: layoutChild.enabled, minWidth: layoutChild.minWidth, minHeight: layoutChild.minHeight, maxWidth: layoutChild.maxWidth, maxHeight: layoutChild.maxHeight, fitWidthProportion: layoutChild.fitWidthProportion, fitHeightProportion: layoutChild.fitHeightProportion, excludeFromLayout: layoutChild.excludeFromLayout }); } constructor(app){ super(app); this.id = 'layoutchild'; this.ComponentType = LayoutChildComponent; this.DataType = LayoutChildComponentData; this.schema = _schema$d; } } Component._buildAccessors(LayoutChildComponent.prototype, _schema$d); const FITTING_NONE = 0; const FITTING_STRETCH = 1; const FITTING_SHRINK = 2; const FITTING_BOTH = 3; const AXIS_MAPPINGS = {}; AXIS_MAPPINGS[ORIENTATION_HORIZONTAL] = { axis: 'x', size: 'width', calculatedSize: 'calculatedWidth', minSize: 'minWidth', maxSize: 'maxWidth', fitting: 'widthFitting', fittingProportion: 'fitWidthProportion' }; AXIS_MAPPINGS[ORIENTATION_VERTICAL] = { axis: 'y', size: 'height', calculatedSize: 'calculatedHeight', minSize: 'minHeight', maxSize: 'maxHeight', fitting: 'heightFitting', fittingProportion: 'fitHeightProportion' }; const OPPOSITE_ORIENTATION = {}; OPPOSITE_ORIENTATION[ORIENTATION_HORIZONTAL] = ORIENTATION_VERTICAL; OPPOSITE_ORIENTATION[ORIENTATION_VERTICAL] = ORIENTATION_HORIZONTAL; const PROPERTY_DEFAULTS = { minWidth: 0, minHeight: 0, maxWidth: Number.POSITIVE_INFINITY, maxHeight: Number.POSITIVE_INFINITY, width: null, height: null, fitWidthProportion: 0, fitHeightProportion: 0 }; const FITTING_ACTION = { NONE: 'NONE', APPLY_STRETCHING: 'APPLY_STRETCHING', APPLY_SHRINKING: 'APPLY_SHRINKING' }; const availableSpace = new Vec2(); function createCalculator(orientation) { let options; const a = AXIS_MAPPINGS[orientation]; const b = AXIS_MAPPINGS[OPPOSITE_ORIENTATION[orientation]]; function minExtentA(element, size) { return -size[a.size] * element.pivot[a.axis]; } function minExtentB(element, size) { return -size[b.size] * element.pivot[b.axis]; } function maxExtentA(element, size) { return size[a.size] * (1 - element.pivot[a.axis]); } function calculateAll(allElements, layoutOptions) { allElements = allElements.filter(shouldIncludeInLayout); options = layoutOptions; availableSpace.x = options.containerSize.x - options.padding.x - options.padding.z; availableSpace.y = options.containerSize.y - options.padding.y - options.padding.w; resetAnchors(allElements); const lines = reverseLinesIfRequired(splitLines(allElements)); const sizes = calculateSizesOnAxisB(lines, calculateSizesOnAxisA(lines)); const positions = calculateBasePositions(lines, sizes); applyAlignmentAndPadding(lines, sizes, positions); applySizesAndPositions(lines, sizes, positions); return createLayoutInfo(lines); } function shouldIncludeInLayout(element) { const layoutChildComponent = element.entity.layoutchild; return !layoutChildComponent || !layoutChildComponent.enabled || !layoutChildComponent.excludeFromLayout; } function resetAnchors(allElements) { for(let i = 0; i < allElements.length; ++i){ const element = allElements[i]; const anchor = element.anchor; if (anchor.x !== 0 || anchor.y !== 0 || anchor.z !== 0 || anchor.w !== 0) { element.anchor = Vec4.ZERO; } } } function splitLines(allElements) { if (!options.wrap) { return [ allElements ]; } const lines = [ [] ]; const sizes = getElementSizeProperties(allElements); let runningSize = 0; const allowOverrun = options[a.fitting] === FITTING_SHRINK; for(let i = 0; i < allElements.length; ++i){ if (lines[lines.length - 1].length > 0) { runningSize += options.spacing[a.axis]; } const idealElementSize = sizes[i][a.size]; runningSize += idealElementSize; if (!allowOverrun && runningSize > availableSpace[a.axis] && lines[lines.length - 1].length !== 0) { runningSize = idealElementSize; lines.push([]); } lines[lines.length - 1].push(allElements[i]); if (allowOverrun && runningSize > availableSpace[a.axis] && i !== allElements.length - 1) { runningSize = 0; lines.push([]); } } return lines; } function reverseLinesIfRequired(lines) { const reverseAxisA = options.orientation === ORIENTATION_HORIZONTAL && options.reverseX || options.orientation === ORIENTATION_VERTICAL && options.reverseY; const reverseAxisB = options.orientation === ORIENTATION_HORIZONTAL && options.reverseY || options.orientation === ORIENTATION_VERTICAL && options.reverseX; if (reverseAxisA) { for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ if (reverseAxisA) { lines[lineIndex].reverse(); } } } if (reverseAxisB) { lines.reverse(); } return lines; } function calculateSizesOnAxisA(lines) { const sizesAllLines = []; for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ const line = lines[lineIndex]; const sizesThisLine = getElementSizeProperties(line); const idealRequiredSpace = calculateTotalSpace(sizesThisLine, a); const fittingAction = determineFittingAction(options[a.fitting], idealRequiredSpace, availableSpace[a.axis]); if (fittingAction === FITTING_ACTION.APPLY_STRETCHING) { stretchSizesToFitContainer(sizesThisLine, idealRequiredSpace, a); } else if (fittingAction === FITTING_ACTION.APPLY_SHRINKING) { shrinkSizesToFitContainer(sizesThisLine, idealRequiredSpace, a); } sizesAllLines.push(sizesThisLine); } return sizesAllLines; } function calculateSizesOnAxisB(lines, sizesAllLines) { const largestElementsForEachLine = []; const largestSizesForEachLine = []; for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ const line = lines[lineIndex]; line.largestElement = null; line.largestSize = { width: Number.NEGATIVE_INFINITY, height: Number.NEGATIVE_INFINITY }; for(let elementIndex = 0; elementIndex < line.length; ++elementIndex){ const sizesThisElement = sizesAllLines[lineIndex][elementIndex]; if (sizesThisElement[b.size] > line.largestSize[b.size]) { line.largestElement = line[elementIndex]; line.largestSize = sizesThisElement; } } largestElementsForEachLine.push(line.largestElement); largestSizesForEachLine.push(line.largestSize); } const idealRequiredSpace = calculateTotalSpace(largestSizesForEachLine, b); const fittingAction = determineFittingAction(options[b.fitting], idealRequiredSpace, availableSpace[b.axis]); if (fittingAction === FITTING_ACTION.APPLY_STRETCHING) { stretchSizesToFitContainer(largestSizesForEachLine, idealRequiredSpace, b); } else if (fittingAction === FITTING_ACTION.APPLY_SHRINKING) { shrinkSizesToFitContainer(largestSizesForEachLine, idealRequiredSpace, b); } for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ const line = lines[lineIndex]; for(let elementIndex = 0; elementIndex < line.length; ++elementIndex){ const sizesForThisElement = sizesAllLines[lineIndex][elementIndex]; const currentSize = sizesForThisElement[b.size]; const availableSize = lines.length === 1 ? availableSpace[b.axis] : line.largestSize[b.size]; const elementFittingAction = determineFittingAction(options[b.fitting], currentSize, availableSize); if (elementFittingAction === FITTING_ACTION.APPLY_STRETCHING) { sizesForThisElement[b.size] = Math.min(availableSize, sizesForThisElement[b.maxSize]); } else if (elementFittingAction === FITTING_ACTION.APPLY_SHRINKING) { sizesForThisElement[b.size] = Math.max(availableSize, sizesForThisElement[b.minSize]); } } } return sizesAllLines; } function determineFittingAction(fittingMode, currentSize, availableSize) { switch(fittingMode){ case FITTING_NONE: return FITTING_ACTION.NONE; case FITTING_STRETCH: if (currentSize < availableSize) { return FITTING_ACTION.APPLY_STRETCHING; } return FITTING_ACTION.NONE; case FITTING_SHRINK: if (currentSize >= availableSize) { return FITTING_ACTION.APPLY_SHRINKING; } return FITTING_ACTION.NONE; case FITTING_BOTH: if (currentSize < availableSize) { return FITTING_ACTION.APPLY_STRETCHING; } return FITTING_ACTION.APPLY_SHRINKING; default: throw new Error(`Unrecognized fitting mode: ${fittingMode}`); } } function calculateTotalSpace(sizes, axis) { const totalSizes = sumValues(sizes, axis.size); const totalSpacing = (sizes.length - 1) * options.spacing[axis.axis]; return totalSizes + totalSpacing; } function stretchSizesToFitContainer(sizesThisLine, idealRequiredSpace, axis) { const ascendingMaxSizeOrder = getTraversalOrder(sizesThisLine, axis.maxSize); const fittingProportions = getNormalizedValues(sizesThisLine, axis.fittingProportion); const fittingProportionSums = createSumArray(fittingProportions, ascendingMaxSizeOrder); let remainingUndershoot = availableSpace[axis.axis] - idealRequiredSpace; for(let i = 0; i < sizesThisLine.length; ++i){ const index = ascendingMaxSizeOrder[i]; const targetIncrease = calculateAdjustment(index, remainingUndershoot, fittingProportions, fittingProportionSums); const targetSize = sizesThisLine[index][axis.size] + targetIncrease; const maxSize = sizesThisLine[index][axis.maxSize]; const actualSize = Math.min(targetSize, maxSize); sizesThisLine[index][axis.size] = actualSize; const actualIncrease = Math.max(targetSize - actualSize, 0); const appliedIncrease = targetIncrease - actualIncrease; remainingUndershoot -= appliedIncrease; } } function shrinkSizesToFitContainer(sizesThisLine, idealRequiredSpace, axis) { const descendingMinSizeOrder = getTraversalOrder(sizesThisLine, axis.minSize, true); const fittingProportions = getNormalizedValues(sizesThisLine, axis.fittingProportion); const inverseFittingProportions = invertNormalizedValues(fittingProportions); const inverseFittingProportionSums = createSumArray(inverseFittingProportions, descendingMinSizeOrder); let remainingOvershoot = idealRequiredSpace - availableSpace[axis.axis]; for(let i = 0; i < sizesThisLine.length; ++i){ const index = descendingMinSizeOrder[i]; const targetReduction = calculateAdjustment(index, remainingOvershoot, inverseFittingProportions, inverseFittingProportionSums); const targetSize = sizesThisLine[index][axis.size] - targetReduction; const minSize = sizesThisLine[index][axis.minSize]; const actualSize = Math.max(targetSize, minSize); sizesThisLine[index][axis.size] = actualSize; const actualReduction = Math.max(actualSize - targetSize, 0); const appliedReduction = targetReduction - actualReduction; remainingOvershoot -= appliedReduction; } } function calculateAdjustment(index, remainingAdjustment, fittingProportions, fittingProportionSums) { const proportion = fittingProportions[index]; const sumOfRemainingProportions = fittingProportionSums[index]; if (Math.abs(proportion) < 1e-5 && Math.abs(sumOfRemainingProportions) < 1e-5) { return remainingAdjustment; } return remainingAdjustment * proportion / sumOfRemainingProportions; } function calculateBasePositions(lines, sizes) { const cursor = {}; cursor[a.axis] = 0; cursor[b.axis] = 0; lines[a.size] = Number.NEGATIVE_INFINITY; const positionsAllLines = []; for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ const line = lines[lineIndex]; if (line.length === 0) { positionsAllLines.push([]); continue; } const positionsThisLine = []; const sizesThisLine = sizes[lineIndex]; for(let elementIndex = 0; elementIndex < line.length; ++elementIndex){ const element = line[elementIndex]; const sizesThisElement = sizesThisLine[elementIndex]; cursor[b.axis] -= minExtentB(element, sizesThisElement); cursor[a.axis] -= minExtentA(element, sizesThisElement); positionsThisLine[elementIndex] = {}; positionsThisLine[elementIndex][a.axis] = cursor[a.axis]; positionsThisLine[elementIndex][b.axis] = cursor[b.axis]; cursor[b.axis] += minExtentB(element, sizesThisElement); cursor[a.axis] += maxExtentA(element, sizesThisElement) + options.spacing[a.axis]; } line[a.size] = cursor[a.axis] - options.spacing[a.axis]; line[b.size] = line.largestSize[b.size]; lines[a.size] = Math.max(lines[a.size], line[a.size]); cursor[a.axis] = 0; cursor[b.axis] += line[b.size] + options.spacing[b.axis]; positionsAllLines.push(positionsThisLine); } lines[b.size] = cursor[b.axis] - options.spacing[b.axis]; return positionsAllLines; } function applyAlignmentAndPadding(lines, sizes, positions) { const alignmentA = options.alignment[a.axis]; const alignmentB = options.alignment[b.axis]; const paddingA = options.padding[a.axis]; const paddingB = options.padding[b.axis]; for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ const line = lines[lineIndex]; const sizesThisLine = sizes[lineIndex]; const positionsThisLine = positions[lineIndex]; const axisAOffset = (availableSpace[a.axis] - line[a.size]) * alignmentA + paddingA; const axisBOffset = (availableSpace[b.axis] - lines[b.size]) * alignmentB + paddingB; for(let elementIndex = 0; elementIndex < line.length; ++elementIndex){ const withinLineAxisBOffset = (line[b.size] - sizesThisLine[elementIndex][b.size]) * options.alignment[b.axis]; positionsThisLine[elementIndex][a.axis] += axisAOffset; positionsThisLine[elementIndex][b.axis] += axisBOffset + withinLineAxisBOffset; } } } function applySizesAndPositions(lines, sizes, positions) { for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex){ const line = lines[lineIndex]; const sizesThisLine = sizes[lineIndex]; const positionsThisLine = positions[lineIndex]; for(let elementIndex = 0; elementIndex < line.length; ++elementIndex){ const element = line[elementIndex]; element[a.calculatedSize] = sizesThisLine[elementIndex][a.size]; element[b.calculatedSize] = sizesThisLine[elementIndex][b.size]; if (options.orientation === ORIENTATION_HORIZONTAL) { element.entity.setLocalPosition(positionsThisLine[elementIndex][a.axis], positionsThisLine[elementIndex][b.axis], element.entity.getLocalPosition().z); } else { element.entity.setLocalPosition(positionsThisLine[elementIndex][b.axis], positionsThisLine[elementIndex][a.axis], element.entity.getLocalPosition().z); } } } } function createLayoutInfo(lines) { const layoutWidth = lines.width; const layoutHeight = lines.height; const xOffset = (availableSpace.x - layoutWidth) * options.alignment.x + options.padding.x; const yOffset = (availableSpace.y - layoutHeight) * options.alignment.y + options.padding.y; return { bounds: new Vec4(xOffset, yOffset, layoutWidth, layoutHeight) }; } function getElementSizeProperties(elements) { const sizeProperties = []; for(let i = 0; i < elements.length; ++i){ const element = elements[i]; const minWidth = Math.max(getProperty(element, 'minWidth'), 0); const minHeight = Math.max(getProperty(element, 'minHeight'), 0); const maxWidth = Math.max(getProperty(element, 'maxWidth'), minWidth); const maxHeight = Math.max(getProperty(element, 'maxHeight'), minHeight); const width = clamp(getProperty(element, 'width'), minWidth, maxWidth); const height = clamp(getProperty(element, 'height'), minHeight, maxHeight); const fitWidthProportion = getProperty(element, 'fitWidthProportion'); const fitHeightProportion = getProperty(element, 'fitHeightProportion'); sizeProperties.push({ minWidth: minWidth, minHeight: minHeight, maxWidth: maxWidth, maxHeight: maxHeight, width: width, height: height, fitWidthProportion: fitWidthProportion, fitHeightProportion: fitHeightProportion }); } return sizeProperties; } function getProperty(element, propertyName) { const layoutChildComponent = element.entity.layoutchild; if (layoutChildComponent && layoutChildComponent.enabled && layoutChildComponent[propertyName] !== undefined && layoutChildComponent[propertyName] !== null) { return layoutChildComponent[propertyName]; } else if (element[propertyName] !== undefined) { return element[propertyName]; } return PROPERTY_DEFAULTS[propertyName]; } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function sumValues(items, propertyName) { return items.reduce((accumulator, current)=>{ return accumulator + current[propertyName]; }, 0); } function getNormalizedValues(items, propertyName) { const sum = sumValues(items, propertyName); const normalizedValues = []; const numItems = items.length; if (sum === 0) { for(let i = 0; i < numItems; ++i){ normalizedValues.push(1 / numItems); } } else { for(let i = 0; i < numItems; ++i){ normalizedValues.push(items[i][propertyName] / sum); } } return normalizedValues; } function invertNormalizedValues(values) { if (values.length === 1) { return [ 1 ]; } const invertedValues = []; const numValues = values.length; for(let i = 0; i < numValues; ++i){ invertedValues.push((1 - values[i]) / (numValues - 1)); } return invertedValues; } function getTraversalOrder(items, orderBy, descending) { items.forEach(assignIndex); return items.slice().sort((itemA, itemB)=>{ return descending ? itemB[orderBy] - itemA[orderBy] : itemA[orderBy] - itemB[orderBy]; }).map(getIndex); } function assignIndex(item, index) { item.index = index; } function getIndex(item) { return item.index; } function createSumArray(values, order) { const sumArray = []; sumArray[order[values.length - 1]] = values[order[values.length - 1]]; for(let i = values.length - 2; i >= 0; --i){ sumArray[order[i]] = sumArray[order[i + 1]] + values[order[i]]; } return sumArray; } return calculateAll; } const CALCULATE_FNS = {}; CALCULATE_FNS[ORIENTATION_HORIZONTAL] = createCalculator(ORIENTATION_HORIZONTAL); CALCULATE_FNS[ORIENTATION_VERTICAL] = createCalculator(ORIENTATION_VERTICAL); class LayoutCalculator { calculateLayout(elements, options) { const calculateFn = CALCULATE_FNS[options.orientation]; if (!calculateFn) { throw new Error(`Unrecognized orientation value: ${options.orientation}`); } else { return calculateFn(elements, options); } } } function getElement(entity) { return entity.element; } function isEnabledAndHasEnabledElement(entity) { return entity.enabled && entity.element && entity.element.enabled; } class LayoutGroupComponent extends Component { set orientation(value) { if (value !== this._orientation) { this._orientation = value; this._scheduleReflow(); } } get orientation() { return this._orientation; } set reverseX(value) { if (value !== this._reverseX) { this._reverseX = value; this._scheduleReflow(); } } get reverseX() { return this._reverseX; } set reverseY(value) { if (value !== this._reverseY) { this._reverseY = value; this._scheduleReflow(); } } get reverseY() { return this._reverseY; } set alignment(value) { if (!value.equals(this._alignment)) { this._alignment.copy(value); this._scheduleReflow(); } } get alignment() { return this._alignment; } set padding(value) { if (!value.equals(this._padding)) { this._padding.copy(value); this._scheduleReflow(); } } get padding() { return this._padding; } set spacing(value) { if (!value.equals(this._spacing)) { this._spacing.copy(value); this._scheduleReflow(); } } get spacing() { return this._spacing; } set widthFitting(value) { if (value !== this._widthFitting) { this._widthFitting = value; this._scheduleReflow(); } } get widthFitting() { return this._widthFitting; } set heightFitting(value) { if (value !== this._heightFitting) { this._heightFitting = value; this._scheduleReflow(); } } get heightFitting() { return this._heightFitting; } set wrap(value) { if (value !== this._wrap) { this._wrap = value; this._scheduleReflow(); } } get wrap() { return this._wrap; } _isSelfOrChild(entity) { return entity === this.entity || this.entity.children.indexOf(entity) !== -1; } _listenForReflowEvents(target, onOff) { if (target.element) { target.element[onOff]('enableelement', this._scheduleReflow, this); target.element[onOff]('disableelement', this._scheduleReflow, this); target.element[onOff]('resize', this._scheduleReflow, this); target.element[onOff]('set:pivot', this._scheduleReflow, this); } if (target.layoutchild) { target.layoutchild[onOff]('set_enabled', this._scheduleReflow, this); target.layoutchild[onOff]('resize', this._scheduleReflow, this); } } _onElementOrLayoutComponentAdd(entity) { if (this._isSelfOrChild(entity)) { this._listenForReflowEvents(entity, 'on'); this._scheduleReflow(); } } _onElementOrLayoutComponentRemove(entity) { if (this._isSelfOrChild(entity)) { this._listenForReflowEvents(entity, 'off'); this._scheduleReflow(); } } _onChildInsert(child) { this._listenForReflowEvents(child, 'on'); this._scheduleReflow(); } _onChildRemove(child) { this._listenForReflowEvents(child, 'off'); this._scheduleReflow(); } _scheduleReflow() { if (this.enabled && this.entity && this.entity.enabled && !this._isPerformingReflow) { this.system.scheduleReflow(this); } } reflow() { const container = getElement(this.entity); const elements = this.entity.children.filter(isEnabledAndHasEnabledElement).map(getElement); if (!container || elements.length === 0) { return; } const containerWidth = Math.max(container.calculatedWidth, 0); const containerHeight = Math.max(container.calculatedHeight, 0); const options = { orientation: this._orientation, reverseX: this._reverseX, reverseY: this._reverseY, alignment: this._alignment, padding: this._padding, spacing: this._spacing, widthFitting: this._widthFitting, heightFitting: this._heightFitting, wrap: this._wrap, containerSize: new Vec2(containerWidth, containerHeight) }; this._isPerformingReflow = true; const layoutInfo = this._layoutCalculator.calculateLayout(elements, options); this._isPerformingReflow = false; this.fire('reflow', layoutInfo); } onEnable() { this._scheduleReflow(); } onRemove() { this.entity.off('childinsert', this._onChildInsert, this); this.entity.off('childremove', this._onChildRemove, this); this._listenForReflowEvents(this.entity, 'off'); this.entity.children.forEach((child)=>{ this._listenForReflowEvents(child, 'off'); }); this.system.app.systems.element.off('add', this._onElementOrLayoutComponentAdd, this); this.system.app.systems.element.off('beforeremove', this._onElementOrLayoutComponentRemove, this); this.system.app.systems.layoutchild.off('add', this._onElementOrLayoutComponentAdd, this); this.system.app.systems.layoutchild.off('beforeremove', this._onElementOrLayoutComponentRemove, this); } constructor(system, entity){ super(system, entity), this._orientation = ORIENTATION_HORIZONTAL, this._reverseX = false, this._reverseY = true, this._alignment = new Vec2(0, 1), this._padding = new Vec4(), this._spacing = new Vec2(), this._widthFitting = FITTING_NONE, this._heightFitting = FITTING_NONE, this._wrap = false, this._layoutCalculator = new LayoutCalculator(); this._listenForReflowEvents(this.entity, 'on'); this.entity.children.forEach((child)=>{ this._listenForReflowEvents(child, 'on'); }); this.entity.on('childinsert', this._onChildInsert, this); this.entity.on('childremove', this._onChildRemove, this); system.app.systems.element.on('add', this._onElementOrLayoutComponentAdd, this); system.app.systems.element.on('beforeremove', this._onElementOrLayoutComponentRemove, this); system.app.systems.layoutchild.on('add', this._onElementOrLayoutComponentAdd, this); system.app.systems.layoutchild.on('beforeremove', this._onElementOrLayoutComponentRemove, this); } } class LayoutGroupComponentData { constructor(){ this.enabled = true; } } const _schema$c = [ 'enabled' ]; const MAX_ITERATIONS = 100; class LayoutGroupComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { if (data.enabled !== undefined) component.enabled = data.enabled; if (data.orientation !== undefined) component.orientation = data.orientation; if (data.reverseX !== undefined) component.reverseX = data.reverseX; if (data.reverseY !== undefined) component.reverseY = data.reverseY; if (data.alignment !== undefined) { component.alignment = Array.isArray(data.alignment) ? new Vec2(data.alignment) : data.alignment; } if (data.padding !== undefined) { component.padding = Array.isArray(data.padding) ? new Vec4(data.padding) : data.padding; } if (data.spacing !== undefined) { component.spacing = Array.isArray(data.spacing) ? new Vec2(data.spacing) : data.spacing; } if (data.widthFitting !== undefined) component.widthFitting = data.widthFitting; if (data.heightFitting !== undefined) component.heightFitting = data.heightFitting; if (data.wrap !== undefined) component.wrap = data.wrap; super.initializeComponentData(component, data, properties); } cloneComponent(entity, clone) { const layoutGroup = entity.layoutgroup; return this.addComponent(clone, { enabled: layoutGroup.enabled, orientation: layoutGroup.orientation, reverseX: layoutGroup.reverseX, reverseY: layoutGroup.reverseY, alignment: layoutGroup.alignment, padding: layoutGroup.padding, spacing: layoutGroup.spacing, widthFitting: layoutGroup.widthFitting, heightFitting: layoutGroup.heightFitting, wrap: layoutGroup.wrap }); } scheduleReflow(component) { if (this._reflowQueue.indexOf(component) === -1) { this._reflowQueue.push(component); } } _onPostUpdate() { this._processReflowQueue(); } _processReflowQueue() { if (this._reflowQueue.length === 0) { return; } let iterationCount = 0; while(this._reflowQueue.length > 0){ const queue = this._reflowQueue.slice(); this._reflowQueue.length = 0; queue.sort((componentA, componentB)=>{ return componentA.entity.graphDepth - componentB.entity.graphDepth; }); for(let i = 0; i < queue.length; ++i){ queue[i].reflow(); } if (++iterationCount >= MAX_ITERATIONS) { console.warn('Max reflow iterations limit reached, bailing.'); break; } } } _onRemoveComponent(entity, component) { component.onRemove(); } destroy() { super.destroy(); this.app.systems.off('postUpdate', this._onPostUpdate, this); } constructor(app){ super(app); this.id = 'layoutgroup'; this.ComponentType = LayoutGroupComponent; this.DataType = LayoutGroupComponentData; this.schema = _schema$c; this._reflowQueue = []; this.on('beforeremove', this._onRemoveComponent, this); this.app.systems.on('postUpdate', this._onPostUpdate, this); } } Component._buildAccessors(LayoutGroupComponent.prototype, _schema$c); class PrimitivesCache { destroy(device) { this.map.forEach((primData)=>primData.mesh.destroy()); } constructor(){ this.map = new Map(); } } const _primitivesCache = new DeviceCache(); const getShapePrimitive = (device, type)=>{ const cache = _primitivesCache.get(device, ()=>{ return new PrimitivesCache(); }); let primData = cache.map.get(type); if (!primData) { let mesh, area; switch(type){ case 'box': mesh = Mesh.fromGeometry(device, new BoxGeometry()); area = { x: 2, y: 2, z: 2, uv: 2.0 / 3 }; break; case 'capsule': mesh = Mesh.fromGeometry(device, new CapsuleGeometry({ radius: 0.5, height: 2 })); area = { x: Math.PI * 2, y: Math.PI, z: Math.PI * 2, uv: 1.0 / 3 + 1.0 / 3 / 3 * 2 }; break; case 'cone': mesh = Mesh.fromGeometry(device, new ConeGeometry({ baseRadius: 0.5, peakRadius: 0, height: 1 })); area = { x: 2.54, y: 2.54, z: 2.54, uv: 1.0 / 3 + 1.0 / 3 / 3 }; break; case 'cylinder': mesh = Mesh.fromGeometry(device, new CylinderGeometry({ radius: 0.5, height: 1 })); area = { x: Math.PI, y: 0.79 * 2, z: Math.PI, uv: 1.0 / 3 + 1.0 / 3 / 3 * 2 }; break; case 'plane': mesh = Mesh.fromGeometry(device, new PlaneGeometry({ halfExtents: new Vec2(0.5, 0.5), widthSegments: 1, lengthSegments: 1 })); area = { x: 0, y: 1, z: 0, uv: 1 }; break; case 'sphere': mesh = Mesh.fromGeometry(device, new SphereGeometry({ radius: 0.5 })); area = { x: Math.PI, y: Math.PI, z: Math.PI, uv: 1 }; break; case 'torus': mesh = Mesh.fromGeometry(device, new TorusGeometry({ tubeRadius: 0.2, ringRadius: 0.3 })); area = { x: Math.PI * 0.5 * 0.5 - Math.PI * 0.1 * 0.1, y: 0.4, z: 0.4, uv: 1 }; break; default: throw new Error(`Invalid primitive type: ${type}`); } mesh.incRefCount(); primData = { mesh: mesh, area: area }; cache.map.set(type, primData); } return primData; }; class ModelComponent extends Component { set meshInstances(value) { if (!this._model) { return; } this._model.meshInstances = value; } get meshInstances() { if (!this._model) { return null; } return this._model.meshInstances; } set customAabb(value) { this._customAabb = value; if (this._model) { const mi = this._model.meshInstances; if (mi) { for(let i = 0; i < mi.length; i++){ mi[i].setCustomAabb(this._customAabb); } } } } get customAabb() { return this._customAabb; } set type(value) { if (this._type === value) return; this._area = null; this._type = value; if (value === 'asset') { if (this._asset !== null) { this._bindModelAsset(this._asset); } else { this.model = null; } } else { const primData = getShapePrimitive(this.system.app.graphicsDevice, value); this._area = primData.area; const mesh = primData.mesh; const node = new GraphNode(); const model = new Model(); model.graph = node; model.meshInstances = [ new MeshInstance(mesh, this._material, node) ]; this.model = model; this._asset = null; } } get type() { return this._type; } set asset(value) { const assets = this.system.app.assets; let _id = value; if (value instanceof Asset) { _id = value.id; } if (this._asset !== _id) { if (this._asset) { assets.off(`add:${this._asset}`, this._onModelAssetAdded, this); const _prev = assets.get(this._asset); if (_prev) { this._unbindModelAsset(_prev); } } this._asset = _id; if (this._asset) { const asset = assets.get(this._asset); if (!asset) { this.model = null; assets.on(`add:${this._asset}`, this._onModelAssetAdded, this); } else { this._bindModelAsset(asset); } } else { this.model = null; } } } get asset() { return this._asset; } set model(value) { if (this._model === value) { return; } if (value && value._immutable) { return; } if (this._model) { this._model._immutable = false; this.removeModelFromLayers(); this._model.getGraph().destroy(); delete this._model._entity; if (this._clonedModel) { this._model.destroy(); this._clonedModel = false; } } this._model = value; if (this._model) { this._model._immutable = true; const meshInstances = this._model.meshInstances; for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].castShadow = this._castShadows; meshInstances[i].receiveShadow = this._receiveShadows; meshInstances[i].setCustomAabb(this._customAabb); } this.lightmapped = this._lightmapped; this.entity.addChild(this._model.graph); if (this.enabled && this.entity.enabled) { this.addModelToLayers(); } this._model._entity = this.entity; if (this.entity.animation) { this.entity.animation.setModel(this._model); } if (this.entity.anim) { this.entity.anim.rebind(); } if (this.type === 'asset') { this.mapping = this._mapping; } else { this._unsetMaterialEvents(); } } } get model() { return this._model; } set lightmapped(value) { if (value !== this._lightmapped) { this._lightmapped = value; if (this._model) { const mi = this._model.meshInstances; for(let i = 0; i < mi.length; i++){ mi[i].setLightmapped(value); } } } } get lightmapped() { return this._lightmapped; } set castShadows(value) { if (this._castShadows === value) return; const model = this._model; if (model) { const layers = this.layers; const scene = this.system.app.scene; if (this._castShadows && !value) { for(let i = 0; i < layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (!layer) continue; layer.removeShadowCasters(model.meshInstances); } } const meshInstances = model.meshInstances; for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].castShadow = value; } if (!this._castShadows && value) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); if (!layer) continue; layer.addShadowCasters(model.meshInstances); } } } this._castShadows = value; } get castShadows() { return this._castShadows; } set receiveShadows(value) { if (this._receiveShadows === value) return; this._receiveShadows = value; if (this._model) { const meshInstances = this._model.meshInstances; for(let i = 0, len = meshInstances.length; i < len; i++){ meshInstances[i].receiveShadow = value; } } } get receiveShadows() { return this._receiveShadows; } set castShadowsLightmap(value) { this._castShadowsLightmap = value; } get castShadowsLightmap() { return this._castShadowsLightmap; } set lightmapSizeMultiplier(value) { this._lightmapSizeMultiplier = value; } get lightmapSizeMultiplier() { return this._lightmapSizeMultiplier; } set layers(value) { const layers = this.system.app.scene.layers; if (this.meshInstances) { for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (!layer) continue; layer.removeMeshInstances(this.meshInstances); } } this._layers.length = 0; for(let i = 0; i < value.length; i++){ this._layers[i] = value[i]; } if (!this.enabled || !this.entity.enabled || !this.meshInstances) return; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (!layer) continue; layer.addMeshInstances(this.meshInstances); } } get layers() { return this._layers; } set batchGroupId(value) { if (this._batchGroupId === value) return; if (this.entity.enabled && this._batchGroupId >= 0) { this.system.app.batcher?.remove(BatchGroup.MODEL, this.batchGroupId, this.entity); } if (this.entity.enabled && value >= 0) { this.system.app.batcher?.insert(BatchGroup.MODEL, value, this.entity); } if (value < 0 && this._batchGroupId >= 0 && this.enabled && this.entity.enabled) { this.addModelToLayers(); } this._batchGroupId = value; } get batchGroupId() { return this._batchGroupId; } set materialAsset(value) { let _id = value; if (value instanceof Asset) { _id = value.id; } const assets = this.system.app.assets; if (_id !== this._materialAsset) { if (this._materialAsset) { assets.off(`add:${this._materialAsset}`, this._onMaterialAssetAdd, this); const _prev = assets.get(this._materialAsset); if (_prev) { this._unbindMaterialAsset(_prev); } } this._materialAsset = _id; if (this._materialAsset) { const asset = assets.get(this._materialAsset); if (!asset) { this._setMaterial(this.system.defaultMaterial); assets.on(`add:${this._materialAsset}`, this._onMaterialAssetAdd, this); } else { this._bindMaterialAsset(asset); } } else { this._setMaterial(this.system.defaultMaterial); } } } get materialAsset() { return this._materialAsset; } set material(value) { if (this._material === value) { return; } this.materialAsset = null; this._setMaterial(value); } get material() { return this._material; } set mapping(value) { if (this._type !== 'asset') { return; } this._unsetMaterialEvents(); if (!value) { value = {}; } this._mapping = value; if (!this._model) return; const meshInstances = this._model.meshInstances; const modelAsset = this.asset ? this.system.app.assets.get(this.asset) : null; const assetMapping = modelAsset ? modelAsset.data.mapping : null; let asset = null; for(let i = 0, len = meshInstances.length; i < len; i++){ if (value[i] !== undefined) { if (value[i]) { asset = this.system.app.assets.get(value[i]); this._loadAndSetMeshInstanceMaterial(asset, meshInstances[i], i); } else { meshInstances[i].material = this.system.defaultMaterial; } } else if (assetMapping) { if (assetMapping[i] && (assetMapping[i].material || assetMapping[i].path)) { if (assetMapping[i].material !== undefined) { asset = this.system.app.assets.get(assetMapping[i].material); } else if (assetMapping[i].path !== undefined) { const url = this._getMaterialAssetUrl(assetMapping[i].path); if (url) { asset = this.system.app.assets.getByUrl(url); } } this._loadAndSetMeshInstanceMaterial(asset, meshInstances[i], i); } else { meshInstances[i].material = this.system.defaultMaterial; } } } } get mapping() { return this._mapping; } addModelToLayers() { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (layer) { layer.addMeshInstances(this.meshInstances); } } } removeModelFromLayers() { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (!layer) continue; layer.removeMeshInstances(this.meshInstances); } } onRemoveChild() { if (this._model) { this.removeModelFromLayers(); } } onInsertChild() { if (this._model && this.enabled && this.entity.enabled) { this.addModelToLayers(); } } onRemove() { this.asset = null; this.model = null; this.materialAsset = null; this._unsetMaterialEvents(); this.entity.off('remove', this.onRemoveChild, this); this.entity.off('insert', this.onInsertChild, this); } onLayersChanged(oldComp, newComp) { this.addModelToLayers(); oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.addMeshInstances(this.meshInstances); } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.removeMeshInstances(this.meshInstances); } _setMaterialEvent(index, event, id, handler) { const evt = `${event}:${id}`; this.system.app.assets.on(evt, handler, this); if (!this._materialEvents) { this._materialEvents = []; } if (!this._materialEvents[index]) { this._materialEvents[index] = {}; } this._materialEvents[index][evt] = { id: id, handler: handler }; } _unsetMaterialEvents() { const assets = this.system.app.assets; const events = this._materialEvents; if (!events) { return; } for(let i = 0, len = events.length; i < len; i++){ if (!events[i]) continue; const evt = events[i]; for(const key in evt){ assets.off(key, evt[key].handler, this); } } this._materialEvents = null; } _getAssetByIdOrPath(idOrPath) { let asset = null; const isPath = isNaN(parseInt(idOrPath, 10)); if (!isPath) { asset = this.system.app.assets.get(idOrPath); } else if (this.asset) { const url = this._getMaterialAssetUrl(idOrPath); if (url) { asset = this.system.app.assets.getByUrl(url); } } return asset; } _getMaterialAssetUrl(path) { if (!this.asset) return null; const modelAsset = this.system.app.assets.get(this.asset); return modelAsset ? modelAsset.getAbsoluteUrl(path) : null; } _loadAndSetMeshInstanceMaterial(materialAsset, meshInstance, index) { const assets = this.system.app.assets; if (!materialAsset) { return; } if (materialAsset.resource) { meshInstance.material = materialAsset.resource; this._setMaterialEvent(index, 'remove', materialAsset.id, function() { meshInstance.material = this.system.defaultMaterial; }); } else { this._setMaterialEvent(index, 'load', materialAsset.id, function(asset) { meshInstance.material = asset.resource; this._setMaterialEvent(index, 'remove', materialAsset.id, function() { meshInstance.material = this.system.defaultMaterial; }); }); if (this.enabled && this.entity.enabled) { assets.load(materialAsset); } } } onEnable() { const app = this.system.app; const scene = app.scene; const layers = scene?.layers; this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } const isAsset = this._type === 'asset'; let asset; if (this._model) { this.addModelToLayers(); } else if (isAsset && this._asset) { asset = app.assets.get(this._asset); if (asset && asset.resource !== this._model) { this._bindModelAsset(asset); } } if (this._materialAsset) { asset = app.assets.get(this._materialAsset); if (asset && asset.resource !== this._material) { this._bindMaterialAsset(asset); } } if (isAsset) { if (this._mapping) { for(const index in this._mapping){ if (this._mapping[index]) { asset = this._getAssetByIdOrPath(this._mapping[index]); if (asset && !asset.resource) { app.assets.load(asset); } } } } } if (this._batchGroupId >= 0) { app.batcher?.insert(BatchGroup.MODEL, this.batchGroupId, this.entity); } } onDisable() { const app = this.system.app; const scene = app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } if (this._batchGroupId >= 0) { app.batcher?.remove(BatchGroup.MODEL, this.batchGroupId, this.entity); } if (this._model) { this.removeModelFromLayers(); } } hide() { if (this._model) { const instances = this._model.meshInstances; for(let i = 0, l = instances.length; i < l; i++){ instances[i].visible = false; } } } show() { if (this._model) { const instances = this._model.meshInstances; for(let i = 0, l = instances.length; i < l; i++){ instances[i].visible = true; } } } _bindMaterialAsset(asset) { asset.on('load', this._onMaterialAssetLoad, this); asset.on('unload', this._onMaterialAssetUnload, this); asset.on('remove', this._onMaterialAssetRemove, this); asset.on('change', this._onMaterialAssetChange, this); if (asset.resource) { this._onMaterialAssetLoad(asset); } else { if (!this.enabled || !this.entity.enabled) return; this.system.app.assets.load(asset); } } _unbindMaterialAsset(asset) { asset.off('load', this._onMaterialAssetLoad, this); asset.off('unload', this._onMaterialAssetUnload, this); asset.off('remove', this._onMaterialAssetRemove, this); asset.off('change', this._onMaterialAssetChange, this); } _onMaterialAssetAdd(asset) { this.system.app.assets.off(`add:${asset.id}`, this._onMaterialAssetAdd, this); if (this._materialAsset === asset.id) { this._bindMaterialAsset(asset); } } _onMaterialAssetLoad(asset) { this._setMaterial(asset.resource); } _onMaterialAssetUnload(asset) { this._setMaterial(this.system.defaultMaterial); } _onMaterialAssetRemove(asset) { this._onMaterialAssetUnload(asset); } _onMaterialAssetChange(asset) {} _bindModelAsset(asset) { this._unbindModelAsset(asset); asset.on('load', this._onModelAssetLoad, this); asset.on('unload', this._onModelAssetUnload, this); asset.on('change', this._onModelAssetChange, this); asset.on('remove', this._onModelAssetRemove, this); if (asset.resource) { this._onModelAssetLoad(asset); } else { if (!this.enabled || !this.entity.enabled) return; this.system.app.assets.load(asset); } } _unbindModelAsset(asset) { asset.off('load', this._onModelAssetLoad, this); asset.off('unload', this._onModelAssetUnload, this); asset.off('change', this._onModelAssetChange, this); asset.off('remove', this._onModelAssetRemove, this); } _onModelAssetAdded(asset) { this.system.app.assets.off(`add:${asset.id}`, this._onModelAssetAdded, this); if (asset.id === this._asset) { this._bindModelAsset(asset); } } _onModelAssetLoad(asset) { this.model = asset.resource.clone(); this._clonedModel = true; } _onModelAssetUnload(asset) { this.model = null; } _onModelAssetChange(asset, attr, _new, _old) { if (attr === 'data') { this.mapping = this._mapping; } } _onModelAssetRemove(asset) { this.model = null; } _setMaterial(material) { if (this._material === material) { return; } this._material = material; const model = this._model; if (model && this._type !== 'asset') { const meshInstances = model.meshInstances; for(let i = 0, len = meshInstances.length; i < len; i++){ meshInstances[i].material = material; } } } constructor(system, entity){ super(system, entity), this._type = 'asset', this._asset = null, this._model = null, this._mapping = {}, this._castShadows = true, this._receiveShadows = true, this._materialAsset = null, this._castShadowsLightmap = true, this._lightmapped = false, this._lightmapSizeMultiplier = 1, this.isStatic = false, this._layers = [ LAYERID_WORLD ], this._batchGroupId = -1, this._customAabb = null, this._area = null, this._materialEvents = null, this._clonedModel = false, this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null; this._material = system.defaultMaterial; entity.on('remove', this.onRemoveChild, this); entity.on('removehierarchy', this.onRemoveChild, this); entity.on('insert', this.onInsertChild, this); entity.on('inserthierarchy', this.onInsertChild, this); } } class ModelComponentData { constructor(){ this.enabled = true; } } const _schema$b = [ 'enabled' ]; class ModelComponentSystem extends ComponentSystem { initializeComponentData(component, _data, properties) { properties = [ 'material', 'materialAsset', 'asset', 'castShadows', 'receiveShadows', 'castShadowsLightmap', 'lightmapped', 'lightmapSizeMultiplier', 'type', 'mapping', 'layers', 'isStatic', 'batchGroupId' ]; if (_data.batchGroupId === null || _data.batchGroupId === undefined) { _data.batchGroupId = -1; } if (_data.layers && _data.layers.length) { _data.layers = _data.layers.slice(0); } for(let i = 0; i < properties.length; i++){ if (_data.hasOwnProperty(properties[i])) { component[properties[i]] = _data[properties[i]]; } } if (_data.aabbCenter && _data.aabbHalfExtents) { component.customAabb = new BoundingBox(new Vec3(_data.aabbCenter), new Vec3(_data.aabbHalfExtents)); } super.initializeComponentData(component, _data, [ 'enabled' ]); } cloneComponent(entity, clone) { const data = { type: entity.model.type, asset: entity.model.asset, castShadows: entity.model.castShadows, receiveShadows: entity.model.receiveShadows, castShadowsLightmap: entity.model.castShadowsLightmap, lightmapped: entity.model.lightmapped, lightmapSizeMultiplier: entity.model.lightmapSizeMultiplier, isStatic: entity.model.isStatic, enabled: entity.model.enabled, layers: entity.model.layers, batchGroupId: entity.model.batchGroupId, mapping: extend({}, entity.model.mapping) }; let materialAsset = entity.model.materialAsset; if (!(materialAsset instanceof Asset) && materialAsset != null) { materialAsset = this.app.assets.get(materialAsset); } const material = entity.model.material; if (!material || material === this.defaultMaterial || !materialAsset || material === materialAsset.resource) { data.materialAsset = materialAsset; } const component = this.addComponent(clone, data); if (entity.model.model && entity.model.type === 'asset' && !entity.model.asset) { component.model = entity.model.model.clone(); component._clonedModel = true; } if (!data.materialAsset) { component.material = material; } if (entity.model.model) { const meshInstances = entity.model.model.meshInstances; const meshInstancesClone = component.model.meshInstances; for(let i = 0; i < meshInstances.length; i++){ meshInstancesClone[i].mask = meshInstances[i].mask; meshInstancesClone[i].material = meshInstances[i].material; meshInstancesClone[i].layer = meshInstances[i].layer; meshInstancesClone[i].receiveShadow = meshInstances[i].receiveShadow; } } if (entity.model.customAabb) { component.customAabb = entity.model.customAabb.clone(); } return component; } onRemove(entity, component) { component.onRemove(); } constructor(app){ super(app); this.id = 'model'; this.ComponentType = ModelComponent; this.DataType = ModelComponentData; this.schema = _schema$b; this.defaultMaterial = getDefaultMaterial(app.graphicsDevice); this.on('beforeremove', this.onRemove, this); } } Component._buildAccessors(ModelComponent.prototype, _schema$b); const SIMPLE_PROPERTIES = [ 'emitterExtents', 'emitterRadius', 'emitterExtentsInner', 'emitterRadiusInner', 'loop', 'initialVelocity', 'animSpeed', 'normalMap', 'particleNormal' ]; const COMPLEX_PROPERTIES = [ 'numParticles', 'lifetime', 'rate', 'rate2', 'startAngle', 'startAngle2', 'lighting', 'halfLambert', 'intensity', 'wrap', 'wrapBounds', 'depthWrite', 'noFog', 'sort', 'stretch', 'alignToMotion', 'preWarm', 'emitterShape', 'animTilesX', 'animTilesY', 'animStartFrame', 'animNumFrames', 'animNumAnimations', 'animIndex', 'randomizeAnimIndex', 'animLoop', 'colorMap', 'localSpace', 'screenSpace', 'orientation' ]; const GRAPH_PROPERTIES = [ 'scaleGraph', 'scaleGraph2', 'colorGraph', 'colorGraph2', 'alphaGraph', 'alphaGraph2', 'velocityGraph', 'velocityGraph2', 'localVelocityGraph', 'localVelocityGraph2', 'rotationSpeedGraph', 'rotationSpeedGraph2', 'radialSpeedGraph', 'radialSpeedGraph2' ]; const ASSET_PROPERTIES = [ 'colorMapAsset', 'normalMapAsset', 'meshAsset', 'renderAsset' ]; let depthLayer; class ParticleSystemComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) { this._setValue('enabled', arg); } get enabled() { return this.data.enabled; } set autoPlay(arg) { this._setValue('autoPlay', arg); } get autoPlay() { return this.data.autoPlay; } set numParticles(arg) { this._setValue('numParticles', arg); } get numParticles() { return this.data.numParticles; } set lifetime(arg) { this._setValue('lifetime', arg); } get lifetime() { return this.data.lifetime; } set rate(arg) { this._setValue('rate', arg); } get rate() { return this.data.rate; } set rate2(arg) { this._setValue('rate2', arg); } get rate2() { return this.data.rate2; } set startAngle(arg) { this._setValue('startAngle', arg); } get startAngle() { return this.data.startAngle; } set startAngle2(arg) { this._setValue('startAngle2', arg); } get startAngle2() { return this.data.startAngle2; } set loop(arg) { this._setValue('loop', arg); } get loop() { return this.data.loop; } set preWarm(arg) { this._setValue('preWarm', arg); } get preWarm() { return this.data.preWarm; } set lighting(arg) { this._setValue('lighting', arg); } get lighting() { return this.data.lighting; } set halfLambert(arg) { this._setValue('halfLambert', arg); } get halfLambert() { return this.data.halfLambert; } set intensity(arg) { this._setValue('intensity', arg); } get intensity() { return this.data.intensity; } set depthWrite(arg) { this._setValue('depthWrite', arg); } get depthWrite() { return this.data.depthWrite; } set noFog(arg) { this._setValue('noFog', arg); } get noFog() { return this.data.noFog; } set depthSoftening(arg) { this._setValue('depthSoftening', arg); } get depthSoftening() { return this.data.depthSoftening; } set sort(arg) { this._setValue('sort', arg); } get sort() { return this.data.sort; } set blendType(arg) { this._setValue('blendType', arg); } get blendType() { return this.data.blendType; } set stretch(arg) { this._setValue('stretch', arg); } get stretch() { return this.data.stretch; } set alignToMotion(arg) { this._setValue('alignToMotion', arg); } get alignToMotion() { return this.data.alignToMotion; } set emitterShape(arg) { this._setValue('emitterShape', arg); } get emitterShape() { return this.data.emitterShape; } set emitterExtents(arg) { this._setValue('emitterExtents', arg); } get emitterExtents() { return this.data.emitterExtents; } set emitterExtentsInner(arg) { this._setValue('emitterExtentsInner', arg); } get emitterExtentsInner() { return this.data.emitterExtentsInner; } set emitterRadius(arg) { this._setValue('emitterRadius', arg); } get emitterRadius() { return this.data.emitterRadius; } set emitterRadiusInner(arg) { this._setValue('emitterRadiusInner', arg); } get emitterRadiusInner() { return this.data.emitterRadiusInner; } set initialVelocity(arg) { this._setValue('initialVelocity', arg); } get initialVelocity() { return this.data.initialVelocity; } set wrap(arg) { this._setValue('wrap', arg); } get wrap() { return this.data.wrap; } set wrapBounds(arg) { this._setValue('wrapBounds', arg); } get wrapBounds() { return this.data.wrapBounds; } set localSpace(arg) { this._setValue('localSpace', arg); } get localSpace() { return this.data.localSpace; } set screenSpace(arg) { this._setValue('screenSpace', arg); } get screenSpace() { return this.data.screenSpace; } set colorMapAsset(arg) { this._setValue('colorMapAsset', arg); } get colorMapAsset() { return this.data.colorMapAsset; } set normalMapAsset(arg) { this._setValue('normalMapAsset', arg); } get normalMapAsset() { return this.data.normalMapAsset; } set mesh(arg) { this._setValue('mesh', arg); } get mesh() { return this.data.mesh; } set meshAsset(arg) { this._setValue('meshAsset', arg); } get meshAsset() { return this.data.meshAsset; } set renderAsset(arg) { this._setValue('renderAsset', arg); } get renderAsset() { return this.data.renderAsset; } set orientation(arg) { this._setValue('orientation', arg); } get orientation() { return this.data.orientation; } set particleNormal(arg) { this._setValue('particleNormal', arg); } get particleNormal() { return this.data.particleNormal; } set localVelocityGraph(arg) { this._setValue('localVelocityGraph', arg); } get localVelocityGraph() { return this.data.localVelocityGraph; } set localVelocityGraph2(arg) { this._setValue('localVelocityGraph2', arg); } get localVelocityGraph2() { return this.data.localVelocityGraph2; } set velocityGraph(arg) { this._setValue('velocityGraph', arg); } get velocityGraph() { return this.data.velocityGraph; } set velocityGraph2(arg) { this._setValue('velocityGraph2', arg); } get velocityGraph2() { return this.data.velocityGraph2; } set rotationSpeedGraph(arg) { this._setValue('rotationSpeedGraph', arg); } get rotationSpeedGraph() { return this.data.rotationSpeedGraph; } set rotationSpeedGraph2(arg) { this._setValue('rotationSpeedGraph2', arg); } get rotationSpeedGraph2() { return this.data.rotationSpeedGraph2; } set radialSpeedGraph(arg) { this._setValue('radialSpeedGraph', arg); } get radialSpeedGraph() { return this.data.radialSpeedGraph; } set radialSpeedGraph2(arg) { this._setValue('radialSpeedGraph2', arg); } get radialSpeedGraph2() { return this.data.radialSpeedGraph2; } set scaleGraph(arg) { this._setValue('scaleGraph', arg); } get scaleGraph() { return this.data.scaleGraph; } set scaleGraph2(arg) { this._setValue('scaleGraph2', arg); } get scaleGraph2() { return this.data.scaleGraph2; } set colorGraph(arg) { this._setValue('colorGraph', arg); } get colorGraph() { return this.data.colorGraph; } set colorGraph2(arg) { this._setValue('colorGraph2', arg); } get colorGraph2() { return this.data.colorGraph2; } set alphaGraph(arg) { this._setValue('alphaGraph', arg); } get alphaGraph() { return this.data.alphaGraph; } set alphaGraph2(arg) { this._setValue('alphaGraph2', arg); } get alphaGraph2() { return this.data.alphaGraph2; } set colorMap(arg) { this._setValue('colorMap', arg); } get colorMap() { return this.data.colorMap; } set normalMap(arg) { this._setValue('normalMap', arg); } get normalMap() { return this.data.normalMap; } set animTilesX(arg) { this._setValue('animTilesX', arg); } get animTilesX() { return this.data.animTilesX; } set animTilesY(arg) { this._setValue('animTilesY', arg); } get animTilesY() { return this.data.animTilesY; } set animStartFrame(arg) { this._setValue('animStartFrame', arg); } get animStartFrame() { return this.data.animStartFrame; } set animNumFrames(arg) { this._setValue('animNumFrames', arg); } get animNumFrames() { return this.data.animNumFrames; } set animNumAnimations(arg) { this._setValue('animNumAnimations', arg); } get animNumAnimations() { return this.data.animNumAnimations; } set animIndex(arg) { this._setValue('animIndex', arg); } get animIndex() { return this.data.animIndex; } set randomizeAnimIndex(arg) { this._setValue('randomizeAnimIndex', arg); } get randomizeAnimIndex() { return this.data.randomizeAnimIndex; } set animSpeed(arg) { this._setValue('animSpeed', arg); } get animSpeed() { return this.data.animSpeed; } set animLoop(arg) { this._setValue('animLoop', arg); } get animLoop() { return this.data.animLoop; } set layers(arg) { this._setValue('layers', arg); } get layers() { return this.data.layers; } set drawOrder(drawOrder) { this._drawOrder = drawOrder; if (this.emitter) { this.emitter.drawOrder = drawOrder; } } get drawOrder() { return this._drawOrder; } _setValue(name, value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); } addMeshInstanceToLayers() { if (!this.emitter) return; for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (!layer) continue; layer.addMeshInstances([ this.emitter.meshInstance ]); this.emitter._layer = layer; } } removeMeshInstanceFromLayers() { if (!this.emitter) return; for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (!layer) continue; layer.removeMeshInstances([ this.emitter.meshInstance ]); } } onSetLayers(name, oldValue, newValue) { if (!this.emitter) return; for(let i = 0; i < oldValue.length; i++){ const layer = this.system.app.scene.layers.getLayerById(oldValue[i]); if (!layer) continue; layer.removeMeshInstances([ this.emitter.meshInstance ]); } if (!this.enabled || !this.entity.enabled) return; for(let i = 0; i < newValue.length; i++){ const layer = this.system.app.scene.layers.getLayerById(newValue[i]); if (!layer) continue; layer.addMeshInstances([ this.emitter.meshInstance ]); } } onLayersChanged(oldComp, newComp) { this.addMeshInstanceToLayers(); oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { if (!this.emitter) return; const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.addMeshInstances([ this.emitter.meshInstance ]); } onLayerRemoved(layer) { if (!this.emitter) return; const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.removeMeshInstances([ this.emitter.meshInstance ]); } _bindColorMapAsset(asset) { asset.on('load', this._onColorMapAssetLoad, this); asset.on('unload', this._onColorMapAssetUnload, this); asset.on('remove', this._onColorMapAssetRemove, this); asset.on('change', this._onColorMapAssetChange, this); if (asset.resource) { this._onColorMapAssetLoad(asset); } else { if (!this.enabled || !this.entity.enabled) return; this.system.app.assets.load(asset); } } _unbindColorMapAsset(asset) { asset.off('load', this._onColorMapAssetLoad, this); asset.off('unload', this._onColorMapAssetUnload, this); asset.off('remove', this._onColorMapAssetRemove, this); asset.off('change', this._onColorMapAssetChange, this); } _onColorMapAssetLoad(asset) { this.colorMap = asset.resource; } _onColorMapAssetUnload(asset) { this.colorMap = null; } _onColorMapAssetRemove(asset) { this._onColorMapAssetUnload(asset); } _onColorMapAssetChange(asset) {} onSetColorMapAsset(name, oldValue, newValue) { const assets = this.system.app.assets; if (oldValue) { const asset = assets.get(oldValue); if (asset) { this._unbindColorMapAsset(asset); } } if (newValue) { if (newValue instanceof Asset) { this.data.colorMapAsset = newValue.id; newValue = newValue.id; } const asset = assets.get(newValue); if (asset) { this._bindColorMapAsset(asset); } else { assets.once(`add:${newValue}`, (asset)=>{ this._bindColorMapAsset(asset); }); } } else { this.colorMap = null; } } _bindNormalMapAsset(asset) { asset.on('load', this._onNormalMapAssetLoad, this); asset.on('unload', this._onNormalMapAssetUnload, this); asset.on('remove', this._onNormalMapAssetRemove, this); asset.on('change', this._onNormalMapAssetChange, this); if (asset.resource) { this._onNormalMapAssetLoad(asset); } else { if (!this.enabled || !this.entity.enabled) return; this.system.app.assets.load(asset); } } _unbindNormalMapAsset(asset) { asset.off('load', this._onNormalMapAssetLoad, this); asset.off('unload', this._onNormalMapAssetUnload, this); asset.off('remove', this._onNormalMapAssetRemove, this); asset.off('change', this._onNormalMapAssetChange, this); } _onNormalMapAssetLoad(asset) { this.normalMap = asset.resource; } _onNormalMapAssetUnload(asset) { this.normalMap = null; } _onNormalMapAssetRemove(asset) { this._onNormalMapAssetUnload(asset); } _onNormalMapAssetChange(asset) {} onSetNormalMapAsset(name, oldValue, newValue) { const assets = this.system.app.assets; if (oldValue) { const asset = assets.get(oldValue); if (asset) { this._unbindNormalMapAsset(asset); } } if (newValue) { if (newValue instanceof Asset) { this.data.normalMapAsset = newValue.id; newValue = newValue.id; } const asset = assets.get(newValue); if (asset) { this._bindNormalMapAsset(asset); } else { assets.once(`add:${newValue}`, (asset)=>{ this._bindNormalMapAsset(asset); }); } } else { this.normalMap = null; } } _bindMeshAsset(asset) { asset.on('load', this._onMeshAssetLoad, this); asset.on('unload', this._onMeshAssetUnload, this); asset.on('remove', this._onMeshAssetRemove, this); asset.on('change', this._onMeshAssetChange, this); if (asset.resource) { this._onMeshAssetLoad(asset); } else { if (!this.enabled || !this.entity.enabled) return; this.system.app.assets.load(asset); } } _unbindMeshAsset(asset) { asset.off('load', this._onMeshAssetLoad, this); asset.off('unload', this._onMeshAssetUnload, this); asset.off('remove', this._onMeshAssetRemove, this); asset.off('change', this._onMeshAssetChange, this); } _onMeshAssetLoad(asset) { this._onMeshChanged(asset.resource); } _onMeshAssetUnload(asset) { this.mesh = null; } _onMeshAssetRemove(asset) { this._onMeshAssetUnload(asset); } _onMeshAssetChange(asset) {} onSetMeshAsset(name, oldValue, newValue) { const assets = this.system.app.assets; if (oldValue) { const asset = assets.get(oldValue); if (asset) { this._unbindMeshAsset(asset); } } if (newValue) { if (newValue instanceof Asset) { this.data.meshAsset = newValue.id; newValue = newValue.id; } const asset = assets.get(newValue); if (asset) { this._bindMeshAsset(asset); } } else { this._onMeshChanged(null); } } onSetMesh(name, oldValue, newValue) { if (!newValue || newValue instanceof Asset || typeof newValue === 'number') { this.meshAsset = newValue; } else { this._onMeshChanged(newValue); } } _onMeshChanged(mesh) { if (mesh && !(mesh instanceof Mesh)) { if (mesh.meshInstances[0]) { mesh = mesh.meshInstances[0].mesh; } else { mesh = null; } } this.data.mesh = mesh; if (this.emitter) { this.emitter.mesh = mesh; this.emitter.resetMaterial(); this.rebuild(); } } onSetRenderAsset(name, oldValue, newValue) { const assets = this.system.app.assets; if (oldValue) { const asset = assets.get(oldValue); if (asset) { this._unbindRenderAsset(asset); } } if (newValue) { if (newValue instanceof Asset) { this.data.renderAsset = newValue.id; newValue = newValue.id; } const asset = assets.get(newValue); if (asset) { this._bindRenderAsset(asset); } } else { this._onRenderChanged(null); } } _bindRenderAsset(asset) { asset.on('load', this._onRenderAssetLoad, this); asset.on('unload', this._onRenderAssetUnload, this); asset.on('remove', this._onRenderAssetRemove, this); if (asset.resource) { this._onRenderAssetLoad(asset); } else { if (!this.enabled || !this.entity.enabled) return; this.system.app.assets.load(asset); } } _unbindRenderAsset(asset) { asset.off('load', this._onRenderAssetLoad, this); asset.off('unload', this._onRenderAssetUnload, this); asset.off('remove', this._onRenderAssetRemove, this); this._evtSetMeshes?.off(); this._evtSetMeshes = null; } _onRenderAssetLoad(asset) { this._onRenderChanged(asset.resource); } _onRenderAssetUnload(asset) { this._onRenderChanged(null); } _onRenderAssetRemove(asset) { this._onRenderAssetUnload(asset); } _onRenderChanged(render) { if (!render) { this._onMeshChanged(null); return; } this._evtSetMeshes?.off(); this._evtSetMeshes = render.on('set:meshes', this._onRenderSetMeshes, this); if (render.meshes) { this._onRenderSetMeshes(render.meshes); } } _onRenderSetMeshes(meshes) { this._onMeshChanged(meshes && meshes[0]); } onSetLoop(name, oldValue, newValue) { if (this.emitter) { this.emitter[name] = newValue; this.emitter.resetTime(); } } onSetBlendType(name, oldValue, newValue) { if (this.emitter) { this.emitter[name] = newValue; this.emitter.material.blendType = newValue; this.emitter.resetMaterial(); this.rebuild(); } } _requestDepth() { if (this._requestedDepth) return; if (!depthLayer) depthLayer = this.system.app.scene.layers.getLayerById(LAYERID_DEPTH); if (depthLayer) { depthLayer.incrementCounter(); this._requestedDepth = true; } } _releaseDepth() { if (!this._requestedDepth) return; if (depthLayer) { depthLayer.decrementCounter(); this._requestedDepth = false; } } onSetDepthSoftening(name, oldValue, newValue) { if (oldValue !== newValue) { if (newValue) { if (this.enabled && this.entity.enabled) this._requestDepth(); if (this.emitter) this.emitter[name] = newValue; } else { if (this.enabled && this.entity.enabled) this._releaseDepth(); if (this.emitter) this.emitter[name] = newValue; } if (this.emitter) { this.reset(); this.emitter.resetMaterial(); this.rebuild(); } } } onSetSimpleProperty(name, oldValue, newValue) { if (this.emitter) { this.emitter[name] = newValue; this.emitter.resetMaterial(); } } onSetComplexProperty(name, oldValue, newValue) { if (this.emitter) { this.emitter[name] = newValue; this.emitter.resetMaterial(); this.rebuild(); this.reset(); } } onSetGraphProperty(name, oldValue, newValue) { if (this.emitter) { this.emitter[name] = newValue; this.emitter.rebuildGraphs(); this.emitter.resetMaterial(); } } onEnable() { const scene = this.system.app.scene; const layers = scene.layers; const data = this.data; for(let i = 0, len = ASSET_PROPERTIES.length; i < len; i++){ let asset = data[ASSET_PROPERTIES[i]]; if (asset) { if (!(asset instanceof Asset)) { const id = parseInt(asset, 10); if (id >= 0) { asset = this.system.app.assets.get(asset); } else { continue; } } if (asset && !asset.resource) { this.system.app.assets.load(asset); } } } if (this.system.app.graphicsDevice.disableParticleSystem) { return; } if (!this.emitter) { let mesh = data.mesh; if (!(mesh instanceof Mesh)) { mesh = null; } this.emitter = new ParticleEmitter(this.system.app.graphicsDevice, { numParticles: data.numParticles, emitterExtents: data.emitterExtents, emitterExtentsInner: data.emitterExtentsInner, emitterRadius: data.emitterRadius, emitterRadiusInner: data.emitterRadiusInner, emitterShape: data.emitterShape, initialVelocity: data.initialVelocity, wrap: data.wrap, localSpace: data.localSpace, screenSpace: data.screenSpace, wrapBounds: data.wrapBounds, lifetime: data.lifetime, rate: data.rate, rate2: data.rate2, orientation: data.orientation, particleNormal: data.particleNormal, animTilesX: data.animTilesX, animTilesY: data.animTilesY, animStartFrame: data.animStartFrame, animNumFrames: data.animNumFrames, animNumAnimations: data.animNumAnimations, animIndex: data.animIndex, randomizeAnimIndex: data.randomizeAnimIndex, animSpeed: data.animSpeed, animLoop: data.animLoop, startAngle: data.startAngle, startAngle2: data.startAngle2, scaleGraph: data.scaleGraph, scaleGraph2: data.scaleGraph2, colorGraph: data.colorGraph, colorGraph2: data.colorGraph2, alphaGraph: data.alphaGraph, alphaGraph2: data.alphaGraph2, localVelocityGraph: data.localVelocityGraph, localVelocityGraph2: data.localVelocityGraph2, velocityGraph: data.velocityGraph, velocityGraph2: data.velocityGraph2, rotationSpeedGraph: data.rotationSpeedGraph, rotationSpeedGraph2: data.rotationSpeedGraph2, radialSpeedGraph: data.radialSpeedGraph, radialSpeedGraph2: data.radialSpeedGraph2, colorMap: data.colorMap, normalMap: data.normalMap, loop: data.loop, preWarm: data.preWarm, sort: data.sort, stretch: data.stretch, alignToMotion: data.alignToMotion, lighting: data.lighting, halfLambert: data.halfLambert, intensity: data.intensity, depthSoftening: data.depthSoftening, scene: this.system.app.scene, mesh: mesh, depthWrite: data.depthWrite, noFog: data.noFog, node: this.entity, blendType: data.blendType }); this.emitter.meshInstance.node = this.entity; this.emitter.drawOrder = this.drawOrder; if (!data.autoPlay) { this.pause(); this.emitter.meshInstance.visible = false; } } if (this.emitter.colorMap) { this.addMeshInstanceToLayers(); } this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this.enabled && this.entity.enabled && data.depthSoftening) { this._requestDepth(); } } onDisable() { const scene = this.system.app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } if (this.emitter) { this.removeMeshInstanceFromLayers(); if (this.data.depthSoftening) this._releaseDepth(); this.emitter.camera = null; } } onBeforeRemove() { if (this.enabled) { this.enabled = false; } if (this.emitter) { this.emitter.destroy(); this.emitter = null; } for(let i = 0; i < ASSET_PROPERTIES.length; i++){ const prop = ASSET_PROPERTIES[i]; if (this.data[prop]) { this[prop] = null; } } this.off(); } reset() { if (this.emitter) { this.emitter.reset(); } } stop() { if (this.emitter) { this.emitter.loop = false; this.emitter.resetTime(); this.emitter.addTime(0, true); } } pause() { this.data.paused = true; } unpause() { this.data.paused = false; } play() { this.data.paused = false; if (this.emitter) { this.emitter.meshInstance.visible = true; this.emitter.loop = this.data.loop; this.emitter.resetTime(); } } isPlaying() { if (this.data.paused) { return false; } if (this.emitter && this.emitter.loop) { return true; } return Date.now() <= this.emitter.endTime; } setInTools() { const { emitter } = this; if (emitter && !emitter.inTools) { emitter.inTools = true; this.rebuild(); } } rebuild() { const enabled = this.enabled; this.enabled = false; if (this.emitter) { this.emitter.rebuild(); } this.enabled = enabled; } constructor(system, entity){ super(system, entity), this._requestedDepth = false, this._drawOrder = 0, this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null, this._evtSetMeshes = null; this.on('set_colorMapAsset', this.onSetColorMapAsset, this); this.on('set_normalMapAsset', this.onSetNormalMapAsset, this); this.on('set_meshAsset', this.onSetMeshAsset, this); this.on('set_mesh', this.onSetMesh, this); this.on('set_renderAsset', this.onSetRenderAsset, this); this.on('set_loop', this.onSetLoop, this); this.on('set_blendType', this.onSetBlendType, this); this.on('set_depthSoftening', this.onSetDepthSoftening, this); this.on('set_layers', this.onSetLayers, this); SIMPLE_PROPERTIES.forEach((prop)=>{ this.on(`set_${prop}`, this.onSetSimpleProperty, this); }); COMPLEX_PROPERTIES.forEach((prop)=>{ this.on(`set_${prop}`, this.onSetComplexProperty, this); }); GRAPH_PROPERTIES.forEach((prop)=>{ this.on(`set_${prop}`, this.onSetGraphProperty, this); }); } } class ParticleSystemComponentData { constructor(){ this.numParticles = 1; this.rate = 1; this.rate2 = null; this.startAngle = 0; this.startAngle2 = null; this.lifetime = 50; this.emitterExtents = new Vec3(); this.emitterExtentsInner = new Vec3(); this.emitterRadius = 0; this.emitterRadiusInner = 0; this.emitterShape = EMITTERSHAPE_BOX; this.initialVelocity = 0; this.wrap = false; this.wrapBounds = new Vec3(); this.localSpace = false; this.screenSpace = false; this.colorMap = null; this.colorMapAsset = null; this.normalMap = null; this.normalMapAsset = null; this.loop = true; this.preWarm = false; this.sort = 0; this.mode = PARTICLEMODE_GPU; this.scene = null; this.lighting = false; this.halfLambert = false; this.intensity = 1; this.stretch = 0.0; this.alignToMotion = false; this.depthSoftening = 0; this.renderAsset = null; this.meshAsset = null; this.mesh = null; this.depthWrite = false; this.noFog = false; this.orientation = PARTICLEORIENTATION_SCREEN; this.particleNormal = new Vec3(0, 1, 0); this.animTilesX = 1; this.animTilesY = 1; this.animStartFrame = 0; this.animNumFrames = 1; this.animNumAnimations = 1; this.animIndex = 0; this.randomizeAnimIndex = false; this.animSpeed = 1; this.animLoop = true; this.scaleGraph = null; this.scaleGraph2 = null; this.colorGraph = null; this.colorGraph2 = null; this.alphaGraph = null; this.alphaGraph2 = null; this.localVelocityGraph = null; this.localVelocityGraph2 = null; this.velocityGraph = null; this.velocityGraph2 = null; this.rotationSpeedGraph = null; this.rotationSpeedGraph2 = null; this.radialSpeedGraph = null; this.radialSpeedGraph2 = null; this.blendType = BLEND_NORMAL; this.enabled = true; this.paused = false; this.autoPlay = true; this.layers = [ LAYERID_WORLD ]; } } var particlePS$1 = ` varying vec4 texCoordsAlphaLife; uniform sampler2D colorMap; uniform sampler2D colorParam; uniform float graphSampleSize; uniform float graphNumSamples; #ifndef CAMERAPLANES #define CAMERAPLANES uniform vec4 camera_params; #endif #ifdef SOFT uniform float softening; #endif uniform float colorMult; float saturate(float x) { return clamp(x, 0.0, 1.0); } void main(void) { vec4 tex = texture2D(colorMap, vec2(texCoordsAlphaLife.x, 1.0 - texCoordsAlphaLife.y)); vec4 ramp = texture2D(colorParam, vec2(texCoordsAlphaLife.w, 0.0)); ramp.rgb *= colorMult; ramp.a += texCoordsAlphaLife.z; vec3 rgb = tex.rgb * ramp.rgb; float a = tex.a * ramp.a; `; var particleVS$1 = ` vec3 unpack3NFloats(float src) { float r = fract(src); float g = fract(src * 256.0); float b = fract(src * 65536.0); return vec3(r, g, b); } float saturate(float x) { return clamp(x, 0.0, 1.0); } vec4 tex1Dlod_lerp(TEXTURE_ACCEPT_HIGHP(tex), vec2 tc) { return mix( texture2D(tex,tc), texture2D(tex,tc + graphSampleSize), fract(tc.x*graphNumSamples) ); } vec4 tex1Dlod_lerp(TEXTURE_ACCEPT_HIGHP(tex), vec2 tc, out vec3 w) { vec4 a = texture2D(tex,tc); vec4 b = texture2D(tex,tc + graphSampleSize); float c = fract(tc.x*graphNumSamples); vec3 unpackedA = unpack3NFloats(a.w); vec3 unpackedB = unpack3NFloats(b.w); w = mix(unpackedA, unpackedB, c); return mix(a, b, c); } vec2 rotate(vec2 quadXY, float pRotation, out mat2 rotMatrix) { float c = cos(pRotation); float s = sin(pRotation); mat2 m = mat2(c, -s, s, c); rotMatrix = m; return m * quadXY; } vec3 billboard(vec3 InstanceCoords, vec2 quadXY) { #ifdef SCREEN_SPACE vec3 pos = vec3(-1, 0, 0) * quadXY.x + vec3(0, -1, 0) * quadXY.y; #else vec3 pos = -matrix_viewInverse[0].xyz * quadXY.x + -matrix_viewInverse[1].xyz * quadXY.y; #endif return pos; } vec3 customFace(vec3 InstanceCoords, vec2 quadXY) { vec3 pos = faceTangent * quadXY.x + faceBinorm * quadXY.y; return pos; } vec2 safeNormalize(vec2 v) { float l = length(v); return (l > 1e-06) ? v / l : v; } void main(void) { vec3 meshLocalPos = particle_vertexData.xyz; float id = floor(particle_vertexData.w); float rndFactor = fract(sin(id + 1.0 + seed)); vec3 rndFactor3 = vec3(rndFactor, fract(rndFactor*10.0), fract(rndFactor*100.0)); float uv = id / numParticlesPot; readInput(uv); #ifdef LOCAL_SPACE inVel = mat3(matrix_model) * inVel; #endif vec2 velocityV = safeNormalize((mat3(matrix_view) * inVel).xy); float particleLifetime = lifetime; if (inLife <= 0.0 || inLife > particleLifetime || !inShow) meshLocalPos = vec3(0.0); vec2 quadXY = meshLocalPos.xy; float nlife = clamp(inLife / particleLifetime, 0.0, 1.0); vec3 paramDiv; vec4 params = tex1Dlod_lerp(TEXTURE_PASS(internalTex2), vec2(nlife, 0), paramDiv); float scale = params.y; float scaleDiv = paramDiv.x; float alphaDiv = paramDiv.z; scale += (scaleDiv * 2.0 - 1.0) * scaleDivMult * fract(rndFactor*10000.0); #ifndef USE_MESH texCoordsAlphaLife = vec4(quadXY * -0.5 + 0.5, (alphaDiv * 2.0 - 1.0) * alphaDivMult * fract(rndFactor*1000.0), nlife); #else texCoordsAlphaLife = vec4(particle_uv, (alphaDiv * 2.0 - 1.0) * alphaDivMult * fract(rndFactor*1000.0), nlife); #endif vec3 particlePos = inPos; vec3 particlePosMoved = vec3(0.0); mat2 rotMatrix; `; var particleAnimFrameClampVS$1 = ` float animFrame = min(floor(texCoordsAlphaLife.w * animTexParams.y) + animTexParams.x, animTexParams.z); `; var particleAnimFrameLoopVS$1 = ` float animFrame = floor(mod(texCoordsAlphaLife.w * animTexParams.y + animTexParams.x, animTexParams.z + 1.0)); `; var particleAnimTexVS$1 = ` float animationIndex; if (animTexIndexParams.y == 1.0) { animationIndex = floor((animTexParams.w + 1.0) * rndFactor3.z) * (animTexParams.z + 1.0); } else { animationIndex = animTexIndexParams.x * (animTexParams.z + 1.0); } float atlasX = (animationIndex + animFrame) * animTexTilesParams.x; float atlasY = 1.0 - floor(atlasX + 1.0) * animTexTilesParams.y; atlasX = fract(atlasX); texCoordsAlphaLife.xy *= animTexTilesParams.xy; texCoordsAlphaLife.xy += vec2(atlasX, atlasY); `; var particleInputFloatPS$1 = ` void readInput(float uv) { vec4 tex = texture2D(particleTexIN, vec2(uv, 0.25)); vec4 tex2 = texture2D(particleTexIN, vec2(uv, 0.75)); inPos = tex.xyz; inVel = tex2.xyz; inAngle = (tex.w < 0.0? -tex.w : tex.w) - 1000.0; inShow = tex.w >= 0.0; inLife = tex2.w; } `; var particleInputRgba8PS$1 = ` #define PI2 6.283185307179586 uniform vec3 inBoundsSize; uniform vec3 inBoundsCenter; uniform float maxVel; float decodeFloatRG(vec2 rg) { return rg.y * (1.0 / 255.0) + rg.x; } float decodeFloatRGBA( vec4 rgba ) { return dot(rgba, vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 160581375.0)); } void readInput(float uv) { vec4 tex0 = texture2D(particleTexIN, vec2(uv, 0.125)); vec4 tex1 = texture2D(particleTexIN, vec2(uv, 0.375)); vec4 tex2 = texture2D(particleTexIN, vec2(uv, 0.625)); vec4 tex3 = texture2D(particleTexIN, vec2(uv, 0.875)); inPos = vec3(decodeFloatRG(tex0.rg), decodeFloatRG(tex0.ba), decodeFloatRG(tex1.rg)); inPos = (inPos - vec3(0.5)) * inBoundsSize + inBoundsCenter; inVel = tex2.xyz; inVel = (inVel - vec3(0.5)) * maxVel; inAngle = decodeFloatRG(tex1.ba) * PI2; inShow = tex2.a > 0.5; inLife = decodeFloatRGBA(tex3); float maxNegLife = max(lifetime, numParticles * (rate+rateDiv)); float maxPosLife = lifetime+1.0; inLife = inLife * (maxNegLife + maxPosLife) - maxNegLife; } `; var particleOutputFloatPS$1 = ` void writeOutput() { if (gl_FragCoord.y<1.0) { gl_FragColor = vec4(outPos, (outAngle + 1000.0) * visMode); } else { gl_FragColor = vec4(outVel, outLife); } } `; var particleOutputRgba8PS$1 = ` uniform vec3 outBoundsMul; uniform vec3 outBoundsAdd; vec2 encodeFloatRG( float v ) { vec2 enc = vec2(1.0, 255.0) * v; enc = fract(enc); enc -= enc.yy * vec2(1.0/255.0, 1.0/255.0); return enc; } vec4 encodeFloatRGBA( float v ) { vec4 enc = vec4(1.0, 255.0, 65025.0, 160581375.0) * v; enc = fract(enc); enc -= enc.yzww * vec4(1.0/255.0,1.0/255.0,1.0/255.0,0.0); return enc; } void writeOutput() { outPos = outPos * outBoundsMul + outBoundsAdd; outAngle = fract(outAngle / PI2); outVel = (outVel / maxVel) + vec3(0.5); float maxNegLife = max(lifetime, numParticles * (rate+rateDiv)); float maxPosLife = lifetime+1.0; outLife = (outLife + maxNegLife) / (maxNegLife + maxPosLife); if (gl_FragCoord.y < 1.0) { gl_FragColor = vec4(encodeFloatRG(outPos.x), encodeFloatRG(outPos.y)); } else if (gl_FragCoord.y < 2.0) { gl_FragColor = vec4(encodeFloatRG(outPos.z), encodeFloatRG(outAngle)); } else if (gl_FragCoord.y < 3.0) { gl_FragColor = vec4(outVel, visMode*0.5+0.5); } else { gl_FragColor = encodeFloatRGBA(outLife); } } `; var particleUpdaterAABBPS$1 = ` uniform mat3 spawnBounds; uniform vec3 spawnPosInnerRatio; vec3 calcSpawnPosition(vec3 inBounds, float rndFactor) { vec3 pos = inBounds - vec3(0.5); vec3 posAbs = abs(pos); vec3 maxPos = vec3(max(posAbs.x, max(posAbs.y, posAbs.z))); vec3 edge = maxPos + (vec3(0.5) - maxPos) * spawnPosInnerRatio; pos.x = edge.x * (maxPos.x == posAbs.x ? sign(pos.x) : 2.0 * pos.x); pos.y = edge.y * (maxPos.y == posAbs.y ? sign(pos.y) : 2.0 * pos.y); pos.z = edge.z * (maxPos.z == posAbs.z ? sign(pos.z) : 2.0 * pos.z); #ifndef LOCAL_SPACE return emitterPos + spawnBounds * pos; #else return spawnBounds * pos; #endif } void addInitialVelocity(inout vec3 localVelocity, vec3 inBounds) { localVelocity -= vec3(0, 0, initialVelocity); } `; var particleUpdaterEndPS$1 = ` writeOutput(); } `; var particleUpdaterInitPS$1 = ` varying vec2 vUv0; uniform highp sampler2D particleTexIN; uniform highp sampler2D internalTex0; uniform highp sampler2D internalTex1; uniform highp sampler2D internalTex2; uniform highp sampler2D internalTex3; uniform mat3 emitterMatrix; uniform mat3 emitterMatrixInv; uniform vec3 emitterScale; uniform vec3 emitterPos; uniform vec3 frameRandom; uniform vec3 localVelocityDivMult; uniform vec3 velocityDivMult; uniform float delta; uniform float rate; uniform float rateDiv; uniform float lifetime; uniform float numParticles; uniform float rotSpeedDivMult; uniform float radialSpeedDivMult; uniform float seed; uniform float startAngle; uniform float startAngle2; uniform float initialVelocity; uniform float graphSampleSize; uniform float graphNumSamples; vec3 inPos; vec3 inVel; float inAngle; bool inShow; float inLife; float visMode; vec3 outPos; vec3 outVel; float outAngle; bool outShow; float outLife; `; var particleUpdaterNoRespawnPS$1 = ` if (outLife >= lifetime) { outLife -= max(lifetime, numParticles * particleRate); visMode = -1.0; } `; var particleUpdaterOnStopPS$1 = ` visMode = outLife < 0.0? -1.0: visMode; `; var particleUpdaterRespawnPS$1 = ` if (outLife >= lifetime) { outLife -= max(lifetime, numParticles * particleRate); visMode = 1.0; } visMode = outLife < 0.0? 1.0: visMode; `; var particleUpdaterSpherePS$1 = ` uniform float spawnBoundsSphere; uniform float spawnBoundsSphereInnerRatio; vec3 calcSpawnPosition(vec3 inBounds, float rndFactor) { float rnd4 = fract(rndFactor * 1000.0); vec3 norm = normalize(inBounds.xyz - vec3(0.5)); float r = rnd4 * (1.0 - spawnBoundsSphereInnerRatio) + spawnBoundsSphereInnerRatio; #ifndef LOCAL_SPACE return emitterPos + norm * r * spawnBoundsSphere; #else return norm * r * spawnBoundsSphere; #endif } void addInitialVelocity(inout vec3 localVelocity, vec3 inBounds) { localVelocity += normalize(inBounds - vec3(0.5)) * initialVelocity; } `; var particleUpdaterStartPS$1 = ` float saturate(float x) { return clamp(x, 0.0, 1.0); } vec3 unpack3NFloats(float src) { float r = fract(src); float g = fract(src * 256.0); float b = fract(src * 65536.0); return vec3(r, g, b); } vec3 tex1Dlod_lerp(TEXTURE_ACCEPT_HIGHP(tex), vec2 tc, out vec3 w) { vec4 a = texture2D(tex, tc); vec4 b = texture2D(tex, tc + graphSampleSize); float c = fract(tc.x * graphNumSamples); vec3 unpackedA = unpack3NFloats(a.w); vec3 unpackedB = unpack3NFloats(b.w); w = mix(unpackedA, unpackedB, c); return mix(a.xyz, b.xyz, c); } #define HASHSCALE4 vec4(1031, .1030, .0973, .1099) vec4 hash41(float p) { vec4 p4 = fract(vec4(p) * HASHSCALE4); p4 += dot(p4, p4.wzxy+19.19); return fract(vec4((p4.x + p4.y)*p4.z, (p4.x + p4.z)*p4.y, (p4.y + p4.z)*p4.w, (p4.z + p4.w)*p4.x)); } void main(void) { if (gl_FragCoord.x > numParticles) discard; readInput(vUv0.x); visMode = inShow? 1.0 : -1.0; vec4 rndFactor = hash41(gl_FragCoord.x + seed); float particleRate = rate + rateDiv * rndFactor.x; outLife = inLife + delta; float nlife = clamp(outLife / lifetime, 0.0, 1.0); vec3 localVelocityDiv; vec3 velocityDiv; vec3 paramDiv; vec3 localVelocity = tex1Dlod_lerp(TEXTURE_PASS(internalTex0), vec2(nlife, 0), localVelocityDiv); vec3 velocity = tex1Dlod_lerp(TEXTURE_PASS(internalTex1), vec2(nlife, 0), velocityDiv); vec3 params = tex1Dlod_lerp(TEXTURE_PASS(internalTex2), vec2(nlife, 0), paramDiv); float rotSpeed = params.x; float rotSpeedDiv = paramDiv.y; vec3 radialParams = tex1Dlod_lerp(TEXTURE_PASS(internalTex3), vec2(nlife, 0), paramDiv); float radialSpeed = radialParams.x; float radialSpeedDiv = radialParams.y; bool respawn = inLife <= 0.0 || outLife >= lifetime; inPos = respawn ? calcSpawnPosition(rndFactor.xyz, rndFactor.x) : inPos; inAngle = respawn ? mix(startAngle, startAngle2, rndFactor.x) : inAngle; #ifndef LOCAL_SPACE vec3 radialVel = inPos - emitterPos; #else vec3 radialVel = inPos; #endif radialVel = (dot(radialVel, radialVel) > 1.0E-8) ? radialSpeed * normalize(radialVel) : vec3(0.0); radialVel += (radialSpeedDiv * vec3(2.0) - vec3(1.0)) * radialSpeedDivMult * rndFactor.xyz; localVelocity += (localVelocityDiv * vec3(2.0) - vec3(1.0)) * localVelocityDivMult * rndFactor.xyz; velocity += (velocityDiv * vec3(2.0) - vec3(1.0)) * velocityDivMult * rndFactor.xyz; rotSpeed += (rotSpeedDiv * 2.0 - 1.0) * rotSpeedDivMult * rndFactor.y; addInitialVelocity(localVelocity, rndFactor.xyz); #ifndef LOCAL_SPACE outVel = emitterMatrix * localVelocity + (radialVel + velocity) * emitterScale; #else outVel = (localVelocity + radialVel) / emitterScale + emitterMatrixInv * velocity; #endif outPos = inPos + outVel * delta; outAngle = inAngle + rotSpeed * delta; `; var particle_billboardVS$1 = ` quadXY = rotate(quadXY, inAngle, rotMatrix); vec3 localPos = billboard(particlePos, quadXY); `; var particle_blendAddPS$1 = ` dBlendModeFogFactor = 0.0; rgb *= saturate(gammaCorrectInput(max(a, 0.0))); if ((rgb.r + rgb.g + rgb.b) < 0.000001) discard; `; var particle_blendMultiplyPS$1 = ` rgb = mix(vec3(1.0), rgb, vec3(a)); if (rgb.r + rgb.g + rgb.b > 2.99) discard; `; var particle_blendNormalPS$1 = ` if (a < 0.01) discard; `; var particle_cpuVS$1 = ` attribute vec4 particle_vertexData; attribute vec4 particle_vertexData2; attribute vec4 particle_vertexData3; attribute float particle_vertexData4; #ifndef USE_MESH attribute vec2 particle_vertexData5; #else attribute vec4 particle_vertexData5; #endif uniform mat4 matrix_viewProjection; uniform mat4 matrix_model; #ifndef VIEWMATRIX #define VIEWMATRIX uniform mat4 matrix_view; #endif uniform mat3 matrix_normal; uniform mat4 matrix_viewInverse; uniform float numParticles; uniform float lifetime; uniform float stretch; uniform float seed; uniform vec3 emitterScale; uniform vec3 faceTangent; uniform vec3 faceBinorm; #ifdef PARTICLE_GPU #ifdef WRAP uniform vec3 wrapBounds; #endif #endif #ifdef PARTICLE_GPU uniform highp sampler2D internalTex0; uniform highp sampler2D internalTex1; uniform highp sampler2D internalTex2; #endif uniform vec3 emitterPos; varying vec4 texCoordsAlphaLife; vec2 rotate(vec2 quadXY, float pRotation, out mat2 rotMatrix) { float c = cos(pRotation); float s = sin(pRotation); mat2 m = mat2(c, -s, s, c); rotMatrix = m; return m * quadXY; } vec3 billboard(vec3 InstanceCoords, vec2 quadXY) { #ifdef SCREEN_SPACE vec3 pos = vec3(-1, 0, 0) * quadXY.x + vec3(0, -1, 0) * quadXY.y; #else vec3 pos = -matrix_viewInverse[0].xyz * quadXY.x + -matrix_viewInverse[1].xyz * quadXY.y; #endif return pos; } vec3 customFace(vec3 InstanceCoords, vec2 quadXY) { vec3 pos = faceTangent * quadXY.x + faceBinorm * quadXY.y; return pos; } void main(void) { vec3 particlePos = particle_vertexData.xyz; vec3 inPos = particlePos; vec3 vertPos = particle_vertexData3.xyz; vec3 inVel = vec3(particle_vertexData2.w, particle_vertexData3.w, particle_vertexData5.x); float id = floor(particle_vertexData4); float rndFactor = fract(sin(id + 1.0 + seed)); vec3 rndFactor3 = vec3(rndFactor, fract(rndFactor*10.0), fract(rndFactor*100.0)); #ifdef LOCAL_SPACE inVel = mat3(matrix_model) * inVel; #endif vec2 velocityV = normalize((mat3(matrix_view) * inVel).xy); vec2 quadXY = vertPos.xy; #ifdef USE_MESH texCoordsAlphaLife = vec4(particle_vertexData5.zw, particle_vertexData2.z, particle_vertexData.w); #else texCoordsAlphaLife = vec4(quadXY * -0.5 + 0.5, particle_vertexData2.z, particle_vertexData.w); #endif mat2 rotMatrix; float inAngle = particle_vertexData2.x; vec3 particlePosMoved = vec3(0.0); vec3 meshLocalPos = particle_vertexData3.xyz; `; var particle_cpu_endVS$1 = ` localPos *= particle_vertexData2.y * emitterScale; localPos += particlePos; #ifdef SCREEN_SPACE gl_Position = vec4(localPos.x, localPos.y, 0.0, 1.0); #else gl_Position = matrix_viewProjection * vec4(localPos, 1.0); #endif `; var particle_customFaceVS$1 = ` quadXY = rotate(quadXY, inAngle, rotMatrix); vec3 localPos = customFace(particlePos, quadXY); `; var particle_endPS$1 = ` rgb = addFog(rgb); rgb = toneMap(rgb); rgb = gammaCorrectOutput(rgb); gl_FragColor = vec4(rgb, a); } `; var particle_endVS$1 = ` localPos *= scale * emitterScale; localPos += particlePos; #ifdef SCREEN_SPACE gl_Position = vec4(localPos.x, localPos.y, 0.0, 1.0); #else gl_Position = matrix_viewProjection * vec4(localPos.xyz, 1.0); #endif `; var particle_halflambertPS$1 = ` vec3 negNormal = normal * 0.5 + 0.5; vec3 posNormal = -normal * 0.5 + 0.5; negNormal *= negNormal; posNormal *= posNormal; `; var particle_initVS$1 = ` attribute vec4 particle_vertexData; #if defined(USE_MESH) #if defined(USE_MESH_UV) attribute vec2 particle_uv; #else vec2 particle_uv = vec2(0.0, 0.0); #endif #endif uniform mat4 matrix_viewProjection; uniform mat4 matrix_model; uniform mat3 matrix_normal; uniform mat4 matrix_viewInverse; #ifndef VIEWMATRIX #define VIEWMATRIX uniform mat4 matrix_view; #endif uniform float numParticles; uniform float numParticlesPot; uniform float graphSampleSize; uniform float graphNumSamples; uniform float stretch; uniform vec3 emitterScale; uniform vec3 emitterPos; uniform vec3 faceTangent; uniform vec3 faceBinorm; uniform float rate; uniform float rateDiv; uniform float lifetime; uniform float scaleDivMult; uniform float alphaDivMult; uniform float seed; uniform float delta; uniform sampler2D particleTexOUT; uniform sampler2D particleTexIN; #ifdef PARTICLE_GPU #ifdef WRAP uniform vec3 wrapBounds; #endif #endif #ifdef PARTICLE_GPU uniform highp sampler2D internalTex0; uniform highp sampler2D internalTex1; uniform highp sampler2D internalTex2; #endif #ifndef CAMERAPLANES #define CAMERAPLANES uniform vec4 camera_params; #endif varying vec4 texCoordsAlphaLife; vec3 inPos; vec3 inVel; float inAngle; bool inShow; float inLife; `; var particle_lambertPS$1 = ` vec3 negNormal = max(normal, vec3(0.0)); vec3 posNormal = max(-normal, vec3(0.0)); `; var particle_lightingPS$1 = ` vec3 light = negNormal.x*lightCube[0] + posNormal.x*lightCube[1] + negNormal.y*lightCube[2] + posNormal.y*lightCube[3] + negNormal.z*lightCube[4] + posNormal.z*lightCube[5]; rgb *= light; `; var particle_localShiftVS$1 = ` particlePos = (matrix_model * vec4(particlePos, 1.0)).xyz; `; var particle_meshVS$1 = ` vec3 localPos = meshLocalPos; localPos.xy = rotate(localPos.xy, inAngle, rotMatrix); localPos.yz = rotate(localPos.yz, inAngle, rotMatrix); billboard(particlePos, quadXY); `; var particle_normalVS$1 = ` Normal = normalize(localPos + matrix_viewInverse[2].xyz); `; var particle_normalMapPS$1 = ` vec3 normalMap = normalize(texture2D(normalMap, vec2(texCoordsAlphaLife.x, 1.0 - texCoordsAlphaLife.y)).xyz * 2.0 - 1.0); vec3 normal = ParticleMat * normalMap; `; var particle_pointAlongVS$1 = ` inAngle = atan(velocityV.x, velocityV.y); `; var particle_simulationPS$1 = ` #include "particleUpdaterInitPS" #ifdef PACK8 #include "particleInputRgba8PS" #include "particleOutputRgba8PS" #else #include "particleInputFloatPS" #include "particleOutputFloatPS" #endif #ifdef EMITTERSHAPE_BOX #include "particleUpdaterAABBPS" #else #include "particleUpdaterSpherePS" #endif #include "particleUpdaterStartPS" #ifdef RESPAWN #include "particleUpdaterRespawnPS" #endif #ifdef NO_RESPAWN #include "particleUpdaterNoRespawnPS" #endif #ifdef ON_STOP #include "particleUpdaterOnStopPS" #endif #include "particleUpdaterEndPS" `; var particle_shaderPS$1 = ` #if NORMAL != NONE #if NORMAL == VERTEX varying vec3 Normal; #endif #if NORMAL == MAP varying mat3 ParticleMat; #endif uniform vec3 lightCube[6]; #endif #ifdef SOFT varying float vDepth; #include "screenDepthPS" #endif #include "gammaPS" #include "tonemappingPS" #include "fogPS" #if NORMAL == MAP uniform sampler2D normalMap; #endif #include "particlePS" #ifdef SOFT #include "particle_softPS" #endif #if NORMAL == VERTEX vec3 normal = Normal; #endif #if NORMAL == MAP #include "particle_normalMapPS" #endif #if NORMAL != NONE #ifdef HALF_LAMBERT #include "particle_halflambertPS" #else #include "particle_lambertPS" #endif #include "particle_lightingPS" #endif #if BLEND == NORMAL #include "particle_blendNormalPS" #elif BLEND == ADDITIVE #include "particle_blendAddPS" #elif BLEND == MULTIPLICATIVE #include "particle_blendMultiplyPS" #endif #include "particle_endPS" `; var particle_shaderVS$1 = ` #ifdef ANIMTEX uniform vec2 animTexTilesParams; uniform vec4 animTexParams; uniform vec2 animTexIndexParams; #endif #if NORMAL == MAP varying mat3 ParticleMat; #endif #if NORMAL == VERTEX varying vec3 Normal; #endif #ifdef SOFT varying float vDepth; #endif #ifdef PARTICLE_GPU #include "particle_initVS" #ifdef PACK8 #include "particleInputRgba8PS" #else #include "particleInputFloatPS" #endif #ifdef SOFT #include "screenDepthPS" #endif #include "particleVS" #else #ifdef SOFT #include "screenDepthPS" #endif #include "particle_cpuVS" #endif #ifdef LOCAL_SPACE #include "particle_localShiftVS" #endif #ifdef ANIMTEX #ifdef ANIMTEX_LOOP #include "particleAnimFrameLoopVS" #else #include "particleAnimFrameClampVS" #endif #include "particleAnimTexVS" #endif #ifdef PARTICLE_GPU #ifdef WRAP #include "particle_wrapVS" #endif #endif #ifdef ALIGN_TO_MOTION #include "particle_pointAlongVS" #endif #ifdef USE_MESH #include "particle_meshVS" #else #ifdef CUSTOM_FACE #include "particle_customFaceVS" #else #include "particle_billboardVS" #endif #endif #if NORMAL == VERTEX #include "particle_normalVS" #endif #if NORMAL == MAP #include "particle_TBNVS" #endif #ifdef STRETCH #include "particle_stretchVS" #endif #ifdef PARTICLE_GPU #include "particle_endVS" #else #include "particle_cpu_endVS" #endif #ifdef SOFT #include "particle_softVS" #endif } `; var particle_softPS$1 = ` float depth = getLinearScreenDepth(); float particleDepth = vDepth; float depthDiff = saturate(abs(particleDepth - depth) * softening); a *= depthDiff; `; var particle_softVS$1 = ` vDepth = getLinearDepth(localPos); `; var particle_stretchVS$1 = ` vec3 moveDir = inVel * stretch; vec3 posPrev = particlePos - moveDir; posPrev += particlePosMoved; vec2 centerToVertexV = normalize((mat3(matrix_view) * localPos).xy); float interpolation = dot(-velocityV, centerToVertexV) * 0.5 + 0.5; particlePos = mix(particlePos, posPrev, interpolation); `; var particle_TBNVS$1 = ` mat3 rot3 = mat3(rotMatrix[0][0], rotMatrix[0][1], 0.0, rotMatrix[1][0], rotMatrix[1][1], 0.0, 0.0, 0.0, 1.0); ParticleMat = mat3(-matrix_viewInverse[0].xyz, -matrix_viewInverse[1].xyz, matrix_viewInverse[2].xyz) * rot3; `; var particle_wrapVS$1 = ` vec3 origParticlePos = particlePos; particlePos -= matrix_model[3].xyz; particlePos = mod(particlePos, wrapBounds) - wrapBounds * 0.5; particlePos += matrix_model[3].xyz; particlePosMoved = particlePos - origParticlePos; `; const particleChunksGLSL = { particlePS: particlePS$1, particleVS: particleVS$1, particleAnimFrameClampVS: particleAnimFrameClampVS$1, particleAnimFrameLoopVS: particleAnimFrameLoopVS$1, particleAnimTexVS: particleAnimTexVS$1, particleInputFloatPS: particleInputFloatPS$1, particleInputRgba8PS: particleInputRgba8PS$1, particleOutputFloatPS: particleOutputFloatPS$1, particleOutputRgba8PS: particleOutputRgba8PS$1, particleUpdaterAABBPS: particleUpdaterAABBPS$1, particleUpdaterEndPS: particleUpdaterEndPS$1, particleUpdaterInitPS: particleUpdaterInitPS$1, particleUpdaterNoRespawnPS: particleUpdaterNoRespawnPS$1, particleUpdaterOnStopPS: particleUpdaterOnStopPS$1, particleUpdaterRespawnPS: particleUpdaterRespawnPS$1, particleUpdaterSpherePS: particleUpdaterSpherePS$1, particleUpdaterStartPS: particleUpdaterStartPS$1, particle_billboardVS: particle_billboardVS$1, particle_blendAddPS: particle_blendAddPS$1, particle_blendMultiplyPS: particle_blendMultiplyPS$1, particle_blendNormalPS: particle_blendNormalPS$1, particle_cpuVS: particle_cpuVS$1, particle_cpu_endVS: particle_cpu_endVS$1, particle_customFaceVS: particle_customFaceVS$1, particle_endPS: particle_endPS$1, particle_endVS: particle_endVS$1, particle_halflambertPS: particle_halflambertPS$1, particle_initVS: particle_initVS$1, particle_lambertPS: particle_lambertPS$1, particle_lightingPS: particle_lightingPS$1, particle_localShiftVS: particle_localShiftVS$1, particle_meshVS: particle_meshVS$1, particle_normalVS: particle_normalVS$1, particle_normalMapPS: particle_normalMapPS$1, particle_pointAlongVS: particle_pointAlongVS$1, particle_simulationPS: particle_simulationPS$1, particle_shaderPS: particle_shaderPS$1, particle_shaderVS: particle_shaderVS$1, particle_softPS: particle_softPS$1, particle_softVS: particle_softVS$1, particle_stretchVS: particle_stretchVS$1, particle_TBNVS: particle_TBNVS$1, particle_wrapVS: particle_wrapVS$1 }; var particlePS = ` varying texCoordsAlphaLife: vec4f; var colorMap: texture_2d; var colorMapSampler: sampler; var colorParam: texture_2d; var colorParamSampler: sampler; uniform graphSampleSize: f32; uniform graphNumSamples: f32; #ifndef CAMERAPLANES #define CAMERAPLANES uniform camera_params: vec4f; #endif #ifdef SOFT uniform softening: f32; #endif uniform colorMult: f32; fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let tex: vec4f = textureSample(colorMap, colorMapSampler, vec2f(input.texCoordsAlphaLife.x, 1.0 - input.texCoordsAlphaLife.y)); var ramp: vec4f = textureSample(colorParam, colorParamSampler, vec2f(input.texCoordsAlphaLife.w, 0.0)); ramp = vec4f(ramp.rgb * uniform.colorMult, ramp.a); ramp.a = ramp.a + input.texCoordsAlphaLife.z; var rgb: vec3f = tex.rgb * ramp.rgb; var a: f32 = tex.a * ramp.a; `; var particleVS = ` fn unpack3NFloats(src: f32) -> vec3f { let r = fract(src); let g = fract(src * 256.0); let b = fract(src * 65536.0); return vec3f(r, g, b); } fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } struct TexLerpUnpackResult { result: vec4f, unpacked: vec3f } fn tex1Dlod_lerp_simple(tex: texture_2d, textureSize: vec2u, tc: vec2f) -> vec4f { let tc_next = tc + vec2f(uniform.graphSampleSize); let texelA: vec2i = vec2i(tc * vec2f(textureSize)); let texelB: vec2i = vec2i(tc_next * vec2f(textureSize)); return mix( textureLoad(tex, texelA, 0), textureLoad(tex, texelB, 0), fract(tc.x * uniform.graphNumSamples) ); } fn tex1Dlod_lerp_unpack(tex: texture_2d, textureSize: vec2u, tc: vec2f) -> TexLerpUnpackResult { let tc_next = tc + vec2f(uniform.graphSampleSize); let texelA: vec2i = vec2i(tc * vec2f(textureSize)); let texelB: vec2i = vec2i(tc_next * vec2f(textureSize)); let a = textureLoad(tex, texelA, 0); let b = textureLoad(tex, texelB, 0); let c = fract(tc.x * uniform.graphNumSamples); let unpackedA = unpack3NFloats(a.w); let unpackedB = unpack3NFloats(b.w); let w_out = mix(unpackedA, unpackedB, c); return TexLerpUnpackResult(mix(a, b, c), w_out); } struct RotateResult { rotatedVec: vec2f, matrix: mat2x2f } fn rotateWithMatrix(quadXY: vec2f, pRotation: f32) -> RotateResult { let c = cos(pRotation); let s = sin(pRotation); let m = mat2x2f(vec2f(c, -s), vec2f(s, c)); return RotateResult(m * quadXY, m); } fn billboard(InstanceCoords: vec3f, quadXY: vec2f) -> vec3f { var pos: vec3f; #ifdef SCREEN_SPACE pos = vec3f(-1.0, 0.0, 0.0) * quadXY.x + vec3f(0.0, -1.0, 0.0) * quadXY.y; #else pos = -uniform.matrix_viewInverse[0].xyz * quadXY.x + -uniform.matrix_viewInverse[1].xyz * quadXY.y; #endif return pos; } fn customFace(InstanceCoords: vec3f, quadXY: vec2f) -> vec3f { let pos = uniform.faceTangent * quadXY.x + uniform.faceBinorm * quadXY.y; return pos; } fn safeNormalize(v: vec2f) -> vec2f { let l = length(v); return select(v, v / l, l > 1e-06); } @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; let meshLocalPos_in = input.particle_vertexData.xyz; let id = floor(input.particle_vertexData.w); let rndFactor = fract(sin(id + 1.0 + uniform.seed)); let rndFactor3 = vec3f(rndFactor, fract(rndFactor*10.0), fract(rndFactor*100.0)); let uv = id / uniform.numParticlesPot; readInput(uv); #ifdef LOCAL_SPACE let modelRotation = mat3x3f(uniform.matrix_model[0].xyz, uniform.matrix_model[1].xyz, uniform.matrix_model[2].xyz); inVel = modelRotation * inVel; #endif let viewRotation = mat3x3f(uniform.matrix_view[0].xyz, uniform.matrix_view[1].xyz, uniform.matrix_view[2].xyz); let velocityV = safeNormalize((viewRotation * inVel).xy); let particleLifetime = uniform.lifetime; var meshLocalPos = meshLocalPos_in; if (inLife <= 0.0 || inLife > particleLifetime || !inShow) { meshLocalPos = vec3f(0.0); } let quadXY = meshLocalPos.xy; let nlife = clamp(inLife / particleLifetime, 0.0, 1.0); let internalTexSize = textureDimensions(internalTex2, 0); let lerp_result = tex1Dlod_lerp_unpack(internalTex2, internalTexSize, vec2f(nlife, 0.0)); let params = lerp_result.result; let paramDiv = lerp_result.unpacked; var scale = params.y; let scaleDiv = paramDiv.x; let alphaDiv = paramDiv.z; scale = scale + (scaleDiv * 2.0 - 1.0) * uniform.scaleDivMult * fract(rndFactor*10000.0); #ifndef USE_MESH output.texCoordsAlphaLife = vec4f(quadXY * -0.5 + 0.5, (alphaDiv * 2.0 - 1.0) * uniform.alphaDivMult * fract(rndFactor*1000.0), nlife); #else output.texCoordsAlphaLife = vec4f(particle_uv, (alphaDiv * 2.0 - 1.0) * uniform.alphaDivMult * fract(rndFactor*1000.0), nlife); #endif var particlePos = inPos; var particlePosMoved = vec3f(0.0); var rotMatrix: mat2x2f; `; var particleAnimFrameClampVS = ` let animFrame: f32 = min(floor(input.texCoordsAlphaLife.w * uniform.animTexParams.y) + uniform.animTexParams.x, uniform.animTexParams.z); `; var particleAnimFrameLoopVS = ` let animFrame: f32 = floor((output.texCoordsAlphaLife.w * uniform.animTexParams.y + uniform.animTexParams.x) % (uniform.animTexParams.z + 1.0)); `; var particleAnimTexVS = ` var animationIndex: f32; if (uniform.animTexIndexParams.y == 1.0) { animationIndex = floor((uniform.animTexParams.w + 1.0) * rndFactor3.z) * (uniform.animTexParams.z + 1.0); } else { animationIndex = uniform.animTexIndexParams.x * (uniform.animTexParams.z + 1.0); } var atlasX: f32 = (animationIndex + animFrame) * uniform.animTexTilesParams.x; let atlasY: f32 = 1.0 - floor(atlasX + 1.0) * uniform.animTexTilesParams.y; atlasX = fract(atlasX); let current_tcal_xy = output.texCoordsAlphaLife.xy; let scaled_tcal_xy = current_tcal_xy * uniform.animTexTilesParams.xy; let final_tcal_xy = scaled_tcal_xy + vec2f(atlasX, atlasY); output.texCoordsAlphaLife = vec4f(final_tcal_xy, output.texCoordsAlphaLife.z, output.texCoordsAlphaLife.w); `; var particleInputFloatPS = ` fn readInput(uv: f32) { let textureSize = textureDimensions(particleTexIN, 0); let texel0: vec2i = vec2i(vec2f(uv, 0.25) * vec2f(textureSize)); let texel1: vec2i = vec2i(vec2f(uv, 0.75) * vec2f(textureSize)); let tex: vec4f = textureLoad(particleTexIN, texel0, 0); let tex2: vec4f = textureLoad(particleTexIN, texel1, 0); inPos = tex.xyz; inVel = tex2.xyz; inAngle = abs(tex.w) - 1000.0; inShow = tex.w >= 0.0; inLife = tex2.w; } `; var particleInputRgba8PS = ` const PI2: f32 = 6.283185307179586; uniform inBoundsSize: vec3f; uniform inBoundsCenter: vec3f; uniform maxVel: f32; fn decodeFloatRG(rg: vec2f) -> f32 { return rg.y * (1.0 / 255.0) + rg.x; } fn decodeFloatRGBA( rgba: vec4f ) -> f32 { return dot(rgba, vec4f(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 160581375.0)); } fn readInput(uv: f32) { let textureSize = textureDimensions(particleTexIN, 0); let texel0: vec2i = vec2i(vec2f(uv, 0.125) * vec2f(textureSize)); let texel1: vec2i = vec2i(vec2f(uv, 0.375) * vec2f(textureSize)); let texel2: vec2i = vec2i(vec2f(uv, 0.625) * vec2f(textureSize)); let texel3: vec2i = vec2i(vec2f(uv, 0.875) * vec2f(textureSize)); let tex0 = textureLoad(particleTexIN, texel0, 0); let tex1 = textureLoad(particleTexIN, texel1, 0); let tex2 = textureLoad(particleTexIN, texel2, 0); let tex3 = textureLoad(particleTexIN, texel3, 0); inPos = vec3f(decodeFloatRG(tex0.rg), decodeFloatRG(tex0.ba), decodeFloatRG(tex1.rg)); inPos = (inPos - vec3f(0.5)) * uniform.inBoundsSize + uniform.inBoundsCenter; inVel = tex2.xyz; inVel = (inVel - vec3f(0.5)) * uniform.maxVel; inAngle = decodeFloatRG(tex1.ba) * PI2; inShow = tex2.a > 0.5; let life_decoded = decodeFloatRGBA(tex3); let maxNegLife = max(uniform.lifetime, uniform.numParticles * (uniform.rate + uniform.rateDiv)); let maxPosLife = uniform.lifetime + 1.0; inLife = life_decoded * (maxNegLife + maxPosLife) - maxNegLife; }`; var particleOutputFloatPS = ` fn getOutput() -> vec4f { if (pcPosition.y < 1.0) { return vec4f(outPos, (outAngle + 1000.0) * visMode); } else { return vec4f(outVel, outLife); } } `; var particleOutputRgba8PS = ` uniform outBoundsMul: vec3f; uniform outBoundsAdd: vec3f; fn encodeFloatRG( v: f32 ) -> vec2f { var enc: vec2f = vec2f(1.0, 255.0) * v; enc = fract(enc); enc = enc - enc.yy * (1.0 / 255.0); return enc; } fn encodeFloatRGBA( v: f32 ) -> vec4f { let factors = vec4f(1.0, 255.0, 65025.0, 160581375.0); var enc: vec4f = factors * v; enc = fract(enc); enc = enc - enc.yzww * vec4f(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0); return enc; } fn getOutput() -> vec4f { outPos = outPos * uniform.outBoundsMul + uniform.outBoundsAdd; outAngle = fract(outAngle / PI2); outVel = (outVel / uniform.maxVel) + vec3f(0.5); let maxNegLife = max(uniform.lifetime, uniform.numParticles * (uniform.rate + uniform.rateDiv)); let maxPosLife = uniform.lifetime + 1.0; outLife = (outLife + maxNegLife) / (maxNegLife + maxPosLife); if (pcPosition.y < 1.0) { return vec4f(encodeFloatRG(outPos.x), encodeFloatRG(outPos.y)); } else if (pcPosition.y < 2.0) { return vec4f(encodeFloatRG(outPos.z), encodeFloatRG(outAngle)); } else if (pcPosition.y < 3.0) { return vec4f(outVel, visMode * 0.5 + 0.5); } else { return encodeFloatRGBA(outLife); } } `; var particleUpdaterAABBPS = ` uniform spawnBounds: mat3x3f; uniform spawnPosInnerRatio: vec3f; fn calcSpawnPosition(inBounds: vec3f, rndFactor: f32) -> vec3f { var pos = inBounds - vec3f(0.5); let posAbs = abs(pos); let maxComp = max(posAbs.x, max(posAbs.y, posAbs.z)); let maxPos = vec3f(maxComp); let edge = maxPos + (vec3f(0.5) - maxPos) * uniform.spawnPosInnerRatio; pos.x = edge.x * select(2.0 * pos.x, sign(pos.x), maxPos.x == posAbs.x); pos.y = edge.y * select(2.0 * pos.y, sign(pos.y), maxPos.y == posAbs.y); pos.z = edge.z * select(2.0 * pos.z, sign(pos.z), maxPos.z == posAbs.z); #ifndef LOCAL_SPACE return uniform.emitterPos + uniform.spawnBounds * pos; #else return uniform.spawnBounds * pos; #endif } fn addInitialVelocity(localVelocity: ptr, inBounds: vec3f) { *localVelocity = *localVelocity - vec3f(0.0, 0.0, uniform.initialVelocity); } `; var particleUpdaterEndPS = ` output.color = getOutput(); return output; } `; var particleUpdaterInitPS = ` varying vUv0: vec2f; var particleTexIN: texture_2d; var internalTex0: texture_2d; var internalTex1: texture_2d; var internalTex2: texture_2d; var internalTex3: texture_2d; uniform emitterMatrix: mat3x3f; uniform emitterMatrixInv: mat3x3f; uniform emitterScale: vec3f; uniform emitterPos: vec3f; uniform frameRandom: vec3f; uniform localVelocityDivMult: vec3f; uniform velocityDivMult: vec3f; uniform delta: f32; uniform rate: f32; uniform rateDiv: f32; uniform lifetime: f32; uniform numParticles: f32; uniform rotSpeedDivMult: f32; uniform radialSpeedDivMult: f32; uniform seed: f32; uniform startAngle: f32; uniform startAngle2: f32; uniform initialVelocity: f32; uniform graphSampleSize: f32; uniform graphNumSamples: f32; var inPos: vec3f; var inVel: vec3f; var inAngle: f32; var inShow: bool; var inLife: f32; var visMode: f32; var outPos: vec3f; var outVel: vec3f; var outAngle: f32; var outShow: bool; var outLife: f32; `; var particleUpdaterNoRespawnPS = ` if (outLife >= uniform.lifetime) { outLife = outLife - max(uniform.lifetime, uniform.numParticles * particleRate); visMode = -1.0; } `; var particleUpdaterOnStopPS = ` visMode = select(visMode, -1.0, outLife < 0.0); `; var particleUpdaterRespawnPS = ` if (outLife >= uniform.lifetime) { let subtractAmount = max(uniform.lifetime, uniform.numParticles * particleRate); outLife = outLife - subtractAmount; visMode = 1.0; } visMode = select(visMode, 1.0, outLife < 0.0); `; var particleUpdaterSpherePS = ` uniform spawnBoundsSphere: f32; uniform spawnBoundsSphereInnerRatio: f32; fn calcSpawnPosition(inBounds: vec3f, rndFactor: f32) -> vec3f { let rnd4: f32 = fract(rndFactor * 1000.0); let norm: vec3f = normalize(inBounds.xyz - vec3f(0.5)); let r: f32 = rnd4 * (1.0 - uniform.spawnBoundsSphereInnerRatio) + uniform.spawnBoundsSphereInnerRatio; #ifndef LOCAL_SPACE return uniform.emitterPos + norm * r * uniform.spawnBoundsSphere; #else return norm * r * uniform.spawnBoundsSphere; #endif } fn addInitialVelocity(localVelocity: ptr, inBounds: vec3f) { let initialVelOffset: vec3f = normalize(inBounds - vec3f(0.5)) * uniform.initialVelocity; *localVelocity = *localVelocity + initialVelOffset; } `; var particleUpdaterStartPS = ` fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } fn unpack3NFloats(src: f32) -> vec3f { let r = fract(src); let g = fract(src * 256.0); let b = fract(src * 65536.0); return vec3f(r, g, b); } struct TexLerpUnpackResult { result: vec3f, unpacked: vec3f } fn tex1Dlod_lerp(tex: texture_2d, textureSize: vec2u, tc: vec2f) -> TexLerpUnpackResult { let tc_next = tc + vec2f(uniform.graphSampleSize); let texelA: vec2i = vec2i(tc * vec2f(textureSize)); let texelB: vec2i = vec2i(tc_next * vec2f(textureSize)); let a = textureLoad(tex, texelA, 0); let b = textureLoad(tex, texelB, 0); let c = fract(tc.x * uniform.graphNumSamples); let unpackedA = unpack3NFloats(a.w); let unpackedB = unpack3NFloats(b.w); let w_out = mix(unpackedA, unpackedB, c); return TexLerpUnpackResult(mix(a.xyz, b.xyz, c), w_out); } const HASHSCALE4: vec4f = vec4f(1031.0, 0.1030, 0.0973, 0.1099); fn hash41(p: f32) -> vec4f { var p4 = fract(vec4f(p) * HASHSCALE4); p4 = p4 + dot(p4, p4.wzxy + 19.19); return fract(vec4f((p4.x + p4.y)*p4.z, (p4.x + p4.z)*p4.y, (p4.y + p4.z)*p4.w, (p4.z + p4.w)*p4.x)); } @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var output: FragmentOutput; if (pcPosition.x > uniform.numParticles) { discard; return output; } readInput(input.vUv0.x); visMode = select(-1.0, 1.0, inShow); let rndFactor = hash41(pcPosition.x + uniform.seed); let particleRate = uniform.rate + uniform.rateDiv * rndFactor.x; outLife = inLife + uniform.delta; let nlife = clamp(outLife / uniform.lifetime, 0.0, 1.0); let internalTexSize = textureDimensions(internalTex0, 0); let lerpResult0 = tex1Dlod_lerp(internalTex0, internalTexSize, vec2f(nlife, 0.0)); var localVelocity = lerpResult0.result; let localVelocityDiv = lerpResult0.unpacked; let lerpResult1 = tex1Dlod_lerp(internalTex1, internalTexSize, vec2f(nlife, 0.0)); var velocity = lerpResult1.result; let velocityDiv = lerpResult1.unpacked; let lerpResult2 = tex1Dlod_lerp(internalTex2, internalTexSize, vec2f(nlife, 0.0)); let params = lerpResult2.result; let paramDiv = lerpResult2.unpacked; var rotSpeed = params.x; let rotSpeedDiv = paramDiv.y; let lerpResult3 = tex1Dlod_lerp(internalTex3, internalTexSize, vec2f(nlife, 0.0)); let radialParams = lerpResult3.result; let radialParamDiv = lerpResult3.unpacked; let radialSpeed = radialParams.x; let radialSpeedDiv = radialParamDiv.y; let respawn = inLife <= 0.0 || outLife >= uniform.lifetime; inPos = select(inPos, calcSpawnPosition(rndFactor.xyz, rndFactor.x), respawn); inAngle = select(inAngle, mix(uniform.startAngle, uniform.startAngle2, rndFactor.x), respawn); #ifndef LOCAL_SPACE var radialVel: vec3f = inPos - uniform.emitterPos; #else var radialVel: vec3f = inPos; #endif radialVel = select(vec3f(0.0), radialSpeed * normalize(radialVel), dot(radialVel, radialVel) > 1.0E-8); radialVel = radialVel + (radialSpeedDiv * vec3f(2.0) - vec3f(1.0)) * uniform.radialSpeedDivMult * rndFactor.xyz; localVelocity = localVelocity + (localVelocityDiv * vec3f(2.0) - vec3f(1.0)) * uniform.localVelocityDivMult * rndFactor.xyz; velocity = velocity + (velocityDiv * vec3f(2.0) - vec3f(1.0)) * uniform.velocityDivMult * rndFactor.xyz; rotSpeed = rotSpeed + (rotSpeedDiv * 2.0 - 1.0) * uniform.rotSpeedDivMult * rndFactor.y; addInitialVelocity(&localVelocity, rndFactor.xyz); #ifndef LOCAL_SPACE outVel = uniform.emitterMatrix * localVelocity + (radialVel + velocity) * uniform.emitterScale; #else outVel = (localVelocity + radialVel) / uniform.emitterScale + uniform.emitterMatrixInv * velocity; #endif outPos = inPos + outVel * uniform.delta; outAngle = inAngle + rotSpeed * uniform.delta; `; var particle_billboardVS = ` let rotationResult = rotateWithMatrix(quadXY, inAngle); let rotatedQuadXY = rotationResult.rotatedVec; rotMatrix = rotationResult.matrix; var localPos = billboard(particlePos, rotatedQuadXY); `; var particle_blendAddPS = ` dBlendModeFogFactor = 0.0; rgb = rgb * saturate(gammaCorrectInput(max(a, 0.0))); if ((rgb.r + rgb.g + rgb.b) < 0.000001) { discard; } `; var particle_blendMultiplyPS = ` rgb = mix(vec3f(1.0), rgb, a); if ((rgb.r + rgb.g + rgb.b) > 2.99) { discard; } `; var particle_blendNormalPS = ` if (a < 0.01) { discard; } `; var particle_cpuVS = ` attribute particle_vertexData: vec4f; attribute particle_vertexData2: vec4f; attribute particle_vertexData3: vec4f; attribute particle_vertexData4: f32; #ifndef USE_MESH attribute particle_vertexData5: vec2f; #else attribute particle_vertexData5: vec4f; #endif uniform matrix_viewProjection: mat4x4f; uniform matrix_model: mat4x4f; #ifndef VIEWMATRIX #define VIEWMATRIX uniform matrix_view: mat4x4f; #endif uniform matrix_normal: mat3x3f; uniform matrix_viewInverse: mat4x4f; uniform numParticles: f32; uniform lifetime: f32; uniform stretch: f32; uniform seed: f32; uniform emitterScale: vec3f; uniform faceTangent: vec3f; uniform faceBinorm: vec3f; #ifdef PARTICLE_GPU #ifdef WRAP uniform wrapBounds: vec3f; #endif #endif #ifdef PARTICLE_GPU var internalTex0: texture_2d; var internalTex1: texture_2d; var internalTex2: texture_2d; #endif uniform emitterPos: vec3f; varying texCoordsAlphaLife: vec4f; struct RotateResult { rotatedVec: vec2f, matrix: mat2x2f } fn rotateWithMatrix(quadXY: vec2f, pRotation: f32) -> RotateResult { let c = cos(pRotation); let s = sin(pRotation); let m = mat2x2f(vec2f(c, -s), vec2f(s, c)); return RotateResult(m * quadXY, m); } fn billboard(InstanceCoords: vec3f, quadXY: vec2f) -> vec3f { var pos: vec3f; #ifdef SCREEN_SPACE pos = vec3f(-1.0, 0.0, 0.0) * quadXY.x + vec3f(0.0, -1.0, 0.0) * quadXY.y; #else pos = -uniform.matrix_viewInverse[0].xyz * quadXY.x + -uniform.matrix_viewInverse[1].xyz * quadXY.y; #endif return pos; } fn customFace(InstanceCoords: vec3f, quadXY: vec2f) -> vec3f { let pos = uniform.faceTangent * quadXY.x + uniform.faceBinorm * quadXY.y; return pos; } fn safeNormalize(v: vec2f) -> vec2f { let l = length(v); return select(v, v / l, l > 1e-06); } @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; var particlePos = input.particle_vertexData.xyz; let inPos = particlePos; let vertPos = input.particle_vertexData3.xyz; var inVel = vec3f(input.particle_vertexData2.w, input.particle_vertexData3.w, input.particle_vertexData5.x); let id = floor(input.particle_vertexData4); let rndFactor = fract(sin(id + 1.0 + uniform.seed)); let rndFactor3 = vec3f(rndFactor, fract(rndFactor*10.0), fract(rndFactor*100.0)); #ifdef LOCAL_SPACE let modelRotation = mat3x3f(uniform.matrix_model[0].xyz, uniform.matrix_model[1].xyz, uniform.matrix_model[2].xyz); inVel = modelRotation * inVel; #endif let velocityV = safeNormalize((mat3x3f(uniform.matrix_view[0].xyz, uniform.matrix_view[1].xyz, uniform.matrix_view[2].xyz) * inVel).xy); let quadXY = vertPos.xy; #ifdef USE_MESH output.texCoordsAlphaLife = vec4f(input.particle_vertexData5.zw, input.particle_vertexData2.z, input.particle_vertexData.w); #else output.texCoordsAlphaLife = vec4f(quadXY * -0.5 + 0.5, input.particle_vertexData2.z, input.particle_vertexData.w); #endif var rotMatrix: mat2x2f; var inAngle = input.particle_vertexData2.x; var particlePosMoved = vec3f(0.0); let meshLocalPos = input.particle_vertexData3.xyz; `; var particle_cpu_endVS = ` localPos = localPos * input.particle_vertexData2.y * uniform.emitterScale; localPos = localPos + particlePos; #ifdef SCREEN_SPACE output.position = vec4f(localPos.x, localPos.y, 0.0, 1.0); #else output.position = uniform.matrix_viewProjection * vec4f(localPos, 1.0); #endif `; var particle_customFaceVS = ` let rotationResult = rotateWithMatrix(quadXY, inAngle); let rotatedQuadXY = rotationResult.rotatedVec; rotMatrix = rotationResult.matrix; var localPos = customFace(particlePos, rotatedQuadXY); `; var particle_endPS = ` rgb = addFog(rgb); rgb = toneMap(rgb); rgb = gammaCorrectOutput(rgb); output.color = vec4f(rgb, a); return output; } `; var particle_endVS = ` localPos = localPos * scale * uniform.emitterScale; localPos = localPos + particlePos; #ifdef SCREEN_SPACE output.position = vec4f(localPos.x, localPos.y, 0.0, 1.0); #else output.position = uniform.matrix_viewProjection * vec4f(localPos.xyz, 1.0); #endif `; var particle_halflambertPS = ` var negNormal: vec3f = normal * 0.5 + 0.5; var posNormal: vec3f = -normal * 0.5 + 0.5; negNormal = negNormal * negNormal; posNormal = posNormal * posNormal; `; var particle_initVS = ` attribute particle_vertexData: vec4f; #if defined(USE_MESH) #if defined(USE_MESH_UV) attribute particle_uv: vec2f; #else var particle_uv: vec2f = vec2f(0.0, 0.0); #endif #endif uniform matrix_viewProjection: mat4x4f; uniform matrix_model: mat4x4f; uniform matrix_normal: mat3x3f; uniform matrix_viewInverse: mat4x4f; #ifndef VIEWMATRIX #define VIEWMATRIX uniform matrix_view: mat4x4f; #endif uniform numParticles: f32; uniform numParticlesPot: f32; uniform graphSampleSize: f32; uniform graphNumSamples: f32; uniform stretch: f32; uniform emitterScale: vec3f; uniform emitterPos: vec3f; uniform faceTangent: vec3f; uniform faceBinorm: vec3f; uniform rate: f32; uniform rateDiv: f32; uniform lifetime: f32; uniform scaleDivMult: f32; uniform alphaDivMult: f32; uniform seed: f32; uniform delta: f32; #ifdef PARTICLE_GPU #ifdef WRAP uniform wrapBounds: vec3f; #endif #endif var particleTexOUT: texture_2d; var particleTexIN: texture_2d; #ifdef PARTICLE_GPU var internalTex0: texture_2d; var internalTex1: texture_2d; var internalTex2: texture_2d; #endif #ifndef CAMERAPLANES #define CAMERAPLANES uniform camera_params: vec4f; #endif varying texCoordsAlphaLife: vec4f; var inPos: vec3f; var inVel: vec3f; var inAngle: f32; var inShow: bool; var inLife: f32; `; var particle_lambertPS = ` var negNormal: vec3f = max(normal, vec3(0.0)); var posNormal: vec3f = max(-normal, vec3(0.0)); `; var particle_lightingPS = ` let light: vec3f = negNormal.x * uniform.lightCube[0] + posNormal.x * uniform.lightCube[1] + negNormal.y * uniform.lightCube[2] + posNormal.y * uniform.lightCube[3] + negNormal.z * uniform.lightCube[4] + posNormal.z * uniform.lightCube[5]; rgb = rgb * light; `; var particle_localShiftVS = ` particlePos = (uniform.matrix_model * vec4f(particlePos, 1.0)).xyz; `; var particle_meshVS = ` var localPos = meshLocalPos; let rotResultXY = rotateWithMatrix(localPos.xy, inAngle); localPos = vec3f(rotResultXY.rotatedVec, localPos.z); rotMatrix = rotResultXY.matrix; let rotResultYZ = rotateWithMatrix(localPos.yz, inAngle); localPos = vec3f(localPos.x, rotResultYZ.rotatedVec); rotMatrix = rotResultYZ.matrix; billboard(particlePos, quadXY); `; var particle_normalVS = ` output.Normal = normalize(localPos + uniform.matrix_viewInverse[2].xyz); `; var particle_normalMapPS = ` let sampledNormal: vec4f = textureSample(normalMap, normalMapSampler, vec2f(input.texCoordsAlphaLife.x, 1.0 - input.texCoordsAlphaLife.y)); let normalMap: vec3f = normalize(sampledNormal.xyz * 2.0 - 1.0); let ParticleMat = mat3x3(ParticleMat0, ParticleMat1, ParticleMat2); let normal: vec3f = ParticleMat * normalMap; `; var particle_pointAlongVS = ` inAngle = atan2(velocityV.x, velocityV.y); `; var particle_simulationPS = ` #include "particleUpdaterInitPS" #ifdef PACK8 #include "particleInputRgba8PS" #include "particleOutputRgba8PS" #else #include "particleInputFloatPS" #include "particleOutputFloatPS" #endif #ifdef EMITTERSHAPE_BOX #include "particleUpdaterAABBPS" #else #include "particleUpdaterSpherePS" #endif #include "particleUpdaterStartPS" #ifdef RESPAWN #include "particleUpdaterRespawnPS" #endif #ifdef NO_RESPAWN #include "particleUpdaterNoRespawnPS" #endif #ifdef ON_STOP #include "particleUpdaterOnStopPS" #endif #include "particleUpdaterEndPS" `; var particle_shaderPS = ` #if NORMAL != NONE #if NORMAL == VERTEX varying Normal: vec3f; #endif #if NORMAL == MAP varying ParticleMat0: vec3f; varying ParticleMat1: vec3f; varying ParticleMat2: vec3f; #endif uniform lightCube: array; #endif #ifdef SOFT varying vDepth: f32; #include "screenDepthPS" #endif #include "gammaPS" #include "tonemappingPS" #include "fogPS" #if NORMAL == MAP var normalMap: texture_2d; var normalMapSampler: sampler; #endif #include "particlePS" #ifdef SOFT #include "particle_softPS" #endif #if NORMAL == VERTEX var normal: vec3f = Normal; #endif #if NORMAL == MAP #include "particle_normalMapPS" #endif #if NORMAL != NONE #ifdef HALF_LAMBERT #include "particle_halflambertPS" #else #include "particle_lambertPS" #endif #include "particle_lightingPS" #endif #if BLEND == NORMAL #include "particle_blendNormalPS" #elif BLEND == ADDITIVE #include "particle_blendAddPS" #elif BLEND == MULTIPLICATIVE #include "particle_blendMultiplyPS" #endif #include "particle_endPS" `; var particle_shaderVS = ` #ifdef ANIMTEX uniform animTexTilesParams: vec2f; uniform animTexParams: vec4f; uniform animTexIndexParams: vec2f; #endif #if NORMAL == MAP varying ParticleMat0: vec3f; varying ParticleMat1: vec3f; varying ParticleMat2: vec3f; #endif #if NORMAL == VERTEX varying Normal: vec3f; #endif #ifdef SOFT varying vDepth: f32; #endif #ifdef PARTICLE_GPU #include "particle_initVS" #ifdef PACK8 #include "particleInputRgba8PS" #else #include "particleInputFloatPS" #endif #ifdef SOFT #include "screenDepthPS" #endif #include "particleVS" #else #ifdef SOFT #include "screenDepthPS" #endif #include "particle_cpuVS" #endif #ifdef LOCAL_SPACE #include "particle_localShiftVS" #endif #ifdef ANIMTEX #ifdef ANIMTEX_LOOP #include "particleAnimFrameLoopVS" #else #include "particleAnimFrameClampVS" #endif #include "particleAnimTexVS" #endif #ifdef PARTICLE_GPU #ifdef WRAP #include "particle_wrapVS" #endif #endif #ifdef ALIGN_TO_MOTION #include "particle_pointAlongVS" #endif #ifdef USE_MESH #include "particle_meshVS" #else #ifdef CUSTOM_FACE #include "particle_customFaceVS" #else #include "particle_billboardVS" #endif #endif #if NORMAL == VERTEX #include "particle_normalVS" #endif #if NORMAL == MAP #include "particle_TBNVS" #endif #ifdef STRETCH #include "particle_stretchVS" #endif #ifdef PARTICLE_GPU #include "particle_endVS" #else #include "particle_cpu_endVS" #endif #ifdef SOFT #include "particle_softVS" #endif return output; } `; var particle_softPS = ` var depth: f32 = getLinearScreenDepthFrag(); var particleDepth: f32 = vDepth; var depthDiff: f32 = saturate(abs(particleDepth - depth) * uniform.softening); a = a * depthDiff; `; var particle_softVS = ` output.vDepth = getLinearDepth(localPos); `; var particle_stretchVS = ` let moveDir: vec3f = inVel * uniform.stretch; var posPrev: vec3f = particlePos - moveDir; posPrev = posPrev + particlePosMoved; let viewRotationTemp: mat3x3f = mat3x3f(uniform.matrix_view[0].xyz, uniform.matrix_view[1].xyz, uniform.matrix_view[2].xyz); let centerToVertexV: vec2f = normalize((viewRotationTemp * localPos).xy); let interpolation: f32 = dot(-velocityV, centerToVertexV) * 0.5 + 0.5; particlePos = mix(particlePos, posPrev, interpolation); `; var particle_TBNVS = ` let rot3 = mat3x3f( vec3f(rotMatrix[0][0], rotMatrix[1][0], 0.0), vec3f(rotMatrix[0][1], rotMatrix[1][1], 0.0), vec3f(0.0, 0.0, 1.0) ); let viewBasis = mat3x3f( -uniform.matrix_viewInverse[0].xyz, -uniform.matrix_viewInverse[1].xyz, uniform.matrix_viewInverse[2].xyz ); let tempMat = viewBasis * rot3; output.ParticleMat0 = tempMat[0]; output.ParticleMat1 = tempMat[1]; output.ParticleMat2 = tempMat[2]; `; var particle_wrapVS = ` let origParticlePos: vec3f = particlePos; particlePos = particlePos - uniform.matrix_model[3].xyz; particlePos = (particlePos % uniform.wrapBounds) - uniform.wrapBounds * 0.5; particlePos = particlePos + uniform.matrix_model[3].xyz; particlePosMoved = particlePos - origParticlePos; `; const particleChunksWGSL = { particlePS, particleVS, particleAnimFrameClampVS, particleAnimFrameLoopVS, particleAnimTexVS, particleInputFloatPS, particleInputRgba8PS, particleOutputFloatPS, particleOutputRgba8PS, particleUpdaterAABBPS, particleUpdaterEndPS, particleUpdaterInitPS, particleUpdaterNoRespawnPS, particleUpdaterOnStopPS, particleUpdaterRespawnPS, particleUpdaterSpherePS, particleUpdaterStartPS, particle_billboardVS, particle_blendAddPS, particle_blendMultiplyPS, particle_blendNormalPS, particle_cpuVS, particle_cpu_endVS, particle_customFaceVS, particle_endPS, particle_endVS, particle_halflambertPS, particle_initVS, particle_lambertPS, particle_lightingPS, particle_localShiftVS, particle_meshVS, particle_normalVS, particle_normalMapPS, particle_pointAlongVS, particle_simulationPS, particle_shaderPS, particle_shaderVS, particle_softPS, particle_softVS, particle_stretchVS, particle_TBNVS, particle_wrapVS }; const _schema$a = [ 'enabled', 'autoPlay', 'numParticles', 'lifetime', 'rate', 'rate2', 'startAngle', 'startAngle2', 'loop', 'preWarm', 'lighting', 'halfLambert', 'intensity', 'depthWrite', 'noFog', 'depthSoftening', 'sort', 'blendType', 'stretch', 'alignToMotion', 'emitterShape', 'emitterExtents', 'emitterExtentsInner', 'emitterRadius', 'emitterRadiusInner', 'initialVelocity', 'wrap', 'wrapBounds', 'localSpace', 'screenSpace', 'colorMapAsset', 'normalMapAsset', 'mesh', 'meshAsset', 'renderAsset', 'orientation', 'particleNormal', 'localVelocityGraph', 'localVelocityGraph2', 'velocityGraph', 'velocityGraph2', 'rotationSpeedGraph', 'rotationSpeedGraph2', 'radialSpeedGraph', 'radialSpeedGraph2', 'scaleGraph', 'scaleGraph2', 'colorGraph', 'colorGraph2', 'alphaGraph', 'alphaGraph2', 'colorMap', 'normalMap', 'animTilesX', 'animTilesY', 'animStartFrame', 'animNumFrames', 'animNumAnimations', 'animIndex', 'randomizeAnimIndex', 'animSpeed', 'animLoop', 'layers' ]; class ParticleSystemComponentSystem extends ComponentSystem { initializeComponentData(component, _data, properties) { const data = {}; properties = []; const types = this.propertyTypes; if (_data.mesh instanceof Asset || typeof _data.mesh === 'number') { _data.meshAsset = _data.mesh; delete _data.mesh; } for(const prop in _data){ if (_data.hasOwnProperty(prop)) { properties.push(prop); data[prop] = _data[prop]; } if (types[prop] === 'vec3') { if (Array.isArray(data[prop])) { data[prop] = new Vec3(data[prop][0], data[prop][1], data[prop][2]); } } else if (types[prop] === 'curve') { if (!(data[prop] instanceof Curve)) { const t = data[prop].type; data[prop] = new Curve(data[prop].keys); data[prop].type = t; } } else if (types[prop] === 'curveset') { if (!(data[prop] instanceof CurveSet)) { const t = data[prop].type; data[prop] = new CurveSet(data[prop].keys); data[prop].type = t; } } if (data.layers && Array.isArray(data.layers)) { data.layers = data.layers.slice(0); } } super.initializeComponentData(component, data, properties); } cloneComponent(entity, clone) { const source = entity.particlesystem.data; const schema = this.schema; const data = {}; for(let i = 0, len = schema.length; i < len; i++){ const prop = schema[i]; let sourceProp = source[prop]; if (sourceProp instanceof Vec3 || sourceProp instanceof Curve || sourceProp instanceof CurveSet) { sourceProp = sourceProp.clone(); data[prop] = sourceProp; } else if (prop === 'layers') { data.layers = source.layers.slice(0); } else { if (sourceProp !== null && sourceProp !== undefined) { data[prop] = sourceProp; } } } return this.addComponent(clone, data); } onUpdate(dt) { const components = this.store; const stats = this.app.stats.particles; const composition = this.app.scene.layers; for(let i = 0; i < composition.layerList.length; i++){ composition.layerList[i].requiresLightCube = false; } for(const id in components){ if (components.hasOwnProperty(id)) { const component = components[id]; const entity = component.entity; const data = component.data; if (data.enabled && entity.enabled) { const emitter = entity.particlesystem.emitter; if (!emitter?.meshInstance.visible) continue; if (emitter.lighting) { const layers = data.layers; for(let i = 0; i < layers.length; i++){ const layer = composition.getLayerById(layers[i]); if (layer) { layer.requiresLightCube = true; } } } if (!data.paused) { let numSteps = 0; emitter.simTime += dt; if (emitter.simTime >= emitter.fixedTimeStep) { numSteps = Math.floor(emitter.simTime / emitter.fixedTimeStep); emitter.simTime -= numSteps * emitter.fixedTimeStep; } if (numSteps) { numSteps = Math.min(numSteps, emitter.maxSubSteps); for(let i = 0; i < numSteps; i++){ emitter.addTime(emitter.fixedTimeStep, false); } stats._updatesPerFrame += numSteps; stats._frameTime += emitter._addTimeTime; emitter._addTimeTime = 0; } emitter.finishFrame(); } } } } } onBeforeRemove(entity, component) { component.onBeforeRemove(); } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); } constructor(app){ super(app); this.id = 'particlesystem'; this.ComponentType = ParticleSystemComponent; this.DataType = ParticleSystemComponentData; this.schema = _schema$a; this.propertyTypes = { emitterExtents: 'vec3', emitterExtentsInner: 'vec3', particleNormal: 'vec3', wrapBounds: 'vec3', localVelocityGraph: 'curveset', localVelocityGraph2: 'curveset', velocityGraph: 'curveset', velocityGraph2: 'curveset', colorGraph: 'curveset', colorGraph2: 'curveset', alphaGraph: 'curve', alphaGraph2: 'curve', rotationSpeedGraph: 'curve', rotationSpeedGraph2: 'curve', radialSpeedGraph: 'curve', radialSpeedGraph2: 'curve', scaleGraph: 'curve', scaleGraph2: 'curve' }; this.on('beforeremove', this.onBeforeRemove, this); this.app.systems.on('update', this.onUpdate, this); ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_GLSL).add(particleChunksGLSL); ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_WGSL).add(particleChunksWGSL); } } class SkinInstanceCachedObject extends RefCountedObject { constructor(skin, skinInstance){ super(); this.skin = skin; this.skinInstance = skinInstance; } } class SkinInstanceCache { static createCachedSkinInstance(skin, rootBone, entity) { let skinInst = SkinInstanceCache.getCachedSkinInstance(skin, rootBone); if (!skinInst) { skinInst = new SkinInstance(skin); skinInst.resolve(rootBone, entity); SkinInstanceCache.addCachedSkinInstance(skin, rootBone, skinInst); } return skinInst; } static getCachedSkinInstance(skin, rootBone) { let skinInstance = null; const cachedObjArray = SkinInstanceCache._skinInstanceCache.get(rootBone); if (cachedObjArray) { const cachedObj = cachedObjArray.find((element)=>element.skin === skin); if (cachedObj) { cachedObj.incRefCount(); skinInstance = cachedObj.skinInstance; } } return skinInstance; } static addCachedSkinInstance(skin, rootBone, skinInstance) { let cachedObjArray = SkinInstanceCache._skinInstanceCache.get(rootBone); if (!cachedObjArray) { cachedObjArray = []; SkinInstanceCache._skinInstanceCache.set(rootBone, cachedObjArray); } let cachedObj = cachedObjArray.find((element)=>element.skin === skin); if (!cachedObj) { cachedObj = new SkinInstanceCachedObject(skin, skinInstance); cachedObjArray.push(cachedObj); } cachedObj.incRefCount(); } static removeCachedSkinInstance(skinInstance) { if (skinInstance) { const rootBone = skinInstance.rootBone; if (rootBone) { const cachedObjArray = SkinInstanceCache._skinInstanceCache.get(rootBone); if (cachedObjArray) { const cachedObjIndex = cachedObjArray.findIndex((element)=>element.skinInstance === skinInstance); if (cachedObjIndex >= 0) { const cachedObj = cachedObjArray[cachedObjIndex]; cachedObj.decRefCount(); if (cachedObj.refCount === 0) { cachedObjArray.splice(cachedObjIndex, 1); if (!cachedObjArray.length) { SkinInstanceCache._skinInstanceCache.delete(rootBone); } if (skinInstance) { skinInstance.destroy(); cachedObj.skinInstance = null; } } } } } } } } SkinInstanceCache._skinInstanceCache = new Map(); class AssetReference { set id(value) { if (this.url) throw Error('Can\'t set id and url'); this._unbind(); this._id = value; this.asset = this._registry.get(this._id); this._bind(); } get id() { return this._id; } set url(value) { if (this.id) throw Error('Can\'t set id and url'); this._unbind(); this._url = value; this.asset = this._registry.getByUrl(this._url); this._bind(); } get url() { return this._url; } _bind() { if (this.id) { if (this._onAssetLoad) this._evtLoadById = this._registry.on(`load:${this.id}`, this._onLoad, this); if (this._onAssetAdd) this._evtAddById = this._registry.once(`add:${this.id}`, this._onAdd, this); if (this._onAssetRemove) this._evtRemoveById = this._registry.on(`remove:${this.id}`, this._onRemove, this); if (this._onAssetUnload) this._evtUnloadById = this._registry.on(`unload:${this.id}`, this._onUnload, this); } if (this.url) { if (this._onAssetLoad) this._evtLoadByUrl = this._registry.on(`load:url:${this.url}`, this._onLoad, this); if (this._onAssetAdd) this._evtAddByUrl = this._registry.once(`add:url:${this.url}`, this._onAdd, this); if (this._onAssetRemove) this._evtRemoveByUrl = this._registry.on(`remove:url:${this.url}`, this._onRemove, this); } } _unbind() { if (this.id) { this._evtLoadById?.off(); this._evtLoadById = null; this._evtAddById?.off(); this._evtAddById = null; this._evtRemoveById?.off(); this._evtRemoveById = null; this._evtUnloadById?.off(); this._evtUnloadById = null; } if (this.url) { this._evtLoadByUrl?.off(); this._evtLoadByUrl = null; this._evtAddByUrl?.off(); this._evtAddByUrl = null; this._evtRemoveByUrl?.off(); this._evtRemoveByUrl = null; } } _onLoad(asset) { this._onAssetLoad.call(this._scope, this.propertyName, this.parent, asset); } _onAdd(asset) { this.asset = asset; this._onAssetAdd.call(this._scope, this.propertyName, this.parent, asset); } _onRemove(asset) { this._onAssetRemove.call(this._scope, this.propertyName, this.parent, asset); this.asset = null; } _onUnload(asset) { this._onAssetUnload.call(this._scope, this.propertyName, this.parent, asset); } constructor(propertyName, parent, registry, callbacks, scope){ this._evtLoadById = null; this._evtUnloadById = null; this._evtAddById = null; this._evtRemoveById = null; this._evtLoadByUrl = null; this._evtAddByUrl = null; this._evtRemoveByUrl = null; this.propertyName = propertyName; this.parent = parent; this._scope = scope; this._registry = registry; this.id = null; this.url = null; this.asset = null; this._onAssetLoad = callbacks.load; this._onAssetAdd = callbacks.add; this._onAssetRemove = callbacks.remove; this._onAssetUnload = callbacks.unload; } } class RenderComponent extends Component { set renderStyle(renderStyle) { if (this._renderStyle !== renderStyle) { this._renderStyle = renderStyle; MeshInstance._prepareRenderStyleForArray(this._meshInstances, renderStyle); } } get renderStyle() { return this._renderStyle; } set customAabb(value) { this._customAabb = value; const mi = this._meshInstances; if (mi) { for(let i = 0; i < mi.length; i++){ mi[i].setCustomAabb(this._customAabb); } } } get customAabb() { return this._customAabb; } set type(value) { if (this._type !== value) { this._area = null; this._type = value; this.destroyMeshInstances(); if (value !== 'asset') { let material = this._material; if (!material || material === this.system.defaultMaterial) { material = this._materialReferences[0] && this._materialReferences[0].asset && this._materialReferences[0].asset.resource; } const primData = getShapePrimitive(this.system.app.graphicsDevice, value); this._area = primData.area; this.meshInstances = [ new MeshInstance(primData.mesh, material || this.system.defaultMaterial, this.entity) ]; } } } get type() { return this._type; } set meshInstances(value) { this.destroyMeshInstances(); this._meshInstances = value; if (this._meshInstances) { const mi = this._meshInstances; for(let i = 0; i < mi.length; i++){ if (!mi[i].node) { mi[i].node = this.entity; } mi[i].castShadow = this._castShadows; mi[i].receiveShadow = this._receiveShadows; mi[i].renderStyle = this._renderStyle; mi[i].setLightmapped(this._lightmapped); mi[i].setCustomAabb(this._customAabb); } if (this.enabled && this.entity.enabled) { this.addToLayers(); } } } get meshInstances() { return this._meshInstances; } set lightmapped(value) { if (value !== this._lightmapped) { this._lightmapped = value; const mi = this._meshInstances; if (mi) { for(let i = 0; i < mi.length; i++){ mi[i].setLightmapped(value); } } } } get lightmapped() { return this._lightmapped; } set castShadows(value) { if (this._castShadows !== value) { const mi = this._meshInstances; if (mi) { const layers = this.layers; const scene = this.system.app.scene; if (this._castShadows && !value) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(this.layers[i]); if (layer) { layer.removeShadowCasters(mi); } } } for(let i = 0; i < mi.length; i++){ mi[i].castShadow = value; } if (!this._castShadows && value) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); if (layer) { layer.addShadowCasters(mi); } } } } this._castShadows = value; } } get castShadows() { return this._castShadows; } set receiveShadows(value) { if (this._receiveShadows !== value) { this._receiveShadows = value; const mi = this._meshInstances; if (mi) { for(let i = 0; i < mi.length; i++){ mi[i].receiveShadow = value; } } } } get receiveShadows() { return this._receiveShadows; } set castShadowsLightmap(value) { this._castShadowsLightmap = value; } get castShadowsLightmap() { return this._castShadowsLightmap; } set lightmapSizeMultiplier(value) { this._lightmapSizeMultiplier = value; } get lightmapSizeMultiplier() { return this._lightmapSizeMultiplier; } set layers(value) { const layers = this.system.app.scene.layers; let layer; if (this._meshInstances) { for(let i = 0; i < this._layers.length; i++){ layer = layers.getLayerById(this._layers[i]); if (layer) { layer.removeMeshInstances(this._meshInstances); } } } this._layers.length = 0; for(let i = 0; i < value.length; i++){ this._layers[i] = value[i]; } if (!this.enabled || !this.entity.enabled || !this._meshInstances) return; for(let i = 0; i < this._layers.length; i++){ layer = layers.getLayerById(this._layers[i]); if (layer) { layer.addMeshInstances(this._meshInstances); } } } get layers() { return this._layers; } set batchGroupId(value) { if (this._batchGroupId !== value) { if (this.entity.enabled && this._batchGroupId >= 0) { this.system.app.batcher?.remove(BatchGroup.RENDER, this.batchGroupId, this.entity); } if (this.entity.enabled && value >= 0) { this.system.app.batcher?.insert(BatchGroup.RENDER, value, this.entity); } if (value < 0 && this._batchGroupId >= 0 && this.enabled && this.entity.enabled) { this.addToLayers(); } this._batchGroupId = value; } } get batchGroupId() { return this._batchGroupId; } set material(value) { if (this._material !== value) { this._material = value; if (this._meshInstances && this._type !== 'asset') { for(let i = 0; i < this._meshInstances.length; i++){ this._meshInstances[i].material = value; } } } } get material() { return this._material; } set materialAssets(value = []) { if (this._materialReferences.length > value.length) { for(let i = value.length; i < this._materialReferences.length; i++){ this._materialReferences[i].id = null; } this._materialReferences.length = value.length; } for(let i = 0; i < value.length; i++){ if (!this._materialReferences[i]) { this._materialReferences.push(new AssetReference(i, this, this.system.app.assets, { add: this._onMaterialAdded, load: this._onMaterialLoad, remove: this._onMaterialRemove, unload: this._onMaterialUnload }, this)); } if (value[i]) { const id = value[i] instanceof Asset ? value[i].id : value[i]; if (this._materialReferences[i].id !== id) { this._materialReferences[i].id = id; } if (this._materialReferences[i].asset) { this._onMaterialAdded(i, this, this._materialReferences[i].asset); } } else { this._materialReferences[i].id = null; if (this._meshInstances[i]) { this._meshInstances[i].material = this.system.defaultMaterial; } } } } get materialAssets() { return this._materialReferences.map((ref)=>{ return ref.id; }); } set asset(value) { const id = value instanceof Asset ? value.id : value; if (this._assetReference.id === id) return; if (this._assetReference.asset && this._assetReference.asset.resource) { this._onRenderAssetRemove(); } this._assetReference.id = id; if (this._assetReference.asset) { this._onRenderAssetAdded(); } } get asset() { return this._assetReference.id; } assignAsset(asset) { const id = asset instanceof Asset ? asset.id : asset; this._assetReference.id = id; } set rootBone(value) { if (this._rootBone !== value) { const isString = typeof value === 'string'; if (this._rootBone && isString && this._rootBone.getGuid() === value) { return; } if (this._rootBone) { this._clearSkinInstances(); } if (value instanceof GraphNode) { this._rootBone = value; } else if (isString) { this._rootBone = this.system.app.getEntityFromIndex(value) || null; if (!this._rootBone) ; } else { this._rootBone = null; } if (this._rootBone) { this._cloneSkinInstances(); } } } get rootBone() { return this._rootBone; } destroyMeshInstances() { const meshInstances = this._meshInstances; if (meshInstances) { this.removeFromLayers(); this._clearSkinInstances(); for(let i = 0; i < meshInstances.length; i++){ meshInstances[i].destroy(); } this._meshInstances.length = 0; } } addToLayers() { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (layer) { layer.addMeshInstances(this._meshInstances); } } } removeFromLayers() { if (this._meshInstances && this._meshInstances.length) { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (layer) { layer.removeMeshInstances(this._meshInstances); } } } } onRemoveChild() { this.removeFromLayers(); } onInsertChild() { if (this._meshInstances && this.enabled && this.entity.enabled) { this.addToLayers(); } } onRemove() { this.destroyMeshInstances(); this.asset = null; this.materialAsset = null; this._assetReference.id = null; for(let i = 0; i < this._materialReferences.length; i++){ this._materialReferences[i].id = null; } this.entity.off('remove', this.onRemoveChild, this); this.entity.off('insert', this.onInsertChild, this); } onLayersChanged(oldComp, newComp) { this.addToLayers(); oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.addMeshInstances(this._meshInstances); } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.removeMeshInstances(this._meshInstances); } onEnable() { const app = this.system.app; const scene = app.scene; const layers = scene.layers; if (this._rootBone) { this._cloneSkinInstances(); } this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } const isAsset = this._type === 'asset'; if (this._meshInstances && this._meshInstances.length) { this.addToLayers(); } else if (isAsset && this.asset) { this._onRenderAssetAdded(); } for(let i = 0; i < this._materialReferences.length; i++){ if (this._materialReferences[i].asset) { this.system.app.assets.load(this._materialReferences[i].asset); } } if (this._batchGroupId >= 0) { app.batcher?.insert(BatchGroup.RENDER, this.batchGroupId, this.entity); } } onDisable() { const app = this.system.app; const scene = app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (this._rootBone) { this._clearSkinInstances(); } if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } if (this._batchGroupId >= 0) { app.batcher?.remove(BatchGroup.RENDER, this.batchGroupId, this.entity); } this.removeFromLayers(); } hide() { if (this._meshInstances) { for(let i = 0; i < this._meshInstances.length; i++){ this._meshInstances[i].visible = false; } } } show() { if (this._meshInstances) { for(let i = 0; i < this._meshInstances.length; i++){ this._meshInstances[i].visible = true; } } } _onRenderAssetAdded() { if (!this._assetReference.asset) return; if (this._assetReference.asset.resource) { this._onRenderAssetLoad(); } else if (this.enabled && this.entity.enabled) { this.system.app.assets.load(this._assetReference.asset); } } _onRenderAssetLoad() { this.destroyMeshInstances(); if (this._assetReference.asset) { const render = this._assetReference.asset.resource; this._evtSetMeshes?.off(); this._evtSetMeshes = render.on('set:meshes', this._onSetMeshes, this); if (render.meshes) { this._onSetMeshes(render.meshes); } } } _onSetMeshes(meshes) { this._cloneMeshes(meshes); } _clearSkinInstances() { for(let i = 0; i < this._meshInstances.length; i++){ const meshInstance = this._meshInstances[i]; SkinInstanceCache.removeCachedSkinInstance(meshInstance.skinInstance); meshInstance.skinInstance = null; } } _cloneSkinInstances() { if (this._meshInstances.length && this._rootBone instanceof GraphNode) { for(let i = 0; i < this._meshInstances.length; i++){ const meshInstance = this._meshInstances[i]; const mesh = meshInstance.mesh; if (mesh.skin && !meshInstance.skinInstance) { meshInstance.skinInstance = SkinInstanceCache.createCachedSkinInstance(mesh.skin, this._rootBone, this.entity); } } } } _cloneMeshes(meshes) { if (meshes && meshes.length) { const meshInstances = []; for(let i = 0; i < meshes.length; i++){ const mesh = meshes[i]; const material = this._materialReferences[i] && this._materialReferences[i].asset && this._materialReferences[i].asset.resource; const meshInst = new MeshInstance(mesh, material || this.system.defaultMaterial, this.entity); meshInstances.push(meshInst); if (mesh.morph) { meshInst.morphInstance = new MorphInstance(mesh.morph); } } this.meshInstances = meshInstances; this._cloneSkinInstances(); } } _onRenderAssetUnload() { if (this._type === 'asset') { this.destroyMeshInstances(); } } _onRenderAssetRemove() { this._evtSetMeshes?.off(); this._evtSetMeshes = null; this._onRenderAssetUnload(); } _onMaterialAdded(index, component, asset) { if (asset.resource) { this._onMaterialLoad(index, component, asset); } else { if (this.enabled && this.entity.enabled) { this.system.app.assets.load(asset); } } } _updateMainMaterial(index, material) { if (index === 0) { this.material = material; } } _onMaterialLoad(index, component, asset) { if (this._meshInstances[index]) { this._meshInstances[index].material = asset.resource; } this._updateMainMaterial(index, asset.resource); } _onMaterialRemove(index, component, asset) { if (this._meshInstances[index]) { this._meshInstances[index].material = this.system.defaultMaterial; } this._updateMainMaterial(index, this.system.defaultMaterial); } _onMaterialUnload(index, component, asset) { if (this._meshInstances[index]) { this._meshInstances[index].material = this.system.defaultMaterial; } this._updateMainMaterial(index, this.system.defaultMaterial); } resolveDuplicatedEntityReferenceProperties(oldRender, duplicatedIdsMap) { if (oldRender.rootBone) { this.rootBone = duplicatedIdsMap[oldRender.rootBone.getGuid()]; } } constructor(system, entity){ super(system, entity), this._type = 'asset', this._castShadows = true, this._receiveShadows = true, this._castShadowsLightmap = true, this._lightmapped = false, this._lightmapSizeMultiplier = 1, this.isStatic = false, this._batchGroupId = -1, this._layers = [ LAYERID_WORLD ], this._renderStyle = RENDERSTYLE_SOLID, this._meshInstances = [], this._customAabb = null, this._area = null, this._materialReferences = [], this._rootBone = null, this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null, this._evtSetMeshes = null; this._assetReference = new AssetReference('asset', this, system.app.assets, { add: this._onRenderAssetAdded, load: this._onRenderAssetLoad, remove: this._onRenderAssetRemove, unload: this._onRenderAssetUnload }, this); this._material = system.defaultMaterial; entity.on('remove', this.onRemoveChild, this); entity.on('removehierarchy', this.onRemoveChild, this); entity.on('insert', this.onInsertChild, this); entity.on('inserthierarchy', this.onInsertChild, this); } } class RenderComponentData { constructor(){ this.enabled = true; } } const _schema$9 = [ 'enabled' ]; const _properties$1 = [ 'material', 'meshInstances', 'asset', 'materialAssets', 'castShadows', 'receiveShadows', 'castShadowsLightmap', 'lightmapped', 'lightmapSizeMultiplier', 'renderStyle', 'type', 'layers', 'isStatic', 'batchGroupId', 'rootBone' ]; class RenderComponentSystem extends ComponentSystem { initializeComponentData(component, _data, properties) { if (_data.batchGroupId === null || _data.batchGroupId === undefined) { _data.batchGroupId = -1; } if (_data.layers && _data.layers.length) { _data.layers = _data.layers.slice(0); } for(let i = 0; i < _properties$1.length; i++){ if (_data.hasOwnProperty(_properties$1[i])) { component[_properties$1[i]] = _data[_properties$1[i]]; } } if (_data.aabbCenter && _data.aabbHalfExtents) { component.customAabb = new BoundingBox(new Vec3(_data.aabbCenter), new Vec3(_data.aabbHalfExtents)); } super.initializeComponentData(component, _data, _schema$9); } cloneComponent(entity, clone) { const data = {}; for(let i = 0; i < _properties$1.length; i++){ data[_properties$1[i]] = entity.render[_properties$1[i]]; } data.enabled = entity.render.enabled; delete data.meshInstances; const component = this.addComponent(clone, data); const srcMeshInstances = entity.render.meshInstances; const meshes = srcMeshInstances.map((mi)=>mi.mesh); component._onSetMeshes(meshes); for(let m = 0; m < srcMeshInstances.length; m++){ component.meshInstances[m].material = srcMeshInstances[m].material; } if (entity.render.customAabb) { component.customAabb = entity.render.customAabb.clone(); } return component; } onRemove(entity, component) { component.onRemove(); } constructor(app){ super(app); this.id = 'render'; this.ComponentType = RenderComponent; this.DataType = RenderComponentData; this.schema = _schema$9; this.defaultMaterial = getDefaultMaterial(app.graphicsDevice); this.on('beforeremove', this.onRemove, this); } } Component._buildAccessors(RenderComponent.prototype, _schema$9); class ObjectPool { _resize(size) { if (size > this._pool.length) { for(let i = this._pool.length; i < size; i++){ this._pool[i] = new this._constructor(); } } } allocate() { if (this._count >= this._pool.length) { this._resize(this._pool.length * 2); } return this._pool[this._count++]; } freeAll() { this._count = 0; } constructor(constructorFunc, size){ this._pool = []; this._count = 0; this._constructor = constructorFunc; this._resize(size); } } let _ammoTransform; let _ammoVec1, _ammoVec2, _ammoQuat; const _quat1 = new Quat(); const _quat2 = new Quat(); const _vec3 = new Vec3(); class RigidBodyComponent extends Component { static onLibraryLoaded() { if (typeof Ammo !== 'undefined') { _ammoTransform = new Ammo.btTransform(); _ammoVec1 = new Ammo.btVector3(); _ammoVec2 = new Ammo.btVector3(); _ammoQuat = new Ammo.btQuaternion(); } } static onAppDestroy() { Ammo.destroy(_ammoTransform); Ammo.destroy(_ammoVec1); Ammo.destroy(_ammoVec2); Ammo.destroy(_ammoQuat); _ammoTransform = null; _ammoVec1 = null; _ammoVec2 = null; _ammoQuat = null; } set angularDamping(damping) { if (this._angularDamping !== damping) { this._angularDamping = damping; if (this._body) { this._body.setDamping(this._linearDamping, damping); } } } get angularDamping() { return this._angularDamping; } set angularFactor(factor) { if (!this._angularFactor.equals(factor)) { this._angularFactor.copy(factor); if (this._body && this._type === BODYTYPE_DYNAMIC) { _ammoVec1.setValue(factor.x, factor.y, factor.z); this._body.setAngularFactor(_ammoVec1); } } } get angularFactor() { return this._angularFactor; } set angularVelocity(velocity) { if (this._body && this._type === BODYTYPE_DYNAMIC) { this._body.activate(); _ammoVec1.setValue(velocity.x, velocity.y, velocity.z); this._body.setAngularVelocity(_ammoVec1); this._angularVelocity.copy(velocity); } } get angularVelocity() { if (this._body && this._type === BODYTYPE_DYNAMIC) { const velocity = this._body.getAngularVelocity(); this._angularVelocity.set(velocity.x(), velocity.y(), velocity.z()); } return this._angularVelocity; } set body(body) { if (this._body !== body) { this._body = body; if (body && this._simulationEnabled) { body.activate(); } } } get body() { return this._body; } set friction(friction) { if (this._friction !== friction) { this._friction = friction; if (this._body) { this._body.setFriction(friction); } } } get friction() { return this._friction; } set group(group) { if (this._group !== group) { this._group = group; if (this.enabled && this.entity.enabled) { this.disableSimulation(); this.enableSimulation(); } } } get group() { return this._group; } set linearDamping(damping) { if (this._linearDamping !== damping) { this._linearDamping = damping; if (this._body) { this._body.setDamping(damping, this._angularDamping); } } } get linearDamping() { return this._linearDamping; } set linearFactor(factor) { if (!this._linearFactor.equals(factor)) { this._linearFactor.copy(factor); if (this._body && this._type === BODYTYPE_DYNAMIC) { _ammoVec1.setValue(factor.x, factor.y, factor.z); this._body.setLinearFactor(_ammoVec1); } } } get linearFactor() { return this._linearFactor; } set linearVelocity(velocity) { if (this._body && this._type === BODYTYPE_DYNAMIC) { this._body.activate(); _ammoVec1.setValue(velocity.x, velocity.y, velocity.z); this._body.setLinearVelocity(_ammoVec1); this._linearVelocity.copy(velocity); } } get linearVelocity() { if (this._body && this._type === BODYTYPE_DYNAMIC) { const velocity = this._body.getLinearVelocity(); this._linearVelocity.set(velocity.x(), velocity.y(), velocity.z()); } return this._linearVelocity; } set mask(mask) { if (this._mask !== mask) { this._mask = mask; if (this.enabled && this.entity.enabled) { this.disableSimulation(); this.enableSimulation(); } } } get mask() { return this._mask; } set mass(mass) { if (this._mass !== mass) { this._mass = mass; if (this._body && this._type === BODYTYPE_DYNAMIC) { const enabled = this.enabled && this.entity.enabled; if (enabled) { this.disableSimulation(); } this._body.getCollisionShape().calculateLocalInertia(mass, _ammoVec1); this._body.setMassProps(mass, _ammoVec1); this._body.updateInertiaTensor(); if (enabled) { this.enableSimulation(); } } } } get mass() { return this._mass; } set restitution(restitution) { if (this._restitution !== restitution) { this._restitution = restitution; if (this._body) { this._body.setRestitution(restitution); } } } get restitution() { return this._restitution; } set rollingFriction(friction) { if (this._rollingFriction !== friction) { this._rollingFriction = friction; if (this._body) { this._body.setRollingFriction(friction); } } } get rollingFriction() { return this._rollingFriction; } set type(type) { if (this._type !== type) { this._type = type; this.disableSimulation(); switch(type){ case BODYTYPE_DYNAMIC: this._group = BODYGROUP_DYNAMIC; this._mask = BODYMASK_ALL; break; case BODYTYPE_KINEMATIC: this._group = BODYGROUP_KINEMATIC; this._mask = BODYMASK_ALL; break; case BODYTYPE_STATIC: default: this._group = BODYGROUP_STATIC; this._mask = BODYMASK_NOT_STATIC; break; } this.createBody(); } } get type() { return this._type; } createBody() { const entity = this.entity; let shape; if (entity.collision) { shape = entity.collision.shape; if (entity.trigger) { entity.trigger.destroy(); delete entity.trigger; } } if (shape) { if (this._body) { this.system.removeBody(this._body); this.system.destroyBody(this._body); this._body = null; } const mass = this._type === BODYTYPE_DYNAMIC ? this._mass : 0; this._getEntityTransform(_ammoTransform); const body = this.system.createBody(mass, shape, _ammoTransform); body.setRestitution(this._restitution); body.setFriction(this._friction); body.setRollingFriction(this._rollingFriction); body.setDamping(this._linearDamping, this._angularDamping); if (this._type === BODYTYPE_DYNAMIC) { const linearFactor = this._linearFactor; _ammoVec1.setValue(linearFactor.x, linearFactor.y, linearFactor.z); body.setLinearFactor(_ammoVec1); const angularFactor = this._angularFactor; _ammoVec1.setValue(angularFactor.x, angularFactor.y, angularFactor.z); body.setAngularFactor(_ammoVec1); } else if (this._type === BODYTYPE_KINEMATIC) { body.setCollisionFlags(body.getCollisionFlags() | BODYFLAG_KINEMATIC_OBJECT); body.setActivationState(BODYSTATE_DISABLE_DEACTIVATION); } body.entity = entity; this.body = body; if (this.enabled && entity.enabled) { this.enableSimulation(); } } } isActive() { return this._body ? this._body.isActive() : false; } activate() { if (this._body) { this._body.activate(); } } enableSimulation() { const entity = this.entity; if (entity.collision && entity.collision.enabled && !this._simulationEnabled) { const body = this._body; if (body) { this.system.addBody(body, this._group, this._mask); switch(this._type){ case BODYTYPE_DYNAMIC: this.system._dynamic.push(this); body.forceActivationState(BODYSTATE_ACTIVE_TAG); this.syncEntityToBody(); break; case BODYTYPE_KINEMATIC: this.system._kinematic.push(this); body.forceActivationState(BODYSTATE_DISABLE_DEACTIVATION); break; case BODYTYPE_STATIC: body.forceActivationState(BODYSTATE_ACTIVE_TAG); this.syncEntityToBody(); break; } if (entity.collision.type === 'compound') { this.system._compounds.push(entity.collision); } body.activate(); this._simulationEnabled = true; } } } disableSimulation() { const body = this._body; if (body && this._simulationEnabled) { const system = this.system; let idx = system._compounds.indexOf(this.entity.collision); if (idx > -1) { system._compounds.splice(idx, 1); } idx = system._dynamic.indexOf(this); if (idx > -1) { system._dynamic.splice(idx, 1); } idx = system._kinematic.indexOf(this); if (idx > -1) { system._kinematic.splice(idx, 1); } system.removeBody(body); body.forceActivationState(BODYSTATE_DISABLE_SIMULATION); this._simulationEnabled = false; } } applyForce(x, y, z, px, py, pz) { const body = this._body; if (body) { body.activate(); if (x instanceof Vec3) { _ammoVec1.setValue(x.x, x.y, x.z); } else { _ammoVec1.setValue(x, y, z); } if (y instanceof Vec3) { _ammoVec2.setValue(y.x, y.y, y.z); } else if (px !== undefined) { _ammoVec2.setValue(px, py, pz); } else { _ammoVec2.setValue(0, 0, 0); } body.applyForce(_ammoVec1, _ammoVec2); } } applyTorque(x, y, z) { const body = this._body; if (body) { body.activate(); if (x instanceof Vec3) { _ammoVec1.setValue(x.x, x.y, x.z); } else { _ammoVec1.setValue(x, y, z); } body.applyTorque(_ammoVec1); } } applyImpulse(x, y, z, px, py, pz) { const body = this._body; if (body) { body.activate(); if (x instanceof Vec3) { _ammoVec1.setValue(x.x, x.y, x.z); } else { _ammoVec1.setValue(x, y, z); } if (y instanceof Vec3) { _ammoVec2.setValue(y.x, y.y, y.z); } else if (px !== undefined) { _ammoVec2.setValue(px, py, pz); } else { _ammoVec2.setValue(0, 0, 0); } body.applyImpulse(_ammoVec1, _ammoVec2); } } applyTorqueImpulse(x, y, z) { const body = this._body; if (body) { body.activate(); if (x instanceof Vec3) { _ammoVec1.setValue(x.x, x.y, x.z); } else { _ammoVec1.setValue(x, y, z); } body.applyTorqueImpulse(_ammoVec1); } } isStatic() { return this._type === BODYTYPE_STATIC; } isStaticOrKinematic() { return this._type === BODYTYPE_STATIC || this._type === BODYTYPE_KINEMATIC; } isKinematic() { return this._type === BODYTYPE_KINEMATIC; } _getEntityTransform(transform) { const entity = this.entity; const component = entity.collision; if (component) { const bodyPos = component.getShapePosition(); const bodyRot = component.getShapeRotation(); _ammoVec1.setValue(bodyPos.x, bodyPos.y, bodyPos.z); _ammoQuat.setValue(bodyRot.x, bodyRot.y, bodyRot.z, bodyRot.w); } else { const pos = entity.getPosition(); const rot = entity.getRotation(); _ammoVec1.setValue(pos.x, pos.y, pos.z); _ammoQuat.setValue(rot.x, rot.y, rot.z, rot.w); } transform.setOrigin(_ammoVec1); transform.setRotation(_ammoQuat); } syncEntityToBody() { const body = this._body; if (body) { this._getEntityTransform(_ammoTransform); body.setWorldTransform(_ammoTransform); if (this._type === BODYTYPE_KINEMATIC) { const motionState = body.getMotionState(); if (motionState) { motionState.setWorldTransform(_ammoTransform); } } body.activate(); } } _updateDynamic() { const body = this._body; if (body.isActive()) { const motionState = body.getMotionState(); if (motionState) { const entity = this.entity; motionState.getWorldTransform(_ammoTransform); const p = _ammoTransform.getOrigin(); const q = _ammoTransform.getRotation(); const component = entity.collision; if (component && component._hasOffset) { const lo = component.data.linearOffset; const ao = component.data.angularOffset; const invertedAo = _quat2.copy(ao).invert(); const entityRot = _quat1.set(q.x(), q.y(), q.z(), q.w()).mul(invertedAo); entityRot.transformVector(lo, _vec3); entity.setPosition(p.x() - _vec3.x, p.y() - _vec3.y, p.z() - _vec3.z); entity.setRotation(entityRot); } else { entity.setPosition(p.x(), p.y(), p.z()); entity.setRotation(q.x(), q.y(), q.z(), q.w()); } } } } _updateKinematic() { const motionState = this._body.getMotionState(); if (motionState) { this._getEntityTransform(_ammoTransform); motionState.setWorldTransform(_ammoTransform); } } teleport(x, y, z, rx, ry, rz) { if (x instanceof Vec3) { this.entity.setPosition(x); } else { this.entity.setPosition(x, y, z); } if (y instanceof Quat) { this.entity.setRotation(y); } else if (y instanceof Vec3) { this.entity.setEulerAngles(y); } else if (rx !== undefined) { this.entity.setEulerAngles(rx, ry, rz); } this.syncEntityToBody(); } onEnable() { if (!this._body) { this.createBody(); } this.enableSimulation(); } onDisable() { this.disableSimulation(); } constructor(...args){ super(...args), this._angularDamping = 0, this._angularFactor = new Vec3(1, 1, 1), this._angularVelocity = new Vec3(), this._body = null, this._friction = 0.5, this._group = BODYGROUP_STATIC, this._linearDamping = 0, this._linearFactor = new Vec3(1, 1, 1), this._linearVelocity = new Vec3(), this._mask = BODYMASK_NOT_STATIC, this._mass = 1, this._restitution = 0, this._rollingFriction = 0, this._simulationEnabled = false, this._type = BODYTYPE_STATIC; } } RigidBodyComponent.EVENT_CONTACT = 'contact'; RigidBodyComponent.EVENT_COLLISIONSTART = 'collisionstart'; RigidBodyComponent.EVENT_COLLISIONEND = 'collisionend'; RigidBodyComponent.EVENT_TRIGGERENTER = 'triggerenter'; RigidBodyComponent.EVENT_TRIGGERLEAVE = 'triggerleave'; RigidBodyComponent.order = -1; class RigidBodyComponentData { constructor(){ this.enabled = true; } } let ammoRayStart, ammoRayEnd; class RaycastResult { constructor(entity, point, normal, hitFraction){ this.entity = entity; this.point = point; this.normal = normal; this.hitFraction = hitFraction; } } class SingleContactResult { constructor(a, b, contactPoint){ if (arguments.length !== 0) { this.a = a; this.b = b; this.impulse = contactPoint.impulse; this.localPointA = contactPoint.localPoint; this.localPointB = contactPoint.localPointOther; this.pointA = contactPoint.point; this.pointB = contactPoint.pointOther; this.normal = contactPoint.normal; } else { this.a = null; this.b = null; this.impulse = 0; this.localPointA = new Vec3(); this.localPointB = new Vec3(); this.pointA = new Vec3(); this.pointB = new Vec3(); this.normal = new Vec3(); } } } class ContactPoint { constructor(localPoint = new Vec3(), localPointOther = new Vec3(), point = new Vec3(), pointOther = new Vec3(), normal = new Vec3(), impulse = 0){ this.localPoint = localPoint; this.localPointOther = localPointOther; this.point = point; this.pointOther = pointOther; this.normal = normal; this.impulse = impulse; } } class ContactResult { constructor(other, contacts){ this.other = other; this.contacts = contacts; } } const _schema$8 = [ 'enabled' ]; class RigidBodyComponentSystem extends ComponentSystem { onLibraryLoaded() { if (typeof Ammo !== 'undefined') { this.collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); this.dispatcher = new Ammo.btCollisionDispatcher(this.collisionConfiguration); this.overlappingPairCache = new Ammo.btDbvtBroadphase(); this.solver = new Ammo.btSequentialImpulseConstraintSolver(); this.dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(this.dispatcher, this.overlappingPairCache, this.solver, this.collisionConfiguration); if (this.dynamicsWorld.setInternalTickCallback) { const checkForCollisionsPointer = Ammo.addFunction(this._checkForCollisions.bind(this), 'vif'); this.dynamicsWorld.setInternalTickCallback(checkForCollisionsPointer); } ammoRayStart = new Ammo.btVector3(); ammoRayEnd = new Ammo.btVector3(); RigidBodyComponent.onLibraryLoaded(); this.contactPointPool = new ObjectPool(ContactPoint, 1); this.contactResultPool = new ObjectPool(ContactResult, 1); this.singleContactResultPool = new ObjectPool(SingleContactResult, 1); this.app.systems.on('update', this.onUpdate, this); } else { this.app.systems.off('update', this.onUpdate, this); } } initializeComponentData(component, data, properties) { const props = [ 'mass', 'linearDamping', 'angularDamping', 'linearFactor', 'angularFactor', 'friction', 'rollingFriction', 'restitution', 'type', 'group', 'mask' ]; for (const property of props){ if (data.hasOwnProperty(property)) { const value = data[property]; if (Array.isArray(value)) { component[property] = new Vec3(value[0], value[1], value[2]); } else { component[property] = value; } } } super.initializeComponentData(component, data, [ 'enabled' ]); } cloneComponent(entity, clone) { const rigidbody = entity.rigidbody; const data = { enabled: rigidbody.enabled, mass: rigidbody.mass, linearDamping: rigidbody.linearDamping, angularDamping: rigidbody.angularDamping, linearFactor: [ rigidbody.linearFactor.x, rigidbody.linearFactor.y, rigidbody.linearFactor.z ], angularFactor: [ rigidbody.angularFactor.x, rigidbody.angularFactor.y, rigidbody.angularFactor.z ], friction: rigidbody.friction, rollingFriction: rigidbody.rollingFriction, restitution: rigidbody.restitution, type: rigidbody.type, group: rigidbody.group, mask: rigidbody.mask }; return this.addComponent(clone, data); } onBeforeRemove(entity, component) { if (component.enabled) { component.enabled = false; } if (component.body) { this.destroyBody(component.body); component.body = null; } } addBody(body, group, mask) { if (group !== undefined && mask !== undefined) { this.dynamicsWorld.addRigidBody(body, group, mask); } else { this.dynamicsWorld.addRigidBody(body); } } removeBody(body) { this.dynamicsWorld.removeRigidBody(body); } createBody(mass, shape, transform) { const localInertia = new Ammo.btVector3(0, 0, 0); if (mass !== 0) { shape.calculateLocalInertia(mass, localInertia); } const motionState = new Ammo.btDefaultMotionState(transform); const bodyInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia); const body = new Ammo.btRigidBody(bodyInfo); Ammo.destroy(bodyInfo); Ammo.destroy(localInertia); return body; } destroyBody(body) { const motionState = body.getMotionState(); if (motionState) { Ammo.destroy(motionState); } Ammo.destroy(body); } raycastFirst(start, end, options = {}) { if (options.filterTags || options.filterCallback) { options.sort = true; return this.raycastAll(start, end, options)[0] || null; } let result = null; ammoRayStart.setValue(start.x, start.y, start.z); ammoRayEnd.setValue(end.x, end.y, end.z); const rayCallback = new Ammo.ClosestRayResultCallback(ammoRayStart, ammoRayEnd); if (typeof options.filterCollisionGroup === 'number') { rayCallback.set_m_collisionFilterGroup(options.filterCollisionGroup); } if (typeof options.filterCollisionMask === 'number') { rayCallback.set_m_collisionFilterMask(options.filterCollisionMask); } this.dynamicsWorld.rayTest(ammoRayStart, ammoRayEnd, rayCallback); if (rayCallback.hasHit()) { const collisionObj = rayCallback.get_m_collisionObject(); const body = Ammo.castObject(collisionObj, Ammo.btRigidBody); if (body) { const point = rayCallback.get_m_hitPointWorld(); const normal = rayCallback.get_m_hitNormalWorld(); result = new RaycastResult(body.entity, new Vec3(point.x(), point.y(), point.z()), new Vec3(normal.x(), normal.y(), normal.z()), rayCallback.get_m_closestHitFraction()); } } Ammo.destroy(rayCallback); return result; } raycastAll(start, end, options = {}) { const results = []; ammoRayStart.setValue(start.x, start.y, start.z); ammoRayEnd.setValue(end.x, end.y, end.z); const rayCallback = new Ammo.AllHitsRayResultCallback(ammoRayStart, ammoRayEnd); if (typeof options.filterCollisionGroup === 'number') { rayCallback.set_m_collisionFilterGroup(options.filterCollisionGroup); } if (typeof options.filterCollisionMask === 'number') { rayCallback.set_m_collisionFilterMask(options.filterCollisionMask); } this.dynamicsWorld.rayTest(ammoRayStart, ammoRayEnd, rayCallback); if (rayCallback.hasHit()) { const collisionObjs = rayCallback.get_m_collisionObjects(); const points = rayCallback.get_m_hitPointWorld(); const normals = rayCallback.get_m_hitNormalWorld(); const hitFractions = rayCallback.get_m_hitFractions(); const numHits = collisionObjs.size(); for(let i = 0; i < numHits; i++){ const body = Ammo.castObject(collisionObjs.at(i), Ammo.btRigidBody); if (body && body.entity) { if (options.filterTags && !body.entity.tags.has(...options.filterTags) || options.filterCallback && !options.filterCallback(body.entity)) { continue; } const point = points.at(i); const normal = normals.at(i); const result = new RaycastResult(body.entity, new Vec3(point.x(), point.y(), point.z()), new Vec3(normal.x(), normal.y(), normal.z()), hitFractions.at(i)); results.push(result); } } if (options.sort) { results.sort((a, b)=>a.hitFraction - b.hitFraction); } } Ammo.destroy(rayCallback); return results; } _storeCollision(entity, other) { let isNewCollision = false; const guid = entity.getGuid(); this.collisions[guid] = this.collisions[guid] || { others: [], entity: entity }; if (this.collisions[guid].others.indexOf(other) < 0) { this.collisions[guid].others.push(other); isNewCollision = true; } this.frameCollisions[guid] = this.frameCollisions[guid] || { others: [], entity: entity }; this.frameCollisions[guid].others.push(other); return isNewCollision; } _createContactPointFromAmmo(contactPoint) { const localPointA = contactPoint.get_m_localPointA(); const localPointB = contactPoint.get_m_localPointB(); const positionWorldOnA = contactPoint.getPositionWorldOnA(); const positionWorldOnB = contactPoint.getPositionWorldOnB(); const normalWorldOnB = contactPoint.get_m_normalWorldOnB(); const contact = this.contactPointPool.allocate(); contact.localPoint.set(localPointA.x(), localPointA.y(), localPointA.z()); contact.localPointOther.set(localPointB.x(), localPointB.y(), localPointB.z()); contact.point.set(positionWorldOnA.x(), positionWorldOnA.y(), positionWorldOnA.z()); contact.pointOther.set(positionWorldOnB.x(), positionWorldOnB.y(), positionWorldOnB.z()); contact.normal.set(normalWorldOnB.x(), normalWorldOnB.y(), normalWorldOnB.z()); contact.impulse = contactPoint.getAppliedImpulse(); return contact; } _createReverseContactPointFromAmmo(contactPoint) { const localPointA = contactPoint.get_m_localPointA(); const localPointB = contactPoint.get_m_localPointB(); const positionWorldOnA = contactPoint.getPositionWorldOnA(); const positionWorldOnB = contactPoint.getPositionWorldOnB(); const normalWorldOnB = contactPoint.get_m_normalWorldOnB(); const contact = this.contactPointPool.allocate(); contact.localPointOther.set(localPointA.x(), localPointA.y(), localPointA.z()); contact.localPoint.set(localPointB.x(), localPointB.y(), localPointB.z()); contact.pointOther.set(positionWorldOnA.x(), positionWorldOnA.y(), positionWorldOnA.z()); contact.point.set(positionWorldOnB.x(), positionWorldOnB.y(), positionWorldOnB.z()); contact.normal.set(normalWorldOnB.x(), normalWorldOnB.y(), normalWorldOnB.z()); contact.impulse = contactPoint.getAppliedImpulse(); return contact; } _createSingleContactResult(a, b, contactPoint) { const result = this.singleContactResultPool.allocate(); result.a = a; result.b = b; result.localPointA = contactPoint.localPoint; result.localPointB = contactPoint.localPointOther; result.pointA = contactPoint.point; result.pointB = contactPoint.pointOther; result.normal = contactPoint.normal; result.impulse = contactPoint.impulse; return result; } _createContactResult(other, contacts) { const result = this.contactResultPool.allocate(); result.other = other; result.contacts = contacts; return result; } _cleanOldCollisions() { for(const guid in this.collisions){ if (this.collisions.hasOwnProperty(guid)) { const frameCollision = this.frameCollisions[guid]; const collision = this.collisions[guid]; const entity = collision.entity; const entityCollision = entity.collision; const entityRigidbody = entity.rigidbody; const others = collision.others; const length = others.length; let i = length; while(i--){ const other = others[i]; if (!frameCollision || frameCollision.others.indexOf(other) < 0) { others.splice(i, 1); if (entity.trigger) { if (entityCollision) { entityCollision.fire('triggerleave', other); } if (other.rigidbody) { other.rigidbody.fire('triggerleave', entity); } } else if (!other.trigger) { if (entityRigidbody) { entityRigidbody.fire('collisionend', other); } if (entityCollision) { entityCollision.fire('collisionend', other); } } } } if (others.length === 0) { delete this.collisions[guid]; } } } } _hasContactEvent(entity) { const c = entity.collision; if (c && (c.hasEvent('collisionstart') || c.hasEvent('collisionend') || c.hasEvent('contact'))) { return true; } const r = entity.rigidbody; return r && (r.hasEvent('collisionstart') || r.hasEvent('collisionend') || r.hasEvent('contact')); } _checkForCollisions(world, timeStep) { const dynamicsWorld = Ammo.wrapPointer(world, Ammo.btDynamicsWorld); const dispatcher = dynamicsWorld.getDispatcher(); const numManifolds = dispatcher.getNumManifolds(); this.frameCollisions = {}; for(let i = 0; i < numManifolds; i++){ const manifold = dispatcher.getManifoldByIndexInternal(i); const body0 = manifold.getBody0(); const body1 = manifold.getBody1(); const wb0 = Ammo.castObject(body0, Ammo.btRigidBody); const wb1 = Ammo.castObject(body1, Ammo.btRigidBody); const e0 = wb0.entity; const e1 = wb1.entity; if (!e0 || !e1) { continue; } const flags0 = wb0.getCollisionFlags(); const flags1 = wb1.getCollisionFlags(); const numContacts = manifold.getNumContacts(); const forwardContacts = []; const reverseContacts = []; let newCollision; if (numContacts > 0) { if (flags0 & BODYFLAG_NORESPONSE_OBJECT || flags1 & BODYFLAG_NORESPONSE_OBJECT) { const e0Events = e0.collision && (e0.collision.hasEvent('triggerenter') || e0.collision.hasEvent('triggerleave')); const e1Events = e1.collision && (e1.collision.hasEvent('triggerenter') || e1.collision.hasEvent('triggerleave')); const e0BodyEvents = e0.rigidbody && (e0.rigidbody.hasEvent('triggerenter') || e0.rigidbody.hasEvent('triggerleave')); const e1BodyEvents = e1.rigidbody && (e1.rigidbody.hasEvent('triggerenter') || e1.rigidbody.hasEvent('triggerleave')); if (e0Events) { newCollision = this._storeCollision(e0, e1); if (newCollision && !(flags1 & BODYFLAG_NORESPONSE_OBJECT)) { e0.collision.fire('triggerenter', e1); } } if (e1Events) { newCollision = this._storeCollision(e1, e0); if (newCollision && !(flags0 & BODYFLAG_NORESPONSE_OBJECT)) { e1.collision.fire('triggerenter', e0); } } if (e0BodyEvents) { if (!newCollision) { newCollision = this._storeCollision(e1, e0); } if (newCollision) { e0.rigidbody.fire('triggerenter', e1); } } if (e1BodyEvents) { if (!newCollision) { newCollision = this._storeCollision(e0, e1); } if (newCollision) { e1.rigidbody.fire('triggerenter', e0); } } } else { const e0Events = this._hasContactEvent(e0); const e1Events = this._hasContactEvent(e1); const globalEvents = this.hasEvent('contact'); if (globalEvents || e0Events || e1Events) { for(let j = 0; j < numContacts; j++){ const btContactPoint = manifold.getContactPoint(j); const contactPoint = this._createContactPointFromAmmo(btContactPoint); if (e0Events || e1Events) { forwardContacts.push(contactPoint); const reverseContactPoint = this._createReverseContactPointFromAmmo(btContactPoint); reverseContacts.push(reverseContactPoint); } if (globalEvents) { const result = this._createSingleContactResult(e0, e1, contactPoint); this.fire('contact', result); } } if (e0Events) { const forwardResult = this._createContactResult(e1, forwardContacts); newCollision = this._storeCollision(e0, e1); if (e0.collision) { e0.collision.fire('contact', forwardResult); if (newCollision) { e0.collision.fire('collisionstart', forwardResult); } } if (e0.rigidbody) { e0.rigidbody.fire('contact', forwardResult); if (newCollision) { e0.rigidbody.fire('collisionstart', forwardResult); } } } if (e1Events) { const reverseResult = this._createContactResult(e0, reverseContacts); newCollision = this._storeCollision(e1, e0); if (e1.collision) { e1.collision.fire('contact', reverseResult); if (newCollision) { e1.collision.fire('collisionstart', reverseResult); } } if (e1.rigidbody) { e1.rigidbody.fire('contact', reverseResult); if (newCollision) { e1.rigidbody.fire('collisionstart', reverseResult); } } } } } } } this._cleanOldCollisions(); this.contactPointPool.freeAll(); this.contactResultPool.freeAll(); this.singleContactResultPool.freeAll(); } onUpdate(dt) { let i, len; this._stats.physicsStart = now(); this._gravityFloat32[0] = this.gravity.x; this._gravityFloat32[1] = this.gravity.y; this._gravityFloat32[2] = this.gravity.z; const gravity = this.dynamicsWorld.getGravity(); if (gravity.x() !== this._gravityFloat32[0] || gravity.y() !== this._gravityFloat32[1] || gravity.z() !== this._gravityFloat32[2]) { gravity.setValue(this.gravity.x, this.gravity.y, this.gravity.z); this.dynamicsWorld.setGravity(gravity); } const triggers = this._triggers; for(i = 0, len = triggers.length; i < len; i++){ triggers[i].updateTransform(); } const compounds = this._compounds; for(i = 0, len = compounds.length; i < len; i++){ compounds[i]._updateCompound(); } const kinematic = this._kinematic; for(i = 0, len = kinematic.length; i < len; i++){ kinematic[i]._updateKinematic(); } this.dynamicsWorld.stepSimulation(dt, this.maxSubSteps, this.fixedTimeStep); const dynamic = this._dynamic; for(i = 0, len = dynamic.length; i < len; i++){ dynamic[i]._updateDynamic(); } if (!this.dynamicsWorld.setInternalTickCallback) { this._checkForCollisions(Ammo.getPointer(this.dynamicsWorld), dt); } this._stats.physicsTime = now() - this._stats.physicsStart; } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); if (typeof Ammo !== 'undefined') { Ammo.destroy(this.dynamicsWorld); Ammo.destroy(this.solver); Ammo.destroy(this.overlappingPairCache); Ammo.destroy(this.dispatcher); Ammo.destroy(this.collisionConfiguration); Ammo.destroy(ammoRayStart); Ammo.destroy(ammoRayEnd); this.dynamicsWorld = null; this.solver = null; this.overlappingPairCache = null; this.dispatcher = null; this.collisionConfiguration = null; ammoRayStart = null; ammoRayEnd = null; RigidBodyComponent.onAppDestroy(); } } constructor(app){ super(app), this.maxSubSteps = 10, this.fixedTimeStep = 1 / 60, this.gravity = new Vec3(0, -9.81, 0), this._gravityFloat32 = new Float32Array(3), this._dynamic = [], this._kinematic = [], this._triggers = [], this._compounds = []; this.id = 'rigidbody'; this._stats = app.stats.frame; this.ComponentType = RigidBodyComponent; this.DataType = RigidBodyComponentData; this.contactPointPool = null; this.contactResultPool = null; this.singleContactResultPool = null; this.schema = _schema$8; this.collisions = {}; this.frameCollisions = {}; this.on('beforeremove', this.onBeforeRemove, this); } } RigidBodyComponentSystem.EVENT_CONTACT = 'contact'; Component._buildAccessors(RigidBodyComponent.prototype, _schema$8); const SCALEMODE_NONE = 'none'; const SCALEMODE_BLEND = 'blend'; const _transform = new Mat4(); class ScreenComponent extends Component { syncDrawOrder() { this.system.queueDrawOrderSync(this.entity.getGuid(), this._processDrawOrderSync, this); } _recurseDrawOrderSync(e, i) { if (!(e instanceof Entity)) { return i; } if (e.element) { const prevDrawOrder = e.element.drawOrder; e.element.drawOrder = i++; if (e.element._batchGroupId >= 0 && prevDrawOrder !== e.element.drawOrder) { this.system.app.batcher?.markGroupDirty(e.element._batchGroupId); } } if (e.particlesystem) { e.particlesystem.drawOrder = i++; } const children = e.children; for(let j = 0; j < children.length; j++){ i = this._recurseDrawOrderSync(children[j], i); } return i; } _processDrawOrderSync() { const i = 1; this._recurseDrawOrderSync(this.entity, i); this.fire('syncdraworder'); } _calcProjectionMatrix() { const w = this._resolution.x / this.scale; const h = this._resolution.y / this.scale; const left = 0; const right = w; const bottom = -h; const top = 0; const near = 1; const far = -1; this._screenMatrix.setOrtho(left, right, bottom, top, near, far); if (!this._screenSpace) { _transform.setScale(0.5 * w, 0.5 * h, 1); this._screenMatrix.mul2(_transform, this._screenMatrix); } } _updateScale() { this.scale = this._calcScale(this._resolution, this.referenceResolution); } _calcScale(resolution, referenceResolution) { const lx = Math.log2((resolution.x || 1) / referenceResolution.x); const ly = Math.log2((resolution.y || 1) / referenceResolution.y); return Math.pow(2, lx * (1 - this._scaleBlend) + ly * this._scaleBlend); } _onResize(width, height) { if (this._screenSpace) { this._resolution.set(width, height); this.resolution = this._resolution; } } _bindElement(element) { this._elements.add(element); } _unbindElement(element) { this._elements.delete(element); } onRemove() { this.system.app.graphicsDevice.off('resizecanvas', this._onResize, this); this.fire('remove'); this._elements.forEach((element)=>element._onScreenRemove()); this._elements.clear(); this.off(); } set resolution(value) { if (!this._screenSpace) { this._resolution.set(value.x, value.y); } else { this._resolution.set(this.system.app.graphicsDevice.width, this.system.app.graphicsDevice.height); } this._updateScale(); this._calcProjectionMatrix(); if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:resolution', this._resolution); this._elements.forEach((element)=>element._onScreenResize(this._resolution)); } get resolution() { return this._resolution; } set referenceResolution(value) { this._referenceResolution.set(value.x, value.y); this._updateScale(); this._calcProjectionMatrix(); if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:referenceresolution', this._resolution); this._elements.forEach((element)=>element._onScreenResize(this._resolution)); } get referenceResolution() { if (this._scaleMode === SCALEMODE_NONE) { return this._resolution; } return this._referenceResolution; } set screenSpace(value) { this._screenSpace = value; if (this._screenSpace) { this._resolution.set(this.system.app.graphicsDevice.width, this.system.app.graphicsDevice.height); } this.resolution = this._resolution; if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:screenspace', this._screenSpace); this._elements.forEach((element)=>element._onScreenSpaceChange()); } get screenSpace() { return this._screenSpace; } set scaleMode(value) { if (value !== SCALEMODE_NONE && value !== SCALEMODE_BLEND) { value = SCALEMODE_NONE; } if (!this._screenSpace && value !== SCALEMODE_NONE) { value = SCALEMODE_NONE; } this._scaleMode = value; this.resolution = this._resolution; this.fire('set:scalemode', this._scaleMode); } get scaleMode() { return this._scaleMode; } set scaleBlend(value) { this._scaleBlend = value; this._updateScale(); this._calcProjectionMatrix(); if (!this.entity._dirtyLocal) { this.entity._dirtifyLocal(); } this.fire('set:scaleblend', this._scaleBlend); this._elements.forEach((element)=>element._onScreenResize(this._resolution)); } get scaleBlend() { return this._scaleBlend; } set priority(value) { value = math.clamp(value, 0, 0x7F); if (this._priority === value) { return; } this._priority = value; this.syncDrawOrder(); } get priority() { return this._priority; } constructor(system, entity){ super(system, entity); this._resolution = new Vec2(640, 320); this._referenceResolution = new Vec2(640, 320); this._scaleMode = SCALEMODE_NONE; this.scale = 1; this._scaleBlend = 0.5; this._priority = 0; this._screenSpace = false; this.cull = this._screenSpace; this._screenMatrix = new Mat4(); this._elements = new Set(); system.app.graphicsDevice.on('resizecanvas', this._onResize, this); } } class ScreenComponentData { constructor(){ this.enabled = true; } } const _schema$7 = [ 'enabled' ]; class ScreenComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { if (data.priority !== undefined) component.priority = data.priority; if (data.screenSpace !== undefined) component.screenSpace = data.screenSpace; component.cull = component.screenSpace; if (data.scaleMode !== undefined) component.scaleMode = data.scaleMode; if (data.scaleBlend !== undefined) component.scaleBlend = data.scaleBlend; if (data.resolution !== undefined) { if (data.resolution instanceof Vec2) { component._resolution.copy(data.resolution); } else { component._resolution.set(data.resolution[0], data.resolution[1]); } component.resolution = component._resolution; } if (data.referenceResolution !== undefined) { if (data.referenceResolution instanceof Vec2) { component._referenceResolution.copy(data.referenceResolution); } else { component._referenceResolution.set(data.referenceResolution[0], data.referenceResolution[1]); } component.referenceResolution = component._referenceResolution; } this._updateDescendantElements(component.entity, component.entity); component.syncDrawOrder(); super.initializeComponentData(component, data, _schema$7); } _updateDescendantElements(entity, screenEntity) { const children = entity.children; for(let i = 0; i < children.length; i++){ const child = children[i]; if (child.element && !child.element.screen) { child.element._updateScreen(screenEntity); } if (!child.screen) { this._updateDescendantElements(child, screenEntity); } } } destroy() { super.destroy(); this.app.graphicsDevice.off('resizecanvas', this._onResize, this); this.app.systems.off('update', this._onUpdate, this); } _onUpdate(dt) { const components = this.store; for(const id in components){ if (components[id].entity.screen.update) components[id].entity.screen.update(dt); } } _onResize(width, height) { this.windowResolution.x = width; this.windowResolution.y = height; } cloneComponent(entity, clone) { const screen = entity.screen; return this.addComponent(clone, { enabled: screen.enabled, screenSpace: screen.screenSpace, scaleMode: screen.scaleMode, scaleBlend: screen.scaleBlend, priority: screen.priority, resolution: screen.resolution.clone(), referenceResolution: screen.referenceResolution.clone() }); } onRemoveComponent(entity, component) { component.onRemove(); } processDrawOrderSyncQueue() { const list = this._drawOrderSyncQueue.list(); for(let i = 0; i < list.length; i++){ const item = list[i]; item.callback.call(item.scope); } this._drawOrderSyncQueue.clear(); } queueDrawOrderSync(id, fn, scope) { if (!this._drawOrderSyncQueue.list().length) { this.app.once('prerender', this.processDrawOrderSyncQueue, this); } if (!this._drawOrderSyncQueue.has(id)) { this._drawOrderSyncQueue.push(id, { callback: fn, scope: scope }); } } constructor(app){ super(app); this.id = 'screen'; this.ComponentType = ScreenComponent; this.DataType = ScreenComponentData; this.schema = _schema$7; this.windowResolution = new Vec2(); this._drawOrderSyncQueue = new IndexedList(); this.app.graphicsDevice.on('resizecanvas', this._onResize, this); this.app.systems.on('update', this._onUpdate, this); this.on('beforeremove', this.onRemoveComponent, this); } } Component._buildAccessors(ScreenComponent.prototype, _schema$7); const _inputScreenPosition = new Vec2(); const _inputWorldPosition = new Vec3(); const _ray = new Ray(); const _plane = new Plane(); const _normal = new Vec3(); const _point = new Vec3(); const _entityRotation = new Quat(); const OPPOSITE_AXIS = { x: 'y', y: 'x' }; class ElementDragHelper extends EventHandler { _toggleLifecycleListeners(onOrOff) { this._element[onOrOff]('mousedown', this._onMouseDownOrTouchStart, this); this._element[onOrOff]('touchstart', this._onMouseDownOrTouchStart, this); this._element[onOrOff]('selectstart', this._onMouseDownOrTouchStart, this); } _toggleDragListeners(onOrOff) { const isOn = onOrOff === 'on'; if (this._hasDragListeners && isOn) { return; } if (this._app.mouse) { this._element[onOrOff]('mousemove', this._onMove, this); this._element[onOrOff]('mouseup', this._onMouseUpOrTouchEnd, this); } if (platform.touch) { this._element[onOrOff]('touchmove', this._onMove, this); this._element[onOrOff]('touchend', this._onMouseUpOrTouchEnd, this); this._element[onOrOff]('touchcancel', this._onMouseUpOrTouchEnd, this); } this._element[onOrOff]('selectmove', this._onMove, this); this._element[onOrOff]('selectend', this._onMouseUpOrTouchEnd, this); this._hasDragListeners = isOn; } _onMouseDownOrTouchStart(event) { if (this._element && !this._isDragging && this.enabled) { this._dragCamera = event.camera; this._calculateDragScale(); const currentMousePosition = this._screenToLocal(event); if (currentMousePosition) { this._toggleDragListeners('on'); this._isDragging = true; this._dragStartMousePosition.copy(currentMousePosition); this._dragStartHandlePosition.copy(this._element.entity.getLocalPosition()); this.fire('drag:start'); } } } _onMouseUpOrTouchEnd() { if (this._isDragging) { this._isDragging = false; this._toggleDragListeners('off'); this.fire('drag:end'); } } _screenToLocal(event) { if (event.inputSource) { _ray.set(event.inputSource.getOrigin(), event.inputSource.getDirection()); } else { this._determineInputPosition(event); this._chooseRayOriginAndDirection(); } _normal.copy(this._element.entity.forward).mulScalar(-1); _plane.setFromPointNormal(this._element.entity.getPosition(), _normal); if (_plane.intersectsRay(_ray, _point)) { _entityRotation.copy(this._element.entity.getRotation()).invert().transformVector(_point, _point); _point.mul(this._dragScale); return _point; } return null; } _determineInputPosition(event) { const devicePixelRatio = this._app.graphicsDevice.maxPixelRatio; if (typeof event.x !== 'undefined' && typeof event.y !== 'undefined') { _inputScreenPosition.x = event.x * devicePixelRatio; _inputScreenPosition.y = event.y * devicePixelRatio; } else if (event.changedTouches) { _inputScreenPosition.x = event.changedTouches[0].x * devicePixelRatio; _inputScreenPosition.y = event.changedTouches[0].y * devicePixelRatio; } else { console.warn('Could not determine position from input event'); } } _chooseRayOriginAndDirection() { if (this._element.screen && this._element.screen.screen.screenSpace) { _ray.origin.set(_inputScreenPosition.x, -_inputScreenPosition.y, 0); _ray.direction.copy(Vec3.FORWARD); } else { _inputWorldPosition.copy(this._dragCamera.screenToWorld(_inputScreenPosition.x, _inputScreenPosition.y, 1)); _ray.origin.copy(this._dragCamera.entity.getPosition()); _ray.direction.copy(_inputWorldPosition).sub(_ray.origin).normalize(); } } _calculateDragScale() { let current = this._element.entity.parent; const screen = this._element.screen && this._element.screen.screen; const isWithin2DScreen = screen && screen.screenSpace; const screenScale = isWithin2DScreen ? screen.scale : 1; const dragScale = this._dragScale; dragScale.set(screenScale, screenScale, screenScale); while(current){ dragScale.mul(current.getLocalScale()); current = current.parent; if (isWithin2DScreen && current.screen) { break; } } dragScale.x = 1 / dragScale.x; dragScale.y = 1 / dragScale.y; dragScale.z = 0; } _onMove(event) { const { _element: element, _deltaMousePosition: deltaMousePosition, _deltaHandlePosition: deltaHandlePosition, _axis: axis } = this; if (element && this._isDragging && this.enabled && element.enabled && element.entity.enabled) { const currentMousePosition = this._screenToLocal(event); if (currentMousePosition) { deltaMousePosition.sub2(currentMousePosition, this._dragStartMousePosition); deltaHandlePosition.add2(this._dragStartHandlePosition, deltaMousePosition); if (axis) { const currentPosition = element.entity.getLocalPosition(); const constrainedAxis = OPPOSITE_AXIS[axis]; deltaHandlePosition[constrainedAxis] = currentPosition[constrainedAxis]; } element.entity.setLocalPosition(deltaHandlePosition); this.fire('drag:move', deltaHandlePosition); } } } destroy() { this._toggleLifecycleListeners('off'); this._toggleDragListeners('off'); } set enabled(value) { this._enabled = value; } get enabled() { return this._enabled; } get isDragging() { return this._isDragging; } constructor(element, axis){ super(); if (!element || !(element instanceof ElementComponent)) { throw new Error('Element was null or not an ElementComponent'); } if (axis && axis !== 'x' && axis !== 'y') { throw new Error(`Unrecognized axis: ${axis}`); } this._element = element; this._app = element.system.app; this._axis = axis || null; this._enabled = true; this._dragScale = new Vec3(); this._dragStartMousePosition = new Vec3(); this._dragStartHandlePosition = new Vec3(); this._deltaMousePosition = new Vec3(); this._deltaHandlePosition = new Vec3(); this._isDragging = false; this._toggleLifecycleListeners('on'); } } ElementDragHelper.EVENT_DRAGSTART = 'drag:start'; ElementDragHelper.EVENT_DRAGEND = 'drag:end'; ElementDragHelper.EVENT_DRAGMOVE = 'drag:move'; const SCROLL_MODE_CLAMP = 0; const SCROLL_MODE_BOUNCE = 1; const SCROLL_MODE_INFINITE = 2; const SCROLLBAR_VISIBILITY_SHOW_ALWAYS = 0; const SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED = 1; const _tempScrollValue = new Vec2(); class ScrollViewComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) { this._setValue('enabled', arg); } get enabled() { return this.data.enabled; } set horizontal(arg) { this._setValue('horizontal', arg); } get horizontal() { return this.data.horizontal; } set vertical(arg) { this._setValue('vertical', arg); } get vertical() { return this.data.vertical; } set scrollMode(arg) { this._setValue('scrollMode', arg); } get scrollMode() { return this.data.scrollMode; } set bounceAmount(arg) { this._setValue('bounceAmount', arg); } get bounceAmount() { return this.data.bounceAmount; } set friction(arg) { this._setValue('friction', arg); } get friction() { return this.data.friction; } set dragThreshold(arg) { this._setValue('dragThreshold', arg); } get dragThreshold() { return this.data.dragThreshold; } set useMouseWheel(arg) { this._setValue('useMouseWheel', arg); } get useMouseWheel() { return this.data.useMouseWheel; } set mouseWheelSensitivity(arg) { this._setValue('mouseWheelSensitivity', arg); } get mouseWheelSensitivity() { return this.data.mouseWheelSensitivity; } set horizontalScrollbarVisibility(arg) { this._setValue('horizontalScrollbarVisibility', arg); } get horizontalScrollbarVisibility() { return this.data.horizontalScrollbarVisibility; } set verticalScrollbarVisibility(arg) { this._setValue('verticalScrollbarVisibility', arg); } get verticalScrollbarVisibility() { return this.data.verticalScrollbarVisibility; } set viewportEntity(arg) { if (this._viewportEntity === arg) { return; } const isString = typeof arg === 'string'; if (this._viewportEntity && isString && this._viewportEntity.getGuid() === arg) { return; } if (this._viewportEntity) { this._viewportEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._viewportEntity = arg; } else if (isString) { this._viewportEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._viewportEntity = null; } if (this._viewportEntity) { this._viewportEntitySubscribe(); } if (this._viewportEntity) { this.data.viewportEntity = this._viewportEntity.getGuid(); } else if (isString && arg) { this.data.viewportEntity = arg; } } get viewportEntity() { return this._viewportEntity; } set contentEntity(arg) { if (this._contentEntity === arg) { return; } const isString = typeof arg === 'string'; if (this._contentEntity && isString && this._contentEntity.getGuid() === arg) { return; } if (this._contentEntity) { this._contentEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._contentEntity = arg; } else if (isString) { this._contentEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._contentEntity = null; } if (this._contentEntity) { this._contentEntitySubscribe(); } if (this._contentEntity) { this.data.contentEntity = this._contentEntity.getGuid(); } else if (isString && arg) { this.data.contentEntity = arg; } } get contentEntity() { return this._contentEntity; } set horizontalScrollbarEntity(arg) { if (this._horizontalScrollbarEntity === arg) { return; } const isString = typeof arg === 'string'; if (this._horizontalScrollbarEntity && isString && this._horizontalScrollbarEntity.getGuid() === arg) { return; } if (this._horizontalScrollbarEntity) { this._horizontalScrollbarEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._horizontalScrollbarEntity = arg; } else if (isString) { this._horizontalScrollbarEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._horizontalScrollbarEntity = null; } this._scrollbarEntities[ORIENTATION_HORIZONTAL] = this._horizontalScrollbarEntity; if (this._horizontalScrollbarEntity) { this._horizontalScrollbarEntitySubscribe(); } if (this._horizontalScrollbarEntity) { this.data.horizontalScrollbarEntity = this._horizontalScrollbarEntity.getGuid(); } else if (isString && arg) { this.data.horizontalScrollbarEntity = arg; } } get horizontalScrollbarEntity() { return this._horizontalScrollbarEntity; } set verticalScrollbarEntity(arg) { if (this._verticalScrollbarEntity === arg) { return; } const isString = typeof arg === 'string'; if (this._verticalScrollbarEntity && isString && this._verticalScrollbarEntity.getGuid() === arg) { return; } if (this._verticalScrollbarEntity) { this._verticalScrollbarEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._verticalScrollbarEntity = arg; } else if (isString) { this._verticalScrollbarEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._verticalScrollbarEntity = null; } this._scrollbarEntities[ORIENTATION_VERTICAL] = this._verticalScrollbarEntity; if (this._verticalScrollbarEntity) { this._verticalScrollbarEntitySubscribe(); } if (this._verticalScrollbarEntity) { this.data.verticalScrollbarEntity = this._verticalScrollbarEntity.getGuid(); } else if (isString && arg) { this.data.verticalScrollbarEntity = arg; } } get verticalScrollbarEntity() { return this._verticalScrollbarEntity; } set scroll(value) { this._onSetScroll(value.x, value.y); } get scroll() { return this._scroll; } _setValue(name, value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); } _toggleLifecycleListeners(onOrOff) { this[onOrOff]('set_horizontal', this._onSetHorizontalScrollingEnabled, this); this[onOrOff]('set_vertical', this._onSetVerticalScrollingEnabled, this); this.entity[onOrOff]('element:add', this._onElementComponentAdd, this); } _toggleElementListeners(onOrOff) { if (this.entity.element) { if (onOrOff === 'on' && this._hasElementListeners) { return; } this.entity.element[onOrOff]('resize', this._syncAll, this); this.entity.element[onOrOff]('mousewheel', this._onMouseWheel, this); this._hasElementListeners = onOrOff === 'on'; } } _onElementComponentAdd(entity) { this._evtElementRemove = this.entity.element.once('beforeremove', this._onElementComponentRemove, this); this._toggleElementListeners('on'); } _onElementComponentRemove(entity) { this._evtElementRemove?.off(); this._evtElementRemove = null; this._toggleElementListeners('off'); } _viewportEntitySubscribe() { this._evtViewportEntityElementAdd = this._viewportEntity.on('element:add', this._onViewportElementGain, this); if (this._viewportEntity.element) { this._onViewportElementGain(); } } _viewportEntityUnsubscribe() { this._evtViewportEntityElementAdd?.off(); this._evtViewportEntityElementAdd = null; if (this._viewportEntity?.element) { this._onViewportElementLose(); } } _viewportEntityElementSubscribe() { const element = this._viewportEntity.element; this._evtViewportElementRemove = element.once('beforeremove', this._onViewportElementLose, this); this._evtViewportResize = element.on('resize', this._syncAll, this); } _viewportEntityElementUnsubscribe() { this._evtViewportElementRemove?.off(); this._evtViewportElementRemove = null; this._evtViewportResize?.off(); this._evtViewportResize = null; } _onViewportElementGain() { this._viewportEntityElementSubscribe(); this._syncAll(); } _onViewportElementLose() { this._viewportEntityElementUnsubscribe(); } _contentEntitySubscribe() { this._evtContentEntityElementAdd = this._contentEntity.on('element:add', this._onContentElementGain, this); if (this._contentEntity.element) { this._onContentElementGain(); } } _contentEntityUnsubscribe() { this._evtContentEntityElementAdd?.off(); this._evtContentEntityElementAdd = null; if (this._contentEntity?.element) { this._onContentElementLose(); } } _contentEntityElementSubscribe() { const element = this._contentEntity.element; this._evtContentElementRemove = element.once('beforeremove', this._onContentElementLose, this); this._evtContentResize = element.on('resize', this._syncAll, this); } _contentEntityElementUnsubscribe() { this._evtContentElementRemove?.off(); this._evtContentElementRemove = null; this._evtContentResize?.off(); this._evtContentResize = null; } _onContentElementGain() { this._contentEntityElementSubscribe(); this._destroyDragHelper(); this._contentDragHelper = new ElementDragHelper(this._contentEntity.element); this._contentDragHelper.on('drag:start', this._onContentDragStart, this); this._contentDragHelper.on('drag:end', this._onContentDragEnd, this); this._contentDragHelper.on('drag:move', this._onContentDragMove, this); this._prevContentSizes[ORIENTATION_HORIZONTAL] = null; this._prevContentSizes[ORIENTATION_VERTICAL] = null; this._syncAll(); } _onContentElementLose() { this._contentEntityElementUnsubscribe(); this._destroyDragHelper(); } _onContentDragStart() { if (this._contentEntity && this.enabled && this.entity.enabled) { this._dragStartPosition.copy(this._contentEntity.getLocalPosition()); } } _onContentDragEnd() { this._prevContentDragPosition = null; this._enableContentInput(); } _onContentDragMove(position) { if (this._contentEntity && this.enabled && this.entity.enabled) { this._wasDragged = true; this._setScrollFromContentPosition(position); this._setVelocityFromContentPositionDelta(position); if (!this._disabledContentInput) { const dx = position.x - this._dragStartPosition.x; const dy = position.y - this._dragStartPosition.y; if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) { this._disableContentInput(); } } } } _horizontalScrollbarEntitySubscribe() { this._evtHorizontalScrollbarAdd = this._horizontalScrollbarEntity.on('scrollbar:add', this._onHorizontalScrollbarGain, this); if (this._horizontalScrollbarEntity.scrollbar) { this._onHorizontalScrollbarGain(); } } _verticalScrollbarEntitySubscribe() { this._evtVerticalScrollbarAdd = this._verticalScrollbarEntity.on('scrollbar:add', this._onVerticalScrollbarGain, this); if (this._verticalScrollbarEntity.scrollbar) { this._onVerticalScrollbarGain(); } } _horizontalScrollbarEntityUnsubscribe() { this._evtHorizontalScrollbarAdd?.off(); this._evtHorizontalScrollbarAdd = null; if (this._horizontalScrollbarEntity.scrollbar) { this._onHorizontalScrollbarLose(); } } _verticalScrollbarEntityUnsubscribe() { this._evtVerticalScrollbarAdd?.off(); this._evtVerticalScrollbarAdd = null; if (this._verticalScrollbarEntity.scrollbar) { this._onVerticalScrollbarLose(); } } _onSetHorizontalScrollbarValue(scrollValueX) { if (!this._scrollbarUpdateFlags[ORIENTATION_HORIZONTAL] && this.enabled && this.entity.enabled) { this._onSetScroll(scrollValueX, null); } } _onSetVerticalScrollbarValue(scrollValueY) { if (!this._scrollbarUpdateFlags[ORIENTATION_VERTICAL] && this.enabled && this.entity.enabled) { this._onSetScroll(null, scrollValueY); } } _onHorizontalScrollbarGain() { const scrollbar = this._horizontalScrollbarEntity?.scrollbar; this._evtHorizontalScrollbarRemove = scrollbar.on('beforeremove', this._onHorizontalScrollbarLose, this); this._evtHorizontalScrollbarValue = scrollbar.on('set:value', this._onSetHorizontalScrollbarValue, this); this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); this._syncScrollbarPosition(ORIENTATION_HORIZONTAL); } _onVerticalScrollbarGain() { const scrollbar = this._verticalScrollbarEntity?.scrollbar; this._evtVerticalScrollbarRemove = scrollbar.on('beforeremove', this._onVerticalScrollbarLose, this); this._evtVerticalScrollbarValue = scrollbar.on('set:value', this._onSetVerticalScrollbarValue, this); this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); this._syncScrollbarPosition(ORIENTATION_VERTICAL); } _onHorizontalScrollbarLose() { this._evtHorizontalScrollbarRemove?.off(); this._evtHorizontalScrollbarRemove = null; this._evtHorizontalScrollbarValue?.off(); this._evtHorizontalScrollbarValue = null; } _onVerticalScrollbarLose() { this._evtVerticalScrollbarRemove?.off(); this._evtVerticalScrollbarRemove = null; this._evtVerticalScrollbarValue?.off(); this._evtVerticalScrollbarValue = null; } _onSetHorizontalScrollingEnabled() { this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); } _onSetVerticalScrollingEnabled() { this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); } _onSetScroll(x, y, resetVelocity) { if (resetVelocity !== false) { this._velocity.set(0, 0, 0); } const xChanged = this._updateAxis(x, 'x', ORIENTATION_HORIZONTAL); const yChanged = this._updateAxis(y, 'y', ORIENTATION_VERTICAL); if (xChanged || yChanged) { this.fire('set:scroll', this._scroll); } } _updateAxis(scrollValue, axis, orientation) { const hasChanged = scrollValue !== null && Math.abs(scrollValue - this._scroll[axis]) > 1e-5; if (hasChanged || this._isDragging() || scrollValue === 0) { this._scroll[axis] = this._determineNewScrollValue(scrollValue, axis, orientation); this._syncContentPosition(orientation); this._syncScrollbarPosition(orientation); } return hasChanged; } _determineNewScrollValue(scrollValue, axis, orientation) { if (!this._getScrollingEnabled(orientation)) { return this._scroll[axis]; } switch(this.scrollMode){ case SCROLL_MODE_CLAMP: return math.clamp(scrollValue, 0, this._getMaxScrollValue(orientation)); case SCROLL_MODE_BOUNCE: this._setVelocityFromOvershoot(scrollValue, axis, orientation); return scrollValue; case SCROLL_MODE_INFINITE: return scrollValue; default: console.warn(`Unhandled scroll mode:${this.scrollMode}`); return scrollValue; } } _syncAll() { this._syncContentPosition(ORIENTATION_HORIZONTAL); this._syncContentPosition(ORIENTATION_VERTICAL); this._syncScrollbarPosition(ORIENTATION_HORIZONTAL); this._syncScrollbarPosition(ORIENTATION_VERTICAL); this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); } _syncContentPosition(orientation) { if (!this._contentEntity) { return; } const axis = this._getAxis(orientation); const sign = this._getSign(orientation); const prevContentSize = this._prevContentSizes[orientation]; const currContentSize = this._getContentSize(orientation); if (prevContentSize !== null && Math.abs(prevContentSize - currContentSize) > 1e-4) { const prevMaxOffset = this._getMaxOffset(orientation, prevContentSize); const currMaxOffset = this._getMaxOffset(orientation, currContentSize); if (currMaxOffset === 0) { this._scroll[axis] = 1; } else { this._scroll[axis] = math.clamp(this._scroll[axis] * prevMaxOffset / currMaxOffset, 0, 1); } } const offset = this._scroll[axis] * this._getMaxOffset(orientation); const contentPosition = this._contentEntity.getLocalPosition(); contentPosition[axis] = offset * sign; this._contentEntity.setLocalPosition(contentPosition); this._prevContentSizes[orientation] = currContentSize; } _syncScrollbarPosition(orientation) { const scrollbarEntity = this._scrollbarEntities[orientation]; if (!scrollbarEntity?.scrollbar) { return; } const axis = this._getAxis(orientation); this._scrollbarUpdateFlags[orientation] = true; scrollbarEntity.scrollbar.value = this._scroll[axis]; scrollbarEntity.scrollbar.handleSize = this._getScrollbarHandleSize(axis, orientation); this._scrollbarUpdateFlags[orientation] = false; } _syncScrollbarEnabledState(orientation) { const entity = this._scrollbarEntities[orientation]; if (!entity) { return; } const isScrollingEnabled = this._getScrollingEnabled(orientation); const requestedVisibility = this._getScrollbarVisibility(orientation); switch(requestedVisibility){ case SCROLLBAR_VISIBILITY_SHOW_ALWAYS: entity.enabled = isScrollingEnabled; return; case SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED: entity.enabled = isScrollingEnabled && this._contentIsLargerThanViewport(orientation); return; default: console.warn(`Unhandled scrollbar visibility:${requestedVisibility}`); entity.enabled = isScrollingEnabled; } } _contentIsLargerThanViewport(orientation) { return this._getContentSize(orientation) > this._getViewportSize(orientation); } _contentPositionToScrollValue(contentPosition) { const maxOffsetH = this._getMaxOffset(ORIENTATION_HORIZONTAL); const maxOffsetV = this._getMaxOffset(ORIENTATION_VERTICAL); if (maxOffsetH === 0) { _tempScrollValue.x = 0; } else { _tempScrollValue.x = contentPosition.x / maxOffsetH; } if (maxOffsetV === 0) { _tempScrollValue.y = 0; } else { _tempScrollValue.y = contentPosition.y / -maxOffsetV; } return _tempScrollValue; } _getMaxOffset(orientation, contentSize) { contentSize = contentSize === undefined ? this._getContentSize(orientation) : contentSize; const viewportSize = this._getViewportSize(orientation); if (contentSize < viewportSize) { return -this._getViewportSize(orientation); } return viewportSize - contentSize; } _getMaxScrollValue(orientation) { return this._contentIsLargerThanViewport(orientation) ? 1 : 0; } _getScrollbarHandleSize(axis, orientation) { const viewportSize = this._getViewportSize(orientation); const contentSize = this._getContentSize(orientation); if (Math.abs(contentSize) < 0.001) { return 1; } const handleSize = Math.min(viewportSize / contentSize, 1); const overshoot = this._toOvershoot(this._scroll[axis], orientation); if (overshoot === 0) { return handleSize; } return handleSize / (1 + Math.abs(overshoot)); } _getViewportSize(orientation) { return this._getSize(orientation, this._viewportEntity); } _getContentSize(orientation) { return this._getSize(orientation, this._contentEntity); } _getSize(orientation, entity) { if (entity?.element) { return entity.element[this._getCalculatedDimension(orientation)]; } return 0; } _getScrollingEnabled(orientation) { if (orientation === ORIENTATION_HORIZONTAL) { return this.horizontal; } else if (orientation === ORIENTATION_VERTICAL) { return this.vertical; } return undefined; } _getScrollbarVisibility(orientation) { if (orientation === ORIENTATION_HORIZONTAL) { return this.horizontalScrollbarVisibility; } else if (orientation === ORIENTATION_VERTICAL) { return this.verticalScrollbarVisibility; } return undefined; } _getSign(orientation) { return orientation === ORIENTATION_HORIZONTAL ? 1 : -1; } _getAxis(orientation) { return orientation === ORIENTATION_HORIZONTAL ? 'x' : 'y'; } _getCalculatedDimension(orientation) { return orientation === ORIENTATION_HORIZONTAL ? 'calculatedWidth' : 'calculatedHeight'; } _destroyDragHelper() { if (this._contentDragHelper) { this._contentDragHelper.destroy(); } } onUpdate() { if (this._contentEntity) { this._updateVelocity(); this._syncScrollbarEnabledState(ORIENTATION_HORIZONTAL); this._syncScrollbarEnabledState(ORIENTATION_VERTICAL); } } _updateVelocity() { if (!this._isDragging()) { if (this.scrollMode === SCROLL_MODE_BOUNCE) { if (this._hasOvershoot('x', ORIENTATION_HORIZONTAL)) { this._setVelocityFromOvershoot(this.scroll.x, 'x', ORIENTATION_HORIZONTAL); } if (this._hasOvershoot('y', ORIENTATION_VERTICAL)) { this._setVelocityFromOvershoot(this.scroll.y, 'y', ORIENTATION_VERTICAL); } } if (Math.abs(this._velocity.x) > 1e-4 || Math.abs(this._velocity.y) > 1e-4) { const position = this._contentEntity.getLocalPosition(); position.x += this._velocity.x; position.y += this._velocity.y; this._contentEntity.setLocalPosition(position); this._setScrollFromContentPosition(position); } this._velocity.x *= 1 - this.friction; this._velocity.y *= 1 - this.friction; } } _hasOvershoot(axis, orientation) { return Math.abs(this._toOvershoot(this.scroll[axis], orientation)) > 0.001; } _toOvershoot(scrollValue, orientation) { const maxScrollValue = this._getMaxScrollValue(orientation); if (scrollValue < 0) { return scrollValue; } else if (scrollValue > maxScrollValue) { return scrollValue - maxScrollValue; } return 0; } _setVelocityFromOvershoot(scrollValue, axis, orientation) { const overshootValue = this._toOvershoot(scrollValue, orientation); const overshootPixels = overshootValue * this._getMaxOffset(orientation) * this._getSign(orientation); if (Math.abs(overshootPixels) > 0) { this._velocity[axis] = -overshootPixels / (this.bounceAmount * 50 + 1); } } _setVelocityFromContentPositionDelta(position) { if (this._prevContentDragPosition) { this._velocity.sub2(position, this._prevContentDragPosition); this._prevContentDragPosition.copy(position); } else { this._velocity.set(0, 0, 0); this._prevContentDragPosition = position.clone(); } } _setScrollFromContentPosition(position) { let scrollValue = this._contentPositionToScrollValue(position); if (this._isDragging()) { scrollValue = this._applyScrollValueTension(scrollValue); } this._onSetScroll(scrollValue.x, scrollValue.y, false); } _applyScrollValueTension(scrollValue) { const factor = 1; let max = this._getMaxScrollValue(ORIENTATION_HORIZONTAL); let overshoot = this._toOvershoot(scrollValue.x, ORIENTATION_HORIZONTAL); if (overshoot > 0) { scrollValue.x = max + factor * Math.log10(1 + overshoot); } else if (overshoot < 0) { scrollValue.x = -factor * Math.log10(1 - overshoot); } max = this._getMaxScrollValue(ORIENTATION_VERTICAL); overshoot = this._toOvershoot(scrollValue.y, ORIENTATION_VERTICAL); if (overshoot > 0) { scrollValue.y = max + factor * Math.log10(1 + overshoot); } else if (overshoot < 0) { scrollValue.y = -factor * Math.log10(1 - overshoot); } return scrollValue; } _isDragging() { return this._contentDragHelper && this._contentDragHelper.isDragging; } _setScrollbarComponentsEnabled(enabled) { if (this._horizontalScrollbarEntity?.scrollbar) { this._horizontalScrollbarEntity.scrollbar.enabled = enabled; } if (this._verticalScrollbarEntity?.scrollbar) { this._verticalScrollbarEntity.scrollbar.enabled = enabled; } } _setContentDraggingEnabled(enabled) { if (this._contentDragHelper) { this._contentDragHelper.enabled = enabled; } } _onMouseWheel(event) { if (!this.useMouseWheel || !this._contentEntity?.element) { return; } const wheelEvent = event.event; const normalizedDeltaX = wheelEvent.deltaX / this._contentEntity.element.calculatedWidth * this.mouseWheelSensitivity.x; const normalizedDeltaY = wheelEvent.deltaY / this._contentEntity.element.calculatedHeight * this.mouseWheelSensitivity.y; const scrollX = math.clamp(this._scroll.x + normalizedDeltaX, 0, this._getMaxScrollValue(ORIENTATION_HORIZONTAL)); const scrollY = math.clamp(this._scroll.y + normalizedDeltaY, 0, this._getMaxScrollValue(ORIENTATION_VERTICAL)); this.scroll = new Vec2(scrollX, scrollY); } _enableContentInput() { while(this._disabledContentInputEntities.length){ const e = this._disabledContentInputEntities.pop(); if (e.element) { e.element.useInput = true; } } this._disabledContentInput = false; } _disableContentInput() { const _disableInput = (e)=>{ if (e.element && e.element.useInput) { this._disabledContentInputEntities.push(e); e.element.useInput = false; } const children = e.children; for(let i = 0, l = children.length; i < l; i++){ _disableInput(children[i]); } }; if (this._contentEntity) { const children = this._contentEntity.children; for(let i = 0, l = children.length; i < l; i++){ _disableInput(children[i]); } } this._disabledContentInput = true; } onEnable() { this._setScrollbarComponentsEnabled(true); this._setContentDraggingEnabled(true); this._syncAll(); } onDisable() { this._setScrollbarComponentsEnabled(false); this._setContentDraggingEnabled(false); } onRemove() { this._toggleLifecycleListeners('off'); this._toggleElementListeners('off'); this._destroyDragHelper(); } resolveDuplicatedEntityReferenceProperties(oldScrollView, duplicatedIdsMap) { if (oldScrollView.viewportEntity) { this.viewportEntity = duplicatedIdsMap[oldScrollView.viewportEntity.getGuid()]; } if (oldScrollView.contentEntity) { this.contentEntity = duplicatedIdsMap[oldScrollView.contentEntity.getGuid()]; } if (oldScrollView.horizontalScrollbarEntity) { this.horizontalScrollbarEntity = duplicatedIdsMap[oldScrollView.horizontalScrollbarEntity.getGuid()]; } if (oldScrollView.verticalScrollbarEntity) { this.verticalScrollbarEntity = duplicatedIdsMap[oldScrollView.verticalScrollbarEntity.getGuid()]; } } constructor(system, entity){ super(system, entity), this._viewportEntity = null, this._contentEntity = null, this._horizontalScrollbarEntity = null, this._verticalScrollbarEntity = null, this._evtElementRemove = null, this._evtViewportElementRemove = null, this._evtViewportResize = null, this._evtContentEntityElementAdd = null, this._evtContentElementRemove = null, this._evtContentResize = null, this._evtHorizontalScrollbarAdd = null, this._evtHorizontalScrollbarRemove = null, this._evtHorizontalScrollbarValue = null, this._evtVerticalScrollbarAdd = null, this._evtVerticalScrollbarRemove = null, this._evtVerticalScrollbarValue = null; this._scrollbarUpdateFlags = {}; this._scrollbarEntities = {}; this._prevContentSizes = {}; this._prevContentSizes[ORIENTATION_HORIZONTAL] = null; this._prevContentSizes[ORIENTATION_VERTICAL] = null; this._scroll = new Vec2(); this._velocity = new Vec3(); this._dragStartPosition = new Vec3(); this._disabledContentInput = false; this._disabledContentInputEntities = []; this._toggleLifecycleListeners('on'); this._toggleElementListeners('on'); } } ScrollViewComponent.EVENT_SETSCROLL = 'set:scroll'; const DEFAULT_DRAG_THRESHOLD$1 = 10; class ScrollViewComponentData { constructor(){ this.enabled = true; this.dragThreshold = DEFAULT_DRAG_THRESHOLD$1; this.useMouseWheel = true; this.mouseWheelSensitivity = new Vec2(1, 1); this.horizontalScrollbarVisibility = 0; this.verticalScrollbarVisibility = 0; this.viewportEntity = null; this.contentEntity = null; this.horizontalScrollbarEntity = null; this.verticalScrollbarEntity = null; } } const _schema$6 = [ { name: 'enabled', type: 'boolean' }, { name: 'horizontal', type: 'boolean' }, { name: 'vertical', type: 'boolean' }, { name: 'scrollMode', type: 'number' }, { name: 'bounceAmount', type: 'number' }, { name: 'friction', type: 'number' }, { name: 'dragThreshold', type: 'number' }, { name: 'useMouseWheel', type: 'boolean' }, { name: 'mouseWheelSensitivity', type: 'vec2' }, { name: 'horizontalScrollbarVisibility', type: 'number' }, { name: 'verticalScrollbarVisibility', type: 'number' } ]; const DEFAULT_DRAG_THRESHOLD = 10; class ScrollViewComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { if (data.dragThreshold === undefined) { data.dragThreshold = DEFAULT_DRAG_THRESHOLD; } if (data.useMouseWheel === undefined) { data.useMouseWheel = true; } if (data.mouseWheelSensitivity === undefined) { data.mouseWheelSensitivity = new Vec2(1, 1); } super.initializeComponentData(component, data, _schema$6); component.viewportEntity = data.viewportEntity; component.contentEntity = data.contentEntity; component.horizontalScrollbarEntity = data.horizontalScrollbarEntity; component.verticalScrollbarEntity = data.verticalScrollbarEntity; } onUpdate(dt) { const components = this.store; for(const id in components){ const entity = components[id].entity; const component = entity.scrollview; if (component.enabled && entity.enabled) { component.onUpdate(); } } } _onRemoveComponent(entity, component) { component.onRemove(); } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); } constructor(app){ super(app); this.id = 'scrollview'; this.ComponentType = ScrollViewComponent; this.DataType = ScrollViewComponentData; this.schema = _schema$6; this.on('beforeremove', this._onRemoveComponent, this); this.app.systems.on('update', this.onUpdate, this); } } class ScrollbarComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) { this._setValue('enabled', arg); } get enabled() { return this.data.enabled; } set orientation(arg) { this._setValue('orientation', arg); } get orientation() { return this.data.orientation; } set value(arg) { this._setValue('value', arg); } get value() { return this.data.value; } set handleSize(arg) { this._setValue('handleSize', arg); } get handleSize() { return this.data.handleSize; } set handleEntity(arg) { if (this._handleEntity === arg) { return; } const isString = typeof arg === 'string'; if (this._handleEntity && isString && this._handleEntity.getGuid() === arg) { return; } if (this._handleEntity) { this._handleEntityUnsubscribe(); } if (arg instanceof GraphNode) { this._handleEntity = arg; } else if (isString) { this._handleEntity = this.system.app.getEntityFromIndex(arg) || null; } else { this._handleEntity = null; } if (this._handleEntity) { this._handleEntitySubscribe(); } if (this._handleEntity) { this.data.handleEntity = this._handleEntity.getGuid(); } else if (isString && arg) { this.data.handleEntity = arg; } } get handleEntity() { return this._handleEntity; } _setValue(name, value) { const data = this.data; const oldValue = data[name]; data[name] = value; this.fire('set', name, oldValue, value); } _toggleLifecycleListeners(onOrOff) { this[onOrOff]('set_value', this._onSetValue, this); this[onOrOff]('set_handleSize', this._onSetHandleSize, this); this[onOrOff]('set_orientation', this._onSetOrientation, this); } _handleEntitySubscribe() { this._evtHandleEntityElementAdd = this._handleEntity.on('element:add', this._onHandleElementGain, this); if (this._handleEntity.element) { this._onHandleElementGain(); } } _handleEntityUnsubscribe() { this._evtHandleEntityElementAdd?.off(); this._evtHandleEntityElementAdd = null; if (this._handleEntity?.element) { this._onHandleElementLose(); } } _handleEntityElementSubscribe() { const element = this._handleEntity.element; const handles = this._evtHandleEntityChanges; handles.push(element.once('beforeremove', this._onHandleElementLose, this)); handles.push(element.on('set:anchor', this._onSetHandleAlignment, this)); handles.push(element.on('set:margin', this._onSetHandleAlignment, this)); handles.push(element.on('set:pivot', this._onSetHandleAlignment, this)); } _handleEntityElementUnsubscribe() { for(let i = 0; i < this._evtHandleEntityChanges.length; i++){ this._evtHandleEntityChanges[i].off(); } this._evtHandleEntityChanges.length = 0; } _onHandleElementGain() { this._handleEntityElementSubscribe(); this._destroyDragHelper(); this._handleDragHelper = new ElementDragHelper(this._handleEntity.element, this._getAxis()); this._handleDragHelper.on('drag:move', this._onHandleDrag, this); this._updateHandlePositionAndSize(); } _onHandleElementLose() { this._handleEntityElementUnsubscribe(); this._destroyDragHelper(); } _onHandleDrag(position) { if (this._handleEntity && this.enabled && this.entity.enabled) { this.value = this._handlePositionToScrollValue(position[this._getAxis()]); } } _onSetValue(name, oldValue, newValue) { if (Math.abs(newValue - oldValue) > 1e-5) { this.data.value = math.clamp(newValue, 0, 1); this._updateHandlePositionAndSize(); this.fire('set:value', this.data.value); } } _onSetHandleSize(name, oldValue, newValue) { if (Math.abs(newValue - oldValue) > 1e-5) { this.data.handleSize = math.clamp(newValue, 0, 1); this._updateHandlePositionAndSize(); } } _onSetHandleAlignment() { this._updateHandlePositionAndSize(); } _onSetOrientation(name, oldValue, newValue) { if (newValue !== oldValue && this._handleEntity?.element) { this._handleEntity.element[this._getOppositeDimension()] = 0; } } _updateHandlePositionAndSize() { const handleEntity = this._handleEntity; const handleElement = handleEntity?.element; if (handleEntity) { const position = handleEntity.getLocalPosition(); position[this._getAxis()] = this._getHandlePosition(); handleEntity.setLocalPosition(position); } if (handleElement) { handleElement[this._getDimension()] = this._getHandleLength(); } } _handlePositionToScrollValue(handlePosition) { return handlePosition * this._getSign() / this._getUsableTrackLength(); } _scrollValueToHandlePosition(value) { return value * this._getSign() * this._getUsableTrackLength(); } _getUsableTrackLength() { return Math.max(this._getTrackLength() - this._getHandleLength(), 0.001); } _getTrackLength() { if (this.entity.element) { return this.orientation === ORIENTATION_HORIZONTAL ? this.entity.element.calculatedWidth : this.entity.element.calculatedHeight; } return 0; } _getHandleLength() { return this._getTrackLength() * this.handleSize; } _getHandlePosition() { return this._scrollValueToHandlePosition(this.value); } _getSign() { return this.orientation === ORIENTATION_HORIZONTAL ? 1 : -1; } _getAxis() { return this.orientation === ORIENTATION_HORIZONTAL ? 'x' : 'y'; } _getDimension() { return this.orientation === ORIENTATION_HORIZONTAL ? 'width' : 'height'; } _getOppositeDimension() { return this.orientation === ORIENTATION_HORIZONTAL ? 'height' : 'width'; } _destroyDragHelper() { if (this._handleDragHelper) { this._handleDragHelper.destroy(); } } _setHandleDraggingEnabled(enabled) { if (this._handleDragHelper) { this._handleDragHelper.enabled = enabled; } } onEnable() { this._setHandleDraggingEnabled(true); } onDisable() { this._setHandleDraggingEnabled(false); } onRemove() { this._destroyDragHelper(); this._toggleLifecycleListeners('off'); } resolveDuplicatedEntityReferenceProperties(oldScrollbar, duplicatedIdsMap) { if (oldScrollbar.handleEntity) { this.handleEntity = duplicatedIdsMap[oldScrollbar.handleEntity.getGuid()]; } } constructor(system, entity){ super(system, entity), this._handleEntity = null, this._evtHandleEntityElementAdd = null, this._evtHandleEntityChanges = []; this._toggleLifecycleListeners('on'); } } ScrollbarComponent.EVENT_SETVALUE = 'set:value'; class ScrollbarComponentData { constructor(){ this.enabled = true; this.orientation = ORIENTATION_HORIZONTAL; this.value = 0; this.handleSize = 0; this.handleEntity = null; } } const _schema$5 = [ { name: 'enabled', type: 'boolean' }, { name: 'orientation', type: 'number' }, { name: 'value', type: 'number' }, { name: 'handleSize', type: 'number' } ]; class ScrollbarComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { super.initializeComponentData(component, data, _schema$5); component.handleEntity = data.handleEntity; } _onAddComponent(entity) { entity.fire('scrollbar:add'); } _onRemoveComponent(entity, component) { component.onRemove(); } constructor(app){ super(app); this.id = 'scrollbar'; this.ComponentType = ScrollbarComponent; this.DataType = ScrollbarComponentData; this.schema = _schema$5; this.on('add', this._onAddComponent, this); this.on('beforeremove', this._onRemoveComponent, this); } } const instanceOptions = { volume: 0, pitch: 0, loop: false, startTime: 0, duration: 0, position: new Vec3(), maxDistance: 0, refDistance: 0, rollOffFactor: 0, distanceModel: 0, onPlay: null, onPause: null, onResume: null, onStop: null, onEnd: null }; class SoundSlot extends EventHandler { play() { if (!this.overlap) { this.stop(); } if (!this.isLoaded && !this._hasAsset()) { return undefined; } const instance = this._createInstance(); this.instances.push(instance); if (!this.isLoaded) { const onLoad = function(sound) { const playWhenLoaded = instance._playWhenLoaded; instance.sound = sound; if (playWhenLoaded) { instance.play(); } }; this.off('load', onLoad); this.once('load', onLoad); this.load(); } else { instance.play(); } return instance; } pause() { let paused = false; const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ if (instances[i].pause()) { paused = true; } } return paused; } resume() { let resumed = false; const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ if (instances[i].resume()) { resumed = true; } } return resumed; } stop() { let stopped = false; const instances = this.instances; let i = instances.length; while(i--){ instances[i].stop(); stopped = true; } instances.length = 0; return stopped; } load() { if (!this._hasAsset()) { return; } const asset = this._assets.get(this._asset); if (!asset) { this._assets.off(`add:${this._asset}`, this._onAssetAdd, this); this._assets.once(`add:${this._asset}`, this._onAssetAdd, this); return; } asset.off('remove', this._onAssetRemoved, this); asset.on('remove', this._onAssetRemoved, this); if (!asset.resource) { asset.off('load', this._onAssetLoad, this); asset.once('load', this._onAssetLoad, this); this._assets.load(asset); return; } this.fire('load', asset.resource); } setExternalNodes(firstNode, lastNode) { if (!firstNode) { console.error('The firstNode must have a valid AudioNode'); return; } if (!lastNode) { lastNode = firstNode; } this._firstNode = firstNode; this._lastNode = lastNode; if (!this._overlap) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].setExternalNodes(firstNode, lastNode); } } } clearExternalNodes() { this._firstNode = null; this._lastNode = null; if (!this._overlap) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].clearExternalNodes(); } } } getExternalNodes() { return [ this._firstNode, this._lastNode ]; } _hasAsset() { return this._asset != null; } _createInstance() { let instance = null; const component = this._component; let sound = null; if (this._hasAsset()) { const asset = this._assets.get(this._asset); if (asset) { sound = asset.resource; } } const data = instanceOptions; data.volume = this._volume * component.volume; data.pitch = this._pitch * component.pitch; data.loop = this._loop; data.startTime = this._startTime; data.duration = this._duration; data.onPlay = this._onInstancePlayHandler; data.onPause = this._onInstancePauseHandler; data.onResume = this._onInstanceResumeHandler; data.onStop = this._onInstanceStopHandler; data.onEnd = this._onInstanceEndHandler; if (component.positional) { data.position.copy(component.entity.getPosition()); data.maxDistance = component.maxDistance; data.refDistance = component.refDistance; data.rollOffFactor = component.rollOffFactor; data.distanceModel = component.distanceModel; instance = new SoundInstance3d(this._manager, sound, data); } else { instance = new SoundInstance(this._manager, sound, data); } if (this._firstNode) { instance.setExternalNodes(this._firstNode, this._lastNode); } return instance; } _onInstancePlay(instance) { this.fire('play', instance); this._component.fire('play', this, instance); } _onInstancePause(instance) { this.fire('pause', instance); this._component.fire('pause', this, instance); } _onInstanceResume(instance) { this.fire('resume', instance); this._component.fire('resume', this, instance); } _onInstanceStop(instance) { const idx = this.instances.indexOf(instance); if (idx !== -1) { this.instances.splice(idx, 1); } this.fire('stop', instance); this._component.fire('stop', this, instance); } _onInstanceEnd(instance) { const idx = this.instances.indexOf(instance); if (idx !== -1) { this.instances.splice(idx, 1); } this.fire('end', instance); this._component.fire('end', this, instance); } _onAssetAdd(asset) { this.load(); } _onAssetLoad(asset) { this.load(); } _onAssetRemoved(asset) { asset.off('remove', this._onAssetRemoved, this); this._assets.off(`add:${asset.id}`, this._onAssetAdd, this); this.stop(); } updatePosition(position) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].position = position; } } set asset(value) { const old = this._asset; if (old) { this._assets.off(`add:${old}`, this._onAssetAdd, this); const oldAsset = this._assets.get(old); if (oldAsset) { oldAsset.off('remove', this._onAssetRemoved, this); } } this._asset = value; if (this._asset instanceof Asset) { this._asset = this._asset.id; } if (this._hasAsset() && this._component.enabled && this._component.entity.enabled) { this.load(); } } get asset() { return this._asset; } set autoPlay(value) { this._autoPlay = !!value; } get autoPlay() { return this._autoPlay; } set duration(value) { this._duration = Math.max(0, Number(value) || 0) || null; if (!this._overlap) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].duration = this._duration; } } } get duration() { let assetDuration = 0; if (this._hasAsset()) { const asset = this._assets.get(this._asset); assetDuration = asset?.resource ? asset.resource.duration : 0; } if (this._duration != null) { return this._duration % (assetDuration || 1); } return assetDuration; } get isLoaded() { if (this._hasAsset()) { const asset = this._assets.get(this._asset); if (asset) { return !!asset.resource; } } return false; } get isPaused() { const instances = this.instances; const len = instances.length; if (len === 0) { return false; } for(let i = 0; i < len; i++){ if (!instances[i].isPaused) { return false; } } return true; } get isPlaying() { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ if (instances[i].isPlaying) { return true; } } return false; } get isStopped() { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ if (!instances[i].isStopped) { return false; } } return true; } set loop(value) { this._loop = !!value; const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].loop = this._loop; } } get loop() { return this._loop; } set overlap(value) { this._overlap = !!value; } get overlap() { return this._overlap; } set pitch(value) { this._pitch = Math.max(Number(value) || 0, 0.01); if (!this._overlap) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].pitch = this.pitch * this._component.pitch; } } } get pitch() { return this._pitch; } set startTime(value) { this._startTime = Math.max(0, Number(value) || 0); if (!this._overlap) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].startTime = this._startTime; } } } get startTime() { return this._startTime; } set volume(value) { this._volume = math.clamp(Number(value) || 0, 0, 1); if (!this._overlap) { const instances = this.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i].volume = this._volume * this._component.volume; } } } get volume() { return this._volume; } constructor(component, name = 'Untitled', options = {}){ super(), this.instances = []; this._component = component; this._assets = component.system.app.assets; this._manager = component.system.manager; this.name = name; this._volume = options.volume !== undefined ? math.clamp(Number(options.volume) || 0, 0, 1) : 1; this._pitch = options.pitch !== undefined ? Math.max(0.01, Number(options.pitch) || 0) : 1; this._loop = !!(options.loop !== undefined ? options.loop : false); this._duration = options.duration > 0 ? options.duration : null; this._startTime = Math.max(0, Number(options.startTime) || 0); this._overlap = !!options.overlap; this._autoPlay = !!options.autoPlay; this._firstNode = null; this._lastNode = null; this._asset = options.asset; if (this._asset instanceof Asset) { this._asset = this._asset.id; } this._onInstancePlayHandler = this._onInstancePlay.bind(this); this._onInstancePauseHandler = this._onInstancePause.bind(this); this._onInstanceResumeHandler = this._onInstanceResume.bind(this); this._onInstanceStopHandler = this._onInstanceStop.bind(this); this._onInstanceEndHandler = this._onInstanceEnd.bind(this); } } SoundSlot.EVENT_PLAY = 'play'; SoundSlot.EVENT_PAUSE = 'pause'; SoundSlot.EVENT_RESUME = 'resume'; SoundSlot.EVENT_STOP = 'stop'; SoundSlot.EVENT_END = 'end'; SoundSlot.EVENT_LOAD = 'load'; class SoundComponent extends Component { _updateSoundInstances(property, value, isFactor) { const slots = this._slots; for(const key in slots){ const slot = slots[key]; if (!slot.overlap) { const instances = slot.instances; for(let i = 0, len = instances.length; i < len; i++){ instances[i][property] = isFactor ? slot[property] * value : value; } } } } set distanceModel(value) { this._distanceModel = value; this._updateSoundInstances('distanceModel', value, false); } get distanceModel() { return this._distanceModel; } set maxDistance(value) { this._maxDistance = value; this._updateSoundInstances('maxDistance', value, false); } get maxDistance() { return this._maxDistance; } set refDistance(value) { this._refDistance = value; this._updateSoundInstances('refDistance', value, false); } get refDistance() { return this._refDistance; } set rollOffFactor(value) { this._rollOffFactor = value; this._updateSoundInstances('rollOffFactor', value, false); } get rollOffFactor() { return this._rollOffFactor; } set pitch(value) { this._pitch = value; this._updateSoundInstances('pitch', value, true); } get pitch() { return this._pitch; } set volume(value) { this._volume = value; this._updateSoundInstances('volume', value, true); } get volume() { return this._volume; } set positional(newValue) { this._positional = newValue; const slots = this._slots; for(const key in slots){ const slot = slots[key]; if (!slot.overlap) { const instances = slot.instances; const oldLength = instances.length; for(let i = oldLength - 1; i >= 0; i--){ const isPlaying = instances[i].isPlaying || instances[i].isSuspended; const currentTime = instances[i].currentTime; if (isPlaying) { instances[i].stop(); } const instance = slot._createInstance(); if (isPlaying) { instance.play(); instance.currentTime = currentTime; } instances.push(instance); } } } } get positional() { return this._positional; } set slots(newValue) { const oldValue = this._slots; if (oldValue) { for(const key in oldValue){ oldValue[key].stop(); } } const slots = {}; for(const key in newValue){ if (!(newValue[key] instanceof SoundSlot)) { if (newValue[key].name) { slots[newValue[key].name] = new SoundSlot(this, newValue[key].name, newValue[key]); } } else { slots[newValue[key].name] = newValue[key]; } } this._slots = slots; if (this.enabled && this.entity.enabled) { this.onEnable(); } } get slots() { return this._slots; } onEnable() { if (this.system._inTools) { return; } const slots = this._slots; const playingBeforeDisable = this._playingBeforeDisable; for(const key in slots){ const slot = slots[key]; if (slot.autoPlay && slot.isStopped) { slot.play(); } else if (playingBeforeDisable[key]) { slot.resume(); } else if (!slot.isLoaded) { slot.load(); } } } onDisable() { const slots = this._slots; const playingBeforeDisable = {}; for(const key in slots){ if (!slots[key].overlap) { if (slots[key].isPlaying) { slots[key].pause(); playingBeforeDisable[key] = true; } } } this._playingBeforeDisable = playingBeforeDisable; } onRemove() { this.off(); } addSlot(name, options) { const slots = this._slots; if (slots[name]) { return null; } const slot = new SoundSlot(this, name, options); slots[name] = slot; if (slot.autoPlay && this.enabled && this.entity.enabled) { slot.play(); } return slot; } removeSlot(name) { const slots = this._slots; if (slots[name]) { slots[name].stop(); delete slots[name]; } } slot(name) { return this._slots[name]; } _getSlotProperty(name, property) { if (!this.enabled || !this.entity.enabled) { return undefined; } const slot = this._slots[name]; if (!slot) { return undefined; } return slot[property]; } isPlaying(name) { return this._getSlotProperty(name, 'isPlaying') || false; } isLoaded(name) { return this._getSlotProperty(name, 'isLoaded') || false; } isPaused(name) { return this._getSlotProperty(name, 'isPaused') || false; } isStopped(name) { return this._getSlotProperty(name, 'isStopped') || false; } play(name) { if (!this.enabled || !this.entity.enabled) { return null; } const slot = this._slots[name]; if (!slot) { return null; } return slot.play(); } pause(name) { const slots = this._slots; if (name) { const slot = slots[name]; if (!slot) { return; } slot.pause(); } else { for(const key in slots){ slots[key].pause(); } } } resume(name) { const slots = this._slots; if (name) { const slot = slots[name]; if (!slot) { return; } if (slot.isPaused) { slot.resume(); } } else { for(const key in slots){ slots[key].resume(); } } } stop(name) { const slots = this._slots; if (name) { const slot = slots[name]; if (!slot) { return; } slot.stop(); } else { for(const key in slots){ slots[key].stop(); } } } constructor(...args){ super(...args), this._volume = 1, this._pitch = 1, this._positional = true, this._refDistance = 1, this._maxDistance = 10000, this._rollOffFactor = 1, this._distanceModel = DISTANCE_LINEAR, this._slots = {}, this._playingBeforeDisable = {}; } } SoundComponent.EVENT_PLAY = 'play'; SoundComponent.EVENT_PAUSE = 'pause'; SoundComponent.EVENT_RESUME = 'resume'; SoundComponent.EVENT_STOP = 'stop'; SoundComponent.EVENT_END = 'end'; class SoundComponentData { constructor(){ this.enabled = true; } } const _schema$4 = [ 'enabled' ]; class SoundComponentSystem extends ComponentSystem { set volume(volume) { this.manager.volume = volume; } get volume() { return this.manager.volume; } get context() { if (!hasAudioContext()) { return null; } return this.manager.context; } initializeComponentData(component, data, properties) { properties = [ 'volume', 'pitch', 'positional', 'refDistance', 'maxDistance', 'rollOffFactor', 'distanceModel', 'slots' ]; for(let i = 0; i < properties.length; i++){ if (data.hasOwnProperty(properties[i])) { component[properties[i]] = data[properties[i]]; } } super.initializeComponentData(component, data, [ 'enabled' ]); } cloneComponent(entity, clone) { const srcComponent = entity.sound; const srcSlots = srcComponent.slots; const slots = {}; for(const key in srcSlots){ const srcSlot = srcSlots[key]; slots[key] = { name: srcSlot.name, volume: srcSlot.volume, pitch: srcSlot.pitch, loop: srcSlot.loop, duration: srcSlot.duration, startTime: srcSlot.startTime, overlap: srcSlot.overlap, autoPlay: srcSlot.autoPlay, asset: srcSlot.asset }; } const cloneData = { distanceModel: srcComponent.distanceModel, enabled: srcComponent.enabled, maxDistance: srcComponent.maxDistance, pitch: srcComponent.pitch, positional: srcComponent.positional, refDistance: srcComponent.refDistance, rollOffFactor: srcComponent.rollOffFactor, slots: slots, volume: srcComponent.volume }; return this.addComponent(clone, cloneData); } onUpdate(dt) { const store = this.store; for(const id in store){ if (store.hasOwnProperty(id)) { const item = store[id]; const entity = item.entity; if (entity.enabled) { const component = entity.sound; if (component.enabled && component.positional) { const position = entity.getPosition(); const slots = component.slots; for(const key in slots){ slots[key].updatePosition(position); } } } } } } onBeforeRemove(entity, component) { const slots = component.slots; for(const key in slots){ if (!slots[key].overlap) { slots[key].stop(); } } component.onRemove(); } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); } constructor(app){ super(app); this.id = 'sound'; this.ComponentType = SoundComponent; this.DataType = SoundComponentData; this.schema = _schema$4; this.manager = app.soundManager; this.app.systems.on('update', this.onUpdate, this); this.on('beforeremove', this.onBeforeRemove, this); } } Component._buildAccessors(SoundComponent.prototype, _schema$4); const SPRITETYPE_SIMPLE = 'simple'; const SPRITETYPE_ANIMATED = 'animated'; class SpriteAnimationClip extends EventHandler { get duration() { if (this._sprite) { const fps = this.fps || Number.MIN_VALUE; return this._sprite.frameKeys.length / Math.abs(fps); } return 0; } set frame(value) { this._setFrame(value); const fps = this.fps || Number.MIN_VALUE; this._setTime(this._frame / fps); } get frame() { return this._frame; } get isPaused() { return this._paused; } get isPlaying() { return this._playing; } set sprite(value) { if (this._sprite) { this._evtSetMeshes?.off(); this._evtSetMeshes = null; this._sprite.off('set:pixelsPerUnit', this._onSpritePpuChanged, this); this._sprite.off('set:atlas', this._onSpriteMeshesChange, this); if (this._sprite.atlas) { this._sprite.atlas.off('set:texture', this._onSpriteMeshesChange, this); } } this._sprite = value; if (this._sprite) { this._evtSetMeshes = this._sprite.on('set:meshes', this._onSpriteMeshesChange, this); this._sprite.on('set:pixelsPerUnit', this._onSpritePpuChanged, this); this._sprite.on('set:atlas', this._onSpriteMeshesChange, this); if (this._sprite.atlas) { this._sprite.atlas.on('set:texture', this._onSpriteMeshesChange, this); } } if (this._component.currentClip === this) { let mi; if (!value || !value.atlas) { mi = this._component._meshInstance; if (mi) { mi.deleteParameter('texture_emissiveMap'); mi.deleteParameter('texture_opacityMap'); } this._component._hideModel(); } else { if (value.atlas.texture) { mi = this._component._meshInstance; if (mi) { mi.setParameter('texture_emissiveMap', value.atlas.texture); mi.setParameter('texture_opacityMap', value.atlas.texture); } if (this._component.enabled && this._component.entity.enabled) { this._component._showModel(); } } if (this.time && this.fps) { this.time = this.time; } else { this.frame = this.frame; } } } } get sprite() { return this._sprite; } set spriteAsset(value) { const assets = this._component.system.app.assets; let id = value; if (value instanceof Asset) { id = value.id; } if (this._spriteAsset !== id) { if (this._spriteAsset) { const prev = assets.get(this._spriteAsset); if (prev) { this._unbindSpriteAsset(prev); } } this._spriteAsset = id; if (this._spriteAsset) { const asset = assets.get(this._spriteAsset); if (!asset) { this.sprite = null; assets.on(`add:${this._spriteAsset}`, this._onSpriteAssetAdded, this); } else { this._bindSpriteAsset(asset); } } else { this.sprite = null; } } } get spriteAsset() { return this._spriteAsset; } set time(value) { this._setTime(value); if (this._sprite) { this.frame = Math.min(this._sprite.frameKeys.length - 1, Math.floor(this._time * Math.abs(this.fps))); } else { this.frame = 0; } } get time() { return this._time; } _onSpriteAssetAdded(asset) { this._component.system.app.assets.off(`add:${asset.id}`, this._onSpriteAssetAdded, this); if (this._spriteAsset === asset.id) { this._bindSpriteAsset(asset); } } _bindSpriteAsset(asset) { asset.on('load', this._onSpriteAssetLoad, this); asset.on('remove', this._onSpriteAssetRemove, this); if (asset.resource) { this._onSpriteAssetLoad(asset); } else { this._component.system.app.assets.load(asset); } } _unbindSpriteAsset(asset) { if (!asset) { return; } asset.off('load', this._onSpriteAssetLoad, this); asset.off('remove', this._onSpriteAssetRemove, this); if (asset.resource && !asset.resource.atlas) { this._component.system.app.assets.off(`load:${asset.data.textureAtlasAsset}`, this._onTextureAtlasLoad, this); } } _onSpriteAssetLoad(asset) { if (!asset.resource) { this.sprite = null; } else { if (!asset.resource.atlas) { const atlasAssetId = asset.data.textureAtlasAsset; const assets = this._component.system.app.assets; assets.off(`load:${atlasAssetId}`, this._onTextureAtlasLoad, this); assets.once(`load:${atlasAssetId}`, this._onTextureAtlasLoad, this); } else { this.sprite = asset.resource; } } } _onTextureAtlasLoad(atlasAsset) { const spriteAsset = this._spriteAsset; if (spriteAsset instanceof Asset) { this._onSpriteAssetLoad(spriteAsset); } else { this._onSpriteAssetLoad(this._component.system.app.assets.get(spriteAsset)); } } _onSpriteAssetRemove(asset) { this.sprite = null; } _onSpriteMeshesChange() { if (this._component.currentClip === this) { this._component._showFrame(this.frame); } } _onSpritePpuChanged() { if (this._component.currentClip === this) { if (this.sprite.renderMode !== SPRITE_RENDERMODE_SIMPLE) { this._component._showFrame(this.frame); } } } _update(dt) { if (this.fps === 0) return; if (!this._playing || this._paused || !this._sprite) return; const dir = this.fps < 0 ? -1 : 1; const time = this._time + dt * this._component.speed * dir; const duration = this.duration; const end = time > duration || time < 0; this._setTime(time); let frame = this.frame; if (this._sprite) { frame = Math.floor(this._sprite.frameKeys.length * this._time / duration); } else { frame = 0; } if (frame !== this._frame) { this._setFrame(frame); } if (end) { if (this.loop) { this.fire('loop'); this._component.fire('loop', this); } else { this._playing = false; this._paused = false; this.fire('end'); this._component.fire('end', this); } } } _setTime(value) { this._time = value; const duration = this.duration; if (this._time < 0) { if (this.loop) { this._time = this._time % duration + duration; } else { this._time = 0; } } else if (this._time > duration) { if (this.loop) { this._time %= duration; } else { this._time = duration; } } } _setFrame(value) { if (this._sprite) { this._frame = math.clamp(value, 0, this._sprite.frameKeys.length - 1); } else { this._frame = value; } if (this._component.currentClip === this) { this._component._showFrame(this._frame); } } _destroy() { if (this._spriteAsset) { const assets = this._component.system.app.assets; this._unbindSpriteAsset(assets.get(this._spriteAsset)); } if (this._sprite) { this.sprite = null; } if (this._spriteAsset) { this.spriteAsset = null; } } play() { if (this._playing) { return; } this._playing = true; this._paused = false; this.frame = 0; this.fire('play'); this._component.fire('play', this); } pause() { if (!this._playing || this._paused) { return; } this._paused = true; this.fire('pause'); this._component.fire('pause', this); } resume() { if (!this._paused) return; this._paused = false; this.fire('resume'); this._component.fire('resume', this); } stop() { if (!this._playing) return; this._playing = false; this._paused = false; this._time = 0; this.frame = 0; this.fire('stop'); this._component.fire('stop', this); } constructor(component, data){ super(), this._evtSetMeshes = null; this._component = component; this._frame = 0; this._sprite = null; this._spriteAsset = null; this.spriteAsset = data.spriteAsset; this.name = data.name; this.fps = data.fps || 0; this.loop = data.loop || false; this._playing = false; this._paused = false; this._time = 0; } } SpriteAnimationClip.EVENT_PLAY = 'play'; SpriteAnimationClip.EVENT_PAUSE = 'pause'; SpriteAnimationClip.EVENT_RESUME = 'resume'; SpriteAnimationClip.EVENT_STOP = 'stop'; SpriteAnimationClip.EVENT_END = 'end'; SpriteAnimationClip.EVENT_LOOP = 'loop'; const PARAM_EMISSIVE_MAP = 'texture_emissiveMap'; const PARAM_OPACITY_MAP = 'texture_opacityMap'; const PARAM_EMISSIVE = 'material_emissive'; const PARAM_OPACITY = 'material_opacity'; const PARAM_INNER_OFFSET = 'innerOffset'; const PARAM_OUTER_SCALE = 'outerScale'; const PARAM_ATLAS_RECT = 'atlasRect'; class SpriteComponent extends Component { set type(value) { if (this._type === value) { return; } this._type = value; if (this._type === SPRITETYPE_SIMPLE) { this.stop(); this._currentClip = this._defaultClip; if (this.enabled && this.entity.enabled) { this._currentClip.frame = this.frame; if (this._currentClip.sprite) { this._showModel(); } else { this._hideModel(); } } } else if (this._type === SPRITETYPE_ANIMATED) { this.stop(); if (this._autoPlayClip) { this._tryAutoPlay(); } if (this._currentClip && this._currentClip.isPlaying && this.enabled && this.entity.enabled) { this._showModel(); } else { this._hideModel(); } } } get type() { return this._type; } set frame(value) { this._currentClip.frame = value; } get frame() { return this._currentClip.frame; } set spriteAsset(value) { this._defaultClip.spriteAsset = value; } get spriteAsset() { return this._defaultClip._spriteAsset; } set sprite(value) { this._currentClip.sprite = value; } get sprite() { return this._currentClip.sprite; } set material(value) { this._material = value; if (this._meshInstance) { this._meshInstance.material = value; } } get material() { return this._material; } set color(value) { this._color.r = value.r; this._color.g = value.g; this._color.b = value.b; if (this._meshInstance) { this._colorUniform[0] = this._color.r; this._colorUniform[1] = this._color.g; this._colorUniform[2] = this._color.b; this._meshInstance.setParameter(PARAM_EMISSIVE, this._colorUniform); } } get color() { return this._color; } set opacity(value) { this._color.a = value; if (this._meshInstance) { this._meshInstance.setParameter(PARAM_OPACITY, value); } } get opacity() { return this._color.a; } set clips(value) { if (!value) { for(const name in this._clips){ this.removeClip(name); } return; } for(const name in this._clips){ let found = false; for(const key in value){ if (value[key].name === name) { found = true; this._clips[name].fps = value[key].fps; this._clips[name].loop = value[key].loop; if (value[key].hasOwnProperty('sprite')) { this._clips[name].sprite = value[key].sprite; } else if (value[key].hasOwnProperty('spriteAsset')) { this._clips[name].spriteAsset = value[key].spriteAsset; } break; } } if (!found) { this.removeClip(name); } } for(const key in value){ if (this._clips[value[key].name]) continue; this.addClip(value[key]); } if (this._autoPlayClip) { this._tryAutoPlay(); } if (!this._currentClip || !this._currentClip.sprite) { this._hideModel(); } } get clips() { return this._clips; } get currentClip() { return this._currentClip; } set speed(value) { this._speed = value; } get speed() { return this._speed; } set flipX(value) { if (this._flipX === value) return; this._flipX = value; this._updateTransform(); } get flipX() { return this._flipX; } set flipY(value) { if (this._flipY === value) return; this._flipY = value; this._updateTransform(); } get flipY() { return this._flipY; } set width(value) { if (value === this._width) return; this._width = value; this._outerScale.x = this._width; if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_TILED || this.sprite.renderMode === SPRITE_RENDERMODE_SLICED)) { this._updateTransform(); } } get width() { return this._width; } set height(value) { if (value === this._height) return; this._height = value; this._outerScale.y = this.height; if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_TILED || this.sprite.renderMode === SPRITE_RENDERMODE_SLICED)) { this._updateTransform(); } } get height() { return this._height; } set batchGroupId(value) { if (this._batchGroupId === value) { return; } const prev = this._batchGroupId; this._batchGroupId = value; if (this.entity.enabled && prev >= 0) { this.system.app.batcher?.remove(BatchGroup.SPRITE, prev, this.entity); } if (this.entity.enabled && value >= 0) { this.system.app.batcher?.insert(BatchGroup.SPRITE, value, this.entity); } else { if (prev >= 0) { if (this._currentClip && this._currentClip.sprite && this.enabled && this.entity.enabled) { this._showModel(); } } } } get batchGroupId() { return this._batchGroupId; } set autoPlayClip(value) { this._autoPlayClip = value instanceof SpriteAnimationClip ? value.name : value; this._tryAutoPlay(); } get autoPlayClip() { return this._autoPlayClip; } set drawOrder(value) { this._drawOrder = value; if (this._meshInstance) { this._meshInstance.drawOrder = value; } } get drawOrder() { return this._drawOrder; } set layers(value) { if (this._addedModel) { this._hideModel(); } this._layers = value; if (!this._meshInstance) { return; } if (this.enabled && this.entity.enabled) { this._showModel(); } } get layers() { return this._layers; } get aabb() { if (this._meshInstance) { return this._meshInstance.aabb; } return null; } onEnable() { const app = this.system.app; const scene = app.scene; const layers = scene.layers; this._evtLayersChanged = scene.on('set:layers', this._onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this._onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this._onLayerRemoved, this); } this._showModel(); if (this._autoPlayClip) { this._tryAutoPlay(); } if (this._batchGroupId >= 0) { app.batcher?.insert(BatchGroup.SPRITE, this._batchGroupId, this.entity); } } onDisable() { const app = this.system.app; const scene = app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } this.stop(); this._hideModel(); if (this._batchGroupId >= 0) { app.batcher?.remove(BatchGroup.SPRITE, this._batchGroupId, this.entity); } } onDestroy() { this._currentClip = null; if (this._defaultClip) { this._defaultClip._destroy(); this._defaultClip = null; } for(const key in this._clips){ this._clips[key]._destroy(); } this._clips = null; this._hideModel(); this._model = null; this._node?.remove(); this._node = null; if (this._meshInstance) { this._meshInstance.material = null; this._meshInstance.mesh = null; this._meshInstance = null; } } _showModel() { if (this._addedModel) return; if (!this._meshInstance) return; const meshInstances = [ this._meshInstance ]; for(let i = 0, len = this._layers.length; i < len; i++){ const layer = this.system.app.scene.layers.getLayerById(this._layers[i]); if (layer) { layer.addMeshInstances(meshInstances); } } this._addedModel = true; } _hideModel() { if (!this._addedModel || !this._meshInstance) return; const meshInstances = [ this._meshInstance ]; for(let i = 0, len = this._layers.length; i < len; i++){ const layer = this.system.app.scene.layers.getLayerById(this._layers[i]); if (layer) { layer.removeMeshInstances(meshInstances); } } this._addedModel = false; } _showFrame(frame) { if (!this.sprite) return; const mesh = this.sprite.meshes[frame]; if (!mesh) { if (this._meshInstance) { this._meshInstance.mesh = null; this._meshInstance.visible = false; } return; } let material; if (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED) { material = this.system.default9SlicedMaterialSlicedMode; } else if (this.sprite.renderMode === SPRITE_RENDERMODE_TILED) { material = this.system.default9SlicedMaterialTiledMode; } else { material = this.system.defaultMaterial; } if (!this._meshInstance) { this._meshInstance = new MeshInstance(mesh, this._material, this._node); this._meshInstance.castShadow = false; this._meshInstance.receiveShadow = false; this._meshInstance.drawOrder = this._drawOrder; this._model.meshInstances.push(this._meshInstance); this._colorUniform[0] = this._color.r; this._colorUniform[1] = this._color.g; this._colorUniform[2] = this._color.b; this._meshInstance.setParameter(PARAM_EMISSIVE, this._colorUniform); this._meshInstance.setParameter(PARAM_OPACITY, this._color.a); if (this.enabled && this.entity.enabled) { this._showModel(); } } if (this._meshInstance.material !== material) { this._meshInstance.material = material; } if (this._meshInstance.mesh !== mesh) { this._meshInstance.mesh = mesh; this._meshInstance.visible = true; this._meshInstance._aabbVer = -1; } if (this.sprite.atlas && this.sprite.atlas.texture) { this._meshInstance.setParameter(PARAM_EMISSIVE_MAP, this.sprite.atlas.texture); this._meshInstance.setParameter(PARAM_OPACITY_MAP, this.sprite.atlas.texture); } else { this._meshInstance.deleteParameter(PARAM_EMISSIVE_MAP); this._meshInstance.deleteParameter(PARAM_OPACITY_MAP); } if (this.sprite.atlas && (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === SPRITE_RENDERMODE_TILED)) { this._meshInstance._updateAabbFunc = this._updateAabbFunc; const frameData = this.sprite.atlas.frames[this.sprite.frameKeys[frame]]; if (frameData) { const borderWidthScale = 2 / frameData.rect.z; const borderHeightScale = 2 / frameData.rect.w; this._innerOffset.set(frameData.border.x * borderWidthScale, frameData.border.y * borderHeightScale, frameData.border.z * borderWidthScale, frameData.border.w * borderHeightScale); const tex = this.sprite.atlas.texture; this._atlasRect.set(frameData.rect.x / tex.width, frameData.rect.y / tex.height, frameData.rect.z / tex.width, frameData.rect.w / tex.height); } else { this._innerOffset.set(0, 0, 0, 0); } this._innerOffsetUniform[0] = this._innerOffset.x; this._innerOffsetUniform[1] = this._innerOffset.y; this._innerOffsetUniform[2] = this._innerOffset.z; this._innerOffsetUniform[3] = this._innerOffset.w; this._meshInstance.setParameter(PARAM_INNER_OFFSET, this._innerOffsetUniform); this._atlasRectUniform[0] = this._atlasRect.x; this._atlasRectUniform[1] = this._atlasRect.y; this._atlasRectUniform[2] = this._atlasRect.z; this._atlasRectUniform[3] = this._atlasRect.w; this._meshInstance.setParameter(PARAM_ATLAS_RECT, this._atlasRectUniform); } else { this._meshInstance._updateAabbFunc = null; } this._updateTransform(); } _updateTransform() { let scaleX = this.flipX ? -1 : 1; let scaleY = this.flipY ? -1 : 1; let posX = 0; let posY = 0; if (this.sprite && (this.sprite.renderMode === SPRITE_RENDERMODE_SLICED || this.sprite.renderMode === SPRITE_RENDERMODE_TILED)) { let w = 1; let h = 1; if (this.sprite.atlas) { const frameData = this.sprite.atlas.frames[this.sprite.frameKeys[this.frame]]; if (frameData) { w = frameData.rect.z; h = frameData.rect.w; posX = (0.5 - frameData.pivot.x) * this._width; posY = (0.5 - frameData.pivot.y) * this._height; } } const scaleMulX = w / this.sprite.pixelsPerUnit; const scaleMulY = h / this.sprite.pixelsPerUnit; this._outerScale.set(Math.max(this._width, this._innerOffset.x * scaleMulX), Math.max(this._height, this._innerOffset.y * scaleMulY)); scaleX *= scaleMulX; scaleY *= scaleMulY; this._outerScale.x /= scaleMulX; this._outerScale.y /= scaleMulY; scaleX *= math.clamp(this._width / (this._innerOffset.x * scaleMulX), 0.0001, 1); scaleY *= math.clamp(this._height / (this._innerOffset.y * scaleMulY), 0.0001, 1); if (this._meshInstance) { this._outerScaleUniform[0] = this._outerScale.x; this._outerScaleUniform[1] = this._outerScale.y; this._meshInstance.setParameter(PARAM_OUTER_SCALE, this._outerScaleUniform); } } this._node.setLocalScale(scaleX, scaleY, 1); this._node.setLocalPosition(posX, posY, 0); } _updateAabb(aabb) { aabb.center.set(0, 0, 0); aabb.halfExtents.set(this._outerScale.x * 0.5, this._outerScale.y * 0.5, 0.001); aabb.setFromTransformedAabb(aabb, this._node.getWorldTransform()); return aabb; } _tryAutoPlay() { if (!this._autoPlayClip) return; if (this.type !== SPRITETYPE_ANIMATED) return; const clip = this._clips[this._autoPlayClip]; if (clip && !clip.isPlaying && (!this._currentClip || !this._currentClip.isPlaying)) { if (this.enabled && this.entity.enabled) { this.play(clip.name); } } } _onLayersChanged(oldComp, newComp) { oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); if (this.enabled && this.entity.enabled) { this._showModel(); } } _onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; if (this._addedModel && this.enabled && this.entity.enabled && this._meshInstance) { layer.addMeshInstances([ this._meshInstance ]); } } _onLayerRemoved(layer) { if (!this._meshInstance) return; const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.removeMeshInstances([ this._meshInstance ]); } removeModelFromLayers() { for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (!layer) continue; layer.removeMeshInstances([ this._meshInstance ]); } } addClip(data) { const clip = new SpriteAnimationClip(this, { name: data.name, fps: data.fps, loop: data.loop, spriteAsset: data.spriteAsset }); this._clips[data.name] = clip; if (clip.name && clip.name === this._autoPlayClip) { this._tryAutoPlay(); } return clip; } removeClip(name) { delete this._clips[name]; } clip(name) { return this._clips[name]; } play(name) { const clip = this._clips[name]; const current = this._currentClip; if (current && current !== clip) { current._playing = false; } this._currentClip = clip; if (this._currentClip) { this._currentClip = clip; this._currentClip.play(); } return clip; } pause() { if (this._currentClip === this._defaultClip) return; if (this._currentClip.isPlaying) { this._currentClip.pause(); } } resume() { if (this._currentClip === this._defaultClip) return; if (this._currentClip.isPaused) { this._currentClip.resume(); } } stop() { if (this._currentClip === this._defaultClip) return; this._currentClip.stop(); } constructor(system, entity){ super(system, entity), this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null; this._type = SPRITETYPE_SIMPLE; this._material = system.defaultMaterial; this._color = new Color(1, 1, 1, 1); this._colorUniform = new Float32Array(3); this._speed = 1; this._flipX = false; this._flipY = false; this._width = 1; this._height = 1; this._drawOrder = 0; this._layers = [ LAYERID_WORLD ]; this._outerScale = new Vec2(1, 1); this._outerScaleUniform = new Float32Array(2); this._innerOffset = new Vec4(); this._innerOffsetUniform = new Float32Array(4); this._atlasRect = new Vec4(); this._atlasRectUniform = new Float32Array(4); this._batchGroupId = -1; this._batchGroup = null; this._node = new GraphNode(); this._model = new Model(); this._model.graph = this._node; this._meshInstance = null; entity.addChild(this._model.graph); this._model._entity = entity; this._updateAabbFunc = this._updateAabb.bind(this); this._addedModel = false; this._autoPlayClip = null; this._clips = {}; this._defaultClip = new SpriteAnimationClip(this, { name: this.entity.name, fps: 0, loop: false, spriteAsset: null }); this._currentClip = this._defaultClip; } } SpriteComponent.EVENT_PLAY = 'play'; SpriteComponent.EVENT_PAUSE = 'pause'; SpriteComponent.EVENT_RESUME = 'resume'; SpriteComponent.EVENT_STOP = 'stop'; SpriteComponent.EVENT_END = 'end'; SpriteComponent.EVENT_LOOP = 'loop'; class SpriteComponentData { constructor(){ this.enabled = true; } } const _schema$3 = [ 'enabled' ]; class SpriteComponentSystem extends ComponentSystem { set defaultMaterial(material) { this._defaultMaterial = material; } get defaultMaterial() { if (!this._defaultMaterial) { const texture = new Texture(this.app.graphicsDevice, { width: 1, height: 1, format: PIXELFORMAT_SRGBA8, name: 'sprite' }); const pixels = new Uint8Array(texture.lock()); pixels[0] = pixels[1] = pixels[2] = pixels[3] = 255; texture.unlock(); const material = new StandardMaterial(); material.diffuse.set(0, 0, 0); material.emissive.set(1, 1, 1); material.emissiveMap = texture; material.opacityMap = texture; material.opacityMapChannel = 'a'; material.useLighting = false; material.useTonemap = false; material.useFog = false; material.useSkybox = false; material.blendType = BLEND_PREMULTIPLIED; material.depthWrite = false; material.pixelSnap = false; material.cull = CULLFACE_NONE; material.update(); this._defaultTexture = texture; this._defaultMaterial = material; } return this._defaultMaterial; } set default9SlicedMaterialSlicedMode(material) { this._default9SlicedMaterialSlicedMode = material; } get default9SlicedMaterialSlicedMode() { if (!this._default9SlicedMaterialSlicedMode) { const material = this.defaultMaterial.clone(); material.nineSlicedMode = SPRITE_RENDERMODE_SLICED; material.update(); this._default9SlicedMaterialSlicedMode = material; } return this._default9SlicedMaterialSlicedMode; } set default9SlicedMaterialTiledMode(material) { this._default9SlicedMaterialTiledMode = material; } get default9SlicedMaterialTiledMode() { if (!this._default9SlicedMaterialTiledMode) { const material = this.defaultMaterial.clone(); material.nineSlicedMode = SPRITE_RENDERMODE_TILED; material.update(); this._default9SlicedMaterialTiledMode = material; } return this._default9SlicedMaterialTiledMode; } destroy() { super.destroy(); this.app.systems.off('update', this.onUpdate, this); if (this._defaultTexture) { this._defaultTexture.destroy(); this._defaultTexture = null; } } initializeComponentData(component, data, properties) { if (data.enabled !== undefined) { component.enabled = data.enabled; } component.type = data.type; if (data.layers && Array.isArray(data.layers)) { component.layers = data.layers.slice(0); } if (data.drawOrder !== undefined) { component.drawOrder = data.drawOrder; } if (data.color !== undefined) { if (data.color instanceof Color) { component.color.set(data.color.r, data.color.g, data.color.b, data.opacity ?? 1); } else { component.color.set(data.color[0], data.color[1], data.color[2], data.opacity ?? 1); } component.color = component.color; } if (data.opacity !== undefined) { component.opacity = data.opacity; } if (data.flipX !== undefined) { component.flipX = data.flipX; } if (data.flipY !== undefined) { component.flipY = data.flipY; } if (data.width !== undefined) { component.width = data.width; } if (data.height !== undefined) { component.height = data.height; } if (data.spriteAsset !== undefined) { component.spriteAsset = data.spriteAsset; } if (data.sprite) { component.sprite = data.sprite; } if (data.frame !== undefined) { component.frame = data.frame; } if (data.clips) { for(const name in data.clips){ component.addClip(data.clips[name]); } } if (data.speed !== undefined) { component.speed = data.speed; } if (data.autoPlayClip) { component.autoPlayClip = data.autoPlayClip; } component.batchGroupId = data.batchGroupId === undefined || data.batchGroupId === null ? -1 : data.batchGroupId; super.initializeComponentData(component, data, properties); } cloneComponent(entity, clone) { const source = entity.sprite; return this.addComponent(clone, { enabled: source.enabled, type: source.type, spriteAsset: source.spriteAsset, sprite: source.sprite, width: source.width, height: source.height, frame: source.frame, color: source.color.clone(), opacity: source.opacity, flipX: source.flipX, flipY: source.flipY, speed: source.speed, clips: source.clips, autoPlayClip: source.autoPlayClip, batchGroupId: source.batchGroupId, drawOrder: source.drawOrder, layers: source.layers.slice(0) }); } onUpdate(dt) { const components = this.store; for(const id in components){ if (components.hasOwnProperty(id)) { const component = components[id]; if (component.data.enabled && component.entity.enabled) { const sprite = component.entity.sprite; if (sprite._currentClip) { sprite._currentClip._update(dt); } } } } } onBeforeRemove(entity, component) { component.onDestroy(); } constructor(app){ super(app); this.id = 'sprite'; this.ComponentType = SpriteComponent; this.DataType = SpriteComponentData; this.schema = _schema$3; this._defaultTexture = null; this._defaultMaterial = null; this._default9SlicedMaterialSlicedMode = null; this._default9SlicedMaterialTiledMode = null; this.app.systems.on('update', this.onUpdate, this); this.on('beforeremove', this.onBeforeRemove, this); } } Component._buildAccessors(SpriteComponent.prototype, _schema$3); class ZoneComponent extends Component { set size(data) { if (data instanceof Vec3) { this._size.copy(data); } else if (data instanceof Array && data.length >= 3) { this.size.set(data[0], data[1], data[2]); } } get size() { return this._size; } onEnable() { this._checkState(); } onDisable() { this._checkState(); } _onSetEnabled(prop, old, value) { this._checkState(); } _checkState() { const state = this.enabled && this.entity.enabled; if (state === this._oldState) { return; } this._oldState = state; this.fire('enable'); this.fire('state', this.enabled); } _onBeforeRemove() { this.fire('remove'); } constructor(system, entity){ super(system, entity); this._oldState = true; this._size = new Vec3(); this.on('set_enabled', this._onSetEnabled, this); } } ZoneComponent.EVENT_ENABLE = 'enable'; ZoneComponent.EVENT_DISABLE = 'disable'; ZoneComponent.EVENT_STATE = 'state'; ZoneComponent.EVENT_REMOVE = 'remove'; class ZoneComponentData { constructor(){ this.enabled = true; } } const _schema$2 = [ 'enabled' ]; class ZoneComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { component.enabled = data.hasOwnProperty('enabled') ? !!data.enabled : true; if (data.size) { if (data.size instanceof Vec3) { component.size.copy(data.size); } else if (data.size instanceof Array && data.size.length >= 3) { component.size.set(data.size[0], data.size[1], data.size[2]); } } } cloneComponent(entity, clone) { const data = { enabled: entity.zone.enabled, size: entity.zone.size }; return this.addComponent(clone, data); } _onBeforeRemove(entity, component) { component._onBeforeRemove(); } constructor(app){ super(app); this.id = 'zone'; this.ComponentType = ZoneComponent; this.DataType = ZoneComponentData; this.schema = _schema$2; this.on('beforeremove', this._onBeforeRemove, this); } } Component._buildAccessors(ZoneComponent.prototype, _schema$2); class PostEffectEntry { constructor(effect, inputTarget){ this.effect = effect; this.inputTarget = inputTarget; this.outputTarget = null; this.name = effect.constructor.name; } } class PostEffectQueue { _allocateColorBuffer(format, name) { const rect = this.camera.rect; const renderTarget = this.destinationRenderTarget; const device = this.app.graphicsDevice; const width = Math.floor(rect.z * (renderTarget?.width ?? device.width)); const height = Math.floor(rect.w * (renderTarget?.height ?? device.height)); const colorBuffer = new Texture(device, { name: name, format: format, width: width, height: height, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); return colorBuffer; } _createOffscreenTarget(useDepth, hdr) { const device = this.app.graphicsDevice; const outputRt = this.destinationRenderTarget ?? device.backBuffer; const srgb = outputRt.isColorBufferSrgb(0); const format = (hdr && device.getRenderableHdrFormat([ PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F ], true)) ?? (srgb ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8); const name = `${this.camera.entity.name}-posteffect-${this.effects.length}`; const colorBuffer = this._allocateColorBuffer(format, name); return new RenderTarget({ colorBuffer: colorBuffer, depth: useDepth, stencil: useDepth && this.app.graphicsDevice.supportsStencil, samples: useDepth ? device.samples : 1 }); } _resizeOffscreenTarget(rt) { const format = rt.colorBuffer.format; const name = rt.colorBuffer.name; rt.destroyFrameBuffers(); rt.destroyTextureBuffers(); rt._colorBuffer = this._allocateColorBuffer(format, name); rt._colorBuffers = [ rt._colorBuffer ]; rt.evaluateDimensions(); } _destroyOffscreenTarget(rt) { rt.destroyTextureBuffers(); rt.destroy(); } addEffect(effect) { const effects = this.effects; const isFirstEffect = effects.length === 0; const inputTarget = this._createOffscreenTarget(isFirstEffect, effect.hdr); const newEntry = new PostEffectEntry(effect, inputTarget); effects.push(newEntry); this._sourceTarget = newEntry.inputTarget; if (effects.length > 1) { effects[effects.length - 2].outputTarget = newEntry.inputTarget; } this._newPostEffect = effect; if (effect.needsDepthBuffer) { this._requestDepthMap(); } this.enable(); this._newPostEffect = undefined; } removeEffect(effect) { let index = -1; for(let i = 0, len = this.effects.length; i < len; i++){ if (this.effects[i].effect === effect) { index = i; break; } } if (index >= 0) { if (index > 0) { this.effects[index - 1].outputTarget = index + 1 < this.effects.length ? this.effects[index + 1].inputTarget : null; } else { if (this.effects.length > 1) { if (!this.effects[1].inputTarget._depth) { this._destroyOffscreenTarget(this.effects[1].inputTarget); this.effects[1].inputTarget = this._createOffscreenTarget(true, this.effects[1].hdr); this._sourceTarget = this.effects[1].inputTarget; } this.camera.renderTarget = this.effects[1].inputTarget; } } this._destroyOffscreenTarget(this.effects[index].inputTarget); this.effects.splice(index, 1); } if (this.enabled) { if (effect.needsDepthBuffer) { this._releaseDepthMap(); } } if (this.effects.length === 0) { this.disable(); } } _requestDepthMaps() { for(let i = 0, len = this.effects.length; i < len; i++){ const effect = this.effects[i].effect; if (this._newPostEffect === effect) { continue; } if (effect.needsDepthBuffer) { this._requestDepthMap(); } } } _releaseDepthMaps() { for(let i = 0, len = this.effects.length; i < len; i++){ const effect = this.effects[i].effect; if (effect.needsDepthBuffer) { this._releaseDepthMap(); } } } _requestDepthMap() { const depthLayer = this.app.scene.layers.getLayerById(LAYERID_DEPTH); if (depthLayer) { depthLayer.incrementCounter(); this.camera.requestSceneDepthMap(true); } } _releaseDepthMap() { const depthLayer = this.app.scene.layers.getLayerById(LAYERID_DEPTH); if (depthLayer) { depthLayer.decrementCounter(); this.camera.requestSceneDepthMap(false); } } destroy() { for(let i = 0, len = this.effects.length; i < len; i++){ this.effects[i].inputTarget.destroy(); } this.effects.length = 0; this.disable(); } enable() { if (!this.enabled && this.effects.length) { this.enabled = true; this._requestDepthMaps(); this.app.graphicsDevice.on('resizecanvas', this._onCanvasResized, this); this.destinationRenderTarget = this.camera.renderTarget; this.camera.renderTarget = this.effects[0].inputTarget; this.camera.onPostprocessing = ()=>{ if (this.enabled) { let rect = null; const len = this.effects.length; if (len) { for(let i = 0; i < len; i++){ const fx = this.effects[i]; let destTarget = fx.outputTarget; if (i === len - 1) { rect = this.camera.rect; if (this.destinationRenderTarget) { destTarget = this.destinationRenderTarget; } } fx.effect.render(fx.inputTarget, destTarget, rect); } } } }; } } disable() { if (this.enabled) { this.enabled = false; this.app.graphicsDevice.off('resizecanvas', this._onCanvasResized, this); this._releaseDepthMaps(); this._destroyOffscreenTarget(this._sourceTarget); this.camera.renderTarget = this.destinationRenderTarget; this.camera.onPostprocessing = null; } } _onCanvasResized(width, height) { const rect = this.camera.rect; const renderTarget = this.destinationRenderTarget; width = renderTarget?.width ?? width; height = renderTarget?.height ?? height; this.camera.camera.aspectRatio = width * rect.z / (height * rect.w); this.resizeRenderTargets(); } resizeRenderTargets() { const device = this.app.graphicsDevice; const renderTarget = this.destinationRenderTarget; const width = renderTarget?.width ?? device.width; const height = renderTarget?.height ?? device.height; const rect = this.camera.rect; const desiredWidth = Math.floor(rect.z * width); const desiredHeight = Math.floor(rect.w * height); const effects = this.effects; for(let i = 0, len = effects.length; i < len; i++){ const fx = effects[i]; if (fx.inputTarget.width !== desiredWidth || fx.inputTarget.height !== desiredHeight) { this._resizeOffscreenTarget(fx.inputTarget); } } } onCameraRectChanged(name, oldValue, newValue) { if (this.enabled) { this.resizeRenderTargets(); } } constructor(app, camera){ this.app = app; this.camera = camera; this.destinationRenderTarget = null; this.effects = []; this.enabled = false; this.depthTarget = null; camera.on('set:rect', this.onCameraRectChanged, this); } } class CameraComponent extends Component { setShaderPass(name) { const shaderPass = ShaderPass.get(this.system.app.graphicsDevice); const shaderPassInfo = name ? shaderPass.allocate(name, { isForward: true }) : null; this._camera.shaderPassInfo = shaderPassInfo; return shaderPassInfo.index; } getShaderPass() { return this._camera.shaderPassInfo?.name; } set renderPasses(passes) { this._camera.renderPasses = passes || []; this.dirtyLayerCompositionCameras(); this.system.app.scene.updateShaders = true; } get renderPasses() { return this._camera.renderPasses; } get shaderParams() { return this._camera.shaderParams; } set gammaCorrection(value) { this.camera.shaderParams.gammaCorrection = value; } get gammaCorrection() { return this.camera.shaderParams.gammaCorrection; } set toneMapping(value) { this.camera.shaderParams.toneMapping = value; } get toneMapping() { return this.camera.shaderParams.toneMapping; } set fog(value) { this._camera.fogParams = value; } get fog() { return this._camera.fogParams; } set aperture(value) { this._camera.aperture = value; } get aperture() { return this._camera.aperture; } set aspectRatio(value) { this._camera.aspectRatio = value; } get aspectRatio() { return this._camera.aspectRatio; } set aspectRatioMode(value) { this._camera.aspectRatioMode = value; } get aspectRatioMode() { return this._camera.aspectRatioMode; } set calculateProjection(value) { this._camera.calculateProjection = value; } get calculateProjection() { return this._camera.calculateProjection; } set calculateTransform(value) { this._camera.calculateTransform = value; } get calculateTransform() { return this._camera.calculateTransform; } get camera() { return this._camera; } set clearColor(value) { this._camera.clearColor = value; } get clearColor() { return this._camera.clearColor; } set clearColorBuffer(value) { this._camera.clearColorBuffer = value; this.dirtyLayerCompositionCameras(); } get clearColorBuffer() { return this._camera.clearColorBuffer; } set clearDepth(value) { this._camera.clearDepth = value; } get clearDepth() { return this._camera.clearDepth; } set clearDepthBuffer(value) { this._camera.clearDepthBuffer = value; this.dirtyLayerCompositionCameras(); } get clearDepthBuffer() { return this._camera.clearDepthBuffer; } set clearStencilBuffer(value) { this._camera.clearStencilBuffer = value; this.dirtyLayerCompositionCameras(); } get clearStencilBuffer() { return this._camera.clearStencilBuffer; } set cullFaces(value) { this._camera.cullFaces = value; } get cullFaces() { return this._camera.cullFaces; } set disablePostEffectsLayer(layer) { this._disablePostEffectsLayer = layer; this.dirtyLayerCompositionCameras(); } get disablePostEffectsLayer() { return this._disablePostEffectsLayer; } set farClip(value) { this._camera.farClip = value; } get farClip() { return this._camera.farClip; } set flipFaces(value) { this._camera.flipFaces = value; } get flipFaces() { return this._camera.flipFaces; } set fov(value) { this._camera.fov = value; } get fov() { return this._camera.fov; } get frustum() { return this._camera.frustum; } set frustumCulling(value) { this._camera.frustumCulling = value; } get frustumCulling() { return this._camera.frustumCulling; } set horizontalFov(value) { this._camera.horizontalFov = value; } get horizontalFov() { return this._camera.horizontalFov; } set layers(newValue) { const oldLayers = this._camera.layers; const scene = this.system.app.scene; oldLayers.forEach((layerId)=>{ const layer = scene.layers.getLayerById(layerId); layer?.removeCamera(this); }); this._camera.layers = newValue; if (this.enabled && this.entity.enabled) { newValue.forEach((layerId)=>{ const layer = scene.layers.getLayerById(layerId); layer?.addCamera(this); }); } this.fire('set:layers'); } get layers() { return this._camera.layers; } get layersSet() { return this._camera.layersSet; } set jitter(value) { this._camera.jitter = value; } get jitter() { return this._camera.jitter; } set nearClip(value) { this._camera.nearClip = value; } get nearClip() { return this._camera.nearClip; } set orthoHeight(value) { this._camera.orthoHeight = value; } get orthoHeight() { return this._camera.orthoHeight; } get postEffects() { return this._postEffects; } get postEffectsEnabled() { return this._postEffects.enabled; } set priority(newValue) { this._priority = newValue; this.dirtyLayerCompositionCameras(); } get priority() { return this._priority; } set projection(value) { this._camera.projection = value; } get projection() { return this._camera.projection; } get projectionMatrix() { return this._camera.projectionMatrix; } set rect(value) { this._camera.rect = value; this.fire('set:rect', this._camera.rect); } get rect() { return this._camera.rect; } set renderSceneColorMap(value) { if (value && !this._sceneColorMapRequested) { this.requestSceneColorMap(true); this._sceneColorMapRequested = true; } else if (this._sceneColorMapRequested) { this.requestSceneColorMap(false); this._sceneColorMapRequested = false; } } get renderSceneColorMap() { return this._renderSceneColorMap > 0; } set renderSceneDepthMap(value) { if (value && !this._sceneDepthMapRequested) { this.requestSceneDepthMap(true); this._sceneDepthMapRequested = true; } else if (this._sceneDepthMapRequested) { this.requestSceneDepthMap(false); this._sceneDepthMapRequested = false; } } get renderSceneDepthMap() { return this._renderSceneDepthMap > 0; } set renderTarget(value) { this._camera.renderTarget = value; this.dirtyLayerCompositionCameras(); } get renderTarget() { return this._camera.renderTarget; } set scissorRect(value) { this._camera.scissorRect = value; } get scissorRect() { return this._camera.scissorRect; } set sensitivity(value) { this._camera.sensitivity = value; } get sensitivity() { return this._camera.sensitivity; } set shutter(value) { this._camera.shutter = value; } get shutter() { return this._camera.shutter; } get viewMatrix() { return this._camera.viewMatrix; } _enableDepthLayer(value) { const hasDepthLayer = this.layers.find((layerId)=>layerId === LAYERID_DEPTH); if (hasDepthLayer) { const depthLayer = this.system.app.scene.layers.getLayerById(LAYERID_DEPTH); if (value) { depthLayer?.incrementCounter(); } else { depthLayer?.decrementCounter(); } } else if (value) { return false; } return true; } requestSceneColorMap(enabled) { this._renderSceneColorMap += enabled ? 1 : -1; this._enableDepthLayer(enabled); this.camera._enableRenderPassColorGrab(this.system.app.graphicsDevice, this.renderSceneColorMap); this.system.app.scene.layers.markDirty(); } requestSceneDepthMap(enabled) { this._renderSceneDepthMap += enabled ? 1 : -1; this._enableDepthLayer(enabled); this.camera._enableRenderPassDepthGrab(this.system.app.graphicsDevice, this.system.app.renderer, this.renderSceneDepthMap); this.system.app.scene.layers.markDirty(); } dirtyLayerCompositionCameras() { const layerComp = this.system.app.scene.layers; layerComp._dirty = true; } screenToWorld(screenx, screeny, cameraz, worldCoord) { const device = this.system.app.graphicsDevice; const { width, height } = device.clientRect; return this._camera.screenToWorld(screenx, screeny, cameraz, width, height, worldCoord); } worldToScreen(worldCoord, screenCoord) { const device = this.system.app.graphicsDevice; const { width, height } = device.clientRect; return this._camera.worldToScreen(worldCoord, width, height, screenCoord); } onAppPrerender() { this._camera._viewMatDirty = true; this._camera._viewProjMatDirty = true; } addCameraToLayers() { const layers = this.layers; for(let i = 0; i < layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(layers[i]); if (layer) { layer.addCamera(this); } } } removeCameraFromLayers() { const layers = this.layers; for(let i = 0; i < layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(layers[i]); if (layer) { layer.removeCamera(this); } } } onLayersChanged(oldComp, newComp) { this.addCameraToLayers(); oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.addCamera(this); } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; layer.removeCamera(this); } onEnable() { const scene = this.system.app.scene; const layers = scene.layers; this.system.addCamera(this); this._evtLayersChanged?.off(); this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved?.off(); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this.enabled && this.entity.enabled) { this.addCameraToLayers(); } this.postEffects.enable(); } onDisable() { const scene = this.system.app.scene; const layers = scene.layers; this.postEffects.disable(); this.removeCameraFromLayers(); this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } this.system.removeCamera(this); } onRemove() { this.onDisable(); this.off(); this.camera.destroy(); } calculateAspectRatio(rt) { const device = this.system.app.graphicsDevice; const width = rt ? rt.width : device.width; const height = rt ? rt.height : device.height; return width * this.rect.z / (height * this.rect.w); } frameUpdate(rt) { if (this.aspectRatioMode === ASPECT_AUTO) { this.aspectRatio = this.calculateAspectRatio(rt); } } startXr(type, spaceType, options) { this.system.app.xr.start(this, type, spaceType, options); } endXr(callback) { if (!this._camera.xr) { if (callback) callback(new Error('Camera is not in XR')); return; } this._camera.xr.end(callback); } copy(source) { this.aperture = source.aperture; this.aspectRatio = source.aspectRatio; this.aspectRatioMode = source.aspectRatioMode; this.calculateProjection = source.calculateProjection; this.calculateTransform = source.calculateTransform; this.clearColor = source.clearColor; this.clearColorBuffer = source.clearColorBuffer; this.clearDepthBuffer = source.clearDepthBuffer; this.clearStencilBuffer = source.clearStencilBuffer; this.cullFaces = source.cullFaces; this.disablePostEffectsLayer = source.disablePostEffectsLayer; this.farClip = source.farClip; this.flipFaces = source.flipFaces; this.fov = source.fov; this.frustumCulling = source.frustumCulling; this.horizontalFov = source.horizontalFov; this.layers = source.layers; this.nearClip = source.nearClip; this.orthoHeight = source.orthoHeight; this.priority = source.priority; this.projection = source.projection; this.rect = source.rect; this.renderTarget = source.renderTarget; this.scissorRect = source.scissorRect; this.sensitivity = source.sensitivity; this.shutter = source.shutter; } constructor(system, entity){ super(system, entity), this.onPostprocessing = null, this._renderSceneDepthMap = 0, this._renderSceneColorMap = 0, this._sceneDepthMapRequested = false, this._sceneColorMapRequested = false, this._priority = 0, this._disablePostEffectsLayer = LAYERID_UI, this._camera = new Camera$1(), this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null; this._camera.node = entity; this._postEffects = new PostEffectQueue(system.app, this); } } class CameraComponentData { constructor(){ this.enabled = true; } } const _schema$1 = [ 'enabled' ]; class CameraComponentSystem extends ComponentSystem { initializeComponentData(component, data, properties) { properties = [ 'aspectRatio', 'aspectRatioMode', 'calculateProjection', 'calculateTransform', 'clearColor', 'clearColorBuffer', 'clearDepth', 'clearDepthBuffer', 'clearStencilBuffer', 'renderSceneColorMap', 'renderSceneDepthMap', 'cullFaces', 'farClip', 'flipFaces', 'fog', 'fov', 'frustumCulling', 'horizontalFov', 'layers', 'renderTarget', 'nearClip', 'orthoHeight', 'projection', 'priority', 'rect', 'scissorRect', 'aperture', 'shutter', 'sensitivity', 'gammaCorrection', 'toneMapping' ]; for(let i = 0; i < properties.length; i++){ const property = properties[i]; if (data.hasOwnProperty(property)) { const value = data[property]; switch(property){ case 'rect': case 'scissorRect': if (Array.isArray(value)) { component[property] = new Vec4(value[0], value[1], value[2], value[3]); } else { component[property] = value; } break; case 'clearColor': if (Array.isArray(value)) { component[property] = new Color(value[0], value[1], value[2], value[3]); } else { component[property] = value; } break; default: component[property] = value; break; } } } super.initializeComponentData(component, data, [ 'enabled' ]); } cloneComponent(entity, clone) { const c = entity.camera; return this.addComponent(clone, { aspectRatio: c.aspectRatio, aspectRatioMode: c.aspectRatioMode, calculateProjection: c.calculateProjection, calculateTransform: c.calculateTransform, clearColor: c.clearColor, clearColorBuffer: c.clearColorBuffer, clearDepthBuffer: c.clearDepthBuffer, clearStencilBuffer: c.clearStencilBuffer, renderSceneDepthMap: c.renderSceneDepthMap, renderSceneColorMap: c.renderSceneColorMap, cullFaces: c.cullFaces, enabled: c.enabled, farClip: c.farClip, flipFaces: c.flipFaces, fov: c.fov, frustumCulling: c.frustumCulling, horizontalFov: c.horizontalFov, layers: c.layers, renderTarget: c.renderTarget, nearClip: c.nearClip, orthoHeight: c.orthoHeight, projection: c.projection, priority: c.priority, rect: c.rect, scissorRect: c.scissorRect, aperture: c.aperture, sensitivity: c.sensitivity, shutter: c.shutter, gammaCorrection: c.gammaCorrection, toneMapping: c.toneMapping }); } onBeforeRemove(entity, component) { this.removeCamera(component); component.onRemove(); } onAppPrerender() { for(let i = 0, len = this.cameras.length; i < len; i++){ this.cameras[i].onAppPrerender(); } } addCamera(camera) { this.cameras.push(camera); sortPriority(this.cameras); } removeCamera(camera) { const index = this.cameras.indexOf(camera); if (index >= 0) { this.cameras.splice(index, 1); sortPriority(this.cameras); } } destroy() { this.app.off('prerender', this.onAppPrerender, this); super.destroy(); } constructor(app){ super(app), this.cameras = []; this.id = 'camera'; this.ComponentType = CameraComponent; this.DataType = CameraComponentData; this.schema = _schema$1; this.on('beforeremove', this.onBeforeRemove, this); this.app.on('prerender', this.onAppPrerender, this); } } Component._buildAccessors(CameraComponent.prototype, _schema$1); class LightComponentData { constructor(){ this.enabled = true; this.type = 'directional'; this.color = new Color(1, 1, 1); this.intensity = 1; this.luminance = 0; this.shape = LIGHTSHAPE_PUNCTUAL; this.affectSpecularity = true; this.castShadows = false; this.shadowDistance = 40; this.shadowIntensity = 1; this.shadowResolution = 1024; this.shadowBias = 0.05; this.numCascades = 1; this.cascadeBlend = 0; this.bakeNumSamples = 1; this.bakeArea = 0; this.cascadeDistribution = 0.5; this.normalOffsetBias = 0; this.range = 10; this.innerConeAngle = 40; this.outerConeAngle = 45; this.falloffMode = LIGHTFALLOFF_LINEAR; this.shadowType = SHADOW_PCF3_32F; this.vsmBlurSize = 11; this.vsmBlurMode = BLUR_GAUSSIAN; this.vsmBias = 0.01 * 0.25; this.cookieAsset = null; this.cookie = null; this.cookieIntensity = 1; this.cookieFalloff = true; this.cookieChannel = 'rgb'; this.cookieAngle = 0; this.cookieScale = null; this.cookieOffset = null; this.shadowUpdateMode = SHADOWUPDATE_REALTIME; this.mask = 1; this.affectDynamic = true; this.affectLightmapped = false; this.bake = false; this.bakeDir = true; this.isStatic = false; this.layers = [ LAYERID_WORLD ]; this.penumbraSize = 1; this.penumbraFalloff = 1; this.shadowSamples = 16; this.shadowBlockerSamples = 16; } } const properties = Object.keys(new LightComponentData()); class LightComponent extends Component { get data() { const record = this.system.store[this.entity.getGuid()]; return record ? record.data : null; } set enabled(arg) { this._setValue('enabled', arg, function(newValue, oldValue) { this.onSetEnabled(null, oldValue, newValue); }); } get enabled() { return this.data.enabled; } set light(arg) { this._setValue('light', arg); } get light() { return this.data.light; } set type(arg) { this._setValue('type', arg, function(newValue, oldValue) { this.system.changeType(this, oldValue, newValue); this.refreshProperties(); }); } get type() { return this.data.type; } set color(arg) { this._setValue('color', arg, function(newValue, oldValue) { this.light.setColor(newValue); }, true); } get color() { return this.data.color; } set intensity(arg) { this._setValue('intensity', arg, function(newValue, oldValue) { this.light.intensity = newValue; }); } get intensity() { return this.data.intensity; } set luminance(arg) { this._setValue('luminance', arg, function(newValue, oldValue) { this.light.luminance = newValue; }); } get luminance() { return this.data.luminance; } set shape(arg) { this._setValue('shape', arg, function(newValue, oldValue) { this.light.shape = newValue; }); } get shape() { return this.data.shape; } set affectSpecularity(arg) { this._setValue('affectSpecularity', arg, function(newValue, oldValue) { this.light.affectSpecularity = newValue; }); } get affectSpecularity() { return this.data.affectSpecularity; } set castShadows(arg) { this._setValue('castShadows', arg, function(newValue, oldValue) { this.light.castShadows = newValue; }); } get castShadows() { return this.data.castShadows; } set shadowDistance(arg) { this._setValue('shadowDistance', arg, function(newValue, oldValue) { this.light.shadowDistance = newValue; }); } get shadowDistance() { return this.data.shadowDistance; } set shadowIntensity(arg) { this._setValue('shadowIntensity', arg, function(newValue, oldValue) { this.light.shadowIntensity = newValue; }); } get shadowIntensity() { return this.data.shadowIntensity; } set shadowResolution(arg) { this._setValue('shadowResolution', arg, function(newValue, oldValue) { this.light.shadowResolution = newValue; }); } get shadowResolution() { return this.data.shadowResolution; } set shadowBias(arg) { this._setValue('shadowBias', arg, function(newValue, oldValue) { this.light.shadowBias = -0.01 * math.clamp(newValue, 0, 1); }); } get shadowBias() { return this.data.shadowBias; } set numCascades(arg) { this._setValue('numCascades', arg, function(newValue, oldValue) { this.light.numCascades = math.clamp(Math.floor(newValue), 1, 4); }); } get numCascades() { return this.data.numCascades; } set cascadeBlend(value) { this._setValue('cascadeBlend', value, function(newValue, oldValue) { this.light.cascadeBlend = math.clamp(newValue, 0, 1); }); } get cascadeBlend() { return this.data.cascadeBlend; } set bakeNumSamples(arg) { this._setValue('bakeNumSamples', arg, function(newValue, oldValue) { this.light.bakeNumSamples = math.clamp(Math.floor(newValue), 1, 255); }); } get bakeNumSamples() { return this.data.bakeNumSamples; } set bakeArea(arg) { this._setValue('bakeArea', arg, function(newValue, oldValue) { this.light.bakeArea = math.clamp(newValue, 0, 180); }); } get bakeArea() { return this.data.bakeArea; } set cascadeDistribution(arg) { this._setValue('cascadeDistribution', arg, function(newValue, oldValue) { this.light.cascadeDistribution = math.clamp(newValue, 0, 1); }); } get cascadeDistribution() { return this.data.cascadeDistribution; } set normalOffsetBias(arg) { this._setValue('normalOffsetBias', arg, function(newValue, oldValue) { this.light.normalOffsetBias = math.clamp(newValue, 0, 1); }); } get normalOffsetBias() { return this.data.normalOffsetBias; } set range(arg) { this._setValue('range', arg, function(newValue, oldValue) { this.light.attenuationEnd = newValue; }); } get range() { return this.data.range; } set innerConeAngle(arg) { this._setValue('innerConeAngle', arg, function(newValue, oldValue) { this.light.innerConeAngle = newValue; }); } get innerConeAngle() { return this.data.innerConeAngle; } set outerConeAngle(arg) { this._setValue('outerConeAngle', arg, function(newValue, oldValue) { this.light.outerConeAngle = newValue; }); } get outerConeAngle() { return this.data.outerConeAngle; } set falloffMode(arg) { this._setValue('falloffMode', arg, function(newValue, oldValue) { this.light.falloffMode = newValue; }); } get falloffMode() { return this.data.falloffMode; } set shadowType(arg) { this._setValue('shadowType', arg, function(newValue, oldValue) { this.light.shadowType = newValue; }); } get shadowType() { return this.data.shadowType; } set vsmBlurSize(arg) { this._setValue('vsmBlurSize', arg, function(newValue, oldValue) { this.light.vsmBlurSize = newValue; }); } get vsmBlurSize() { return this.data.vsmBlurSize; } set vsmBlurMode(arg) { this._setValue('vsmBlurMode', arg, function(newValue, oldValue) { this.light.vsmBlurMode = newValue; }); } get vsmBlurMode() { return this.data.vsmBlurMode; } set vsmBias(arg) { this._setValue('vsmBias', arg, function(newValue, oldValue) { this.light.vsmBias = math.clamp(newValue, 0, 1); }); } get vsmBias() { return this.data.vsmBias; } set cookieAsset(arg) { this._setValue('cookieAsset', arg, function(newValue, oldValue) { if (this._cookieAssetId && (newValue instanceof Asset && newValue.id === this._cookieAssetId || newValue === this._cookieAssetId)) { return; } this.onCookieAssetRemove(); this._cookieAssetId = null; if (newValue instanceof Asset) { this.data.cookieAsset = newValue.id; this._cookieAssetId = newValue.id; this.onCookieAssetAdd(newValue); } else if (typeof newValue === 'number') { this._cookieAssetId = newValue; const asset = this.system.app.assets.get(newValue); if (asset) { this.onCookieAssetAdd(asset); } else { this._cookieAssetAdd = true; this.system.app.assets.on(`add:${this._cookieAssetId}`, this.onCookieAssetAdd, this); } } }); } get cookieAsset() { return this.data.cookieAsset; } set cookie(arg) { this._setValue('cookie', arg, function(newValue, oldValue) { this.light.cookie = newValue; }); } get cookie() { return this.data.cookie; } set cookieIntensity(arg) { this._setValue('cookieIntensity', arg, function(newValue, oldValue) { this.light.cookieIntensity = math.clamp(newValue, 0, 1); }); } get cookieIntensity() { return this.data.cookieIntensity; } set cookieFalloff(arg) { this._setValue('cookieFalloff', arg, function(newValue, oldValue) { this.light.cookieFalloff = newValue; }); } get cookieFalloff() { return this.data.cookieFalloff; } set cookieChannel(arg) { this._setValue('cookieChannel', arg, function(newValue, oldValue) { this.light.cookieChannel = newValue; }); } get cookieChannel() { return this.data.cookieChannel; } set cookieAngle(arg) { this._setValue('cookieAngle', arg, function(newValue, oldValue) { if (newValue !== 0 || this.cookieScale !== null) { if (!this._cookieMatrix) this._cookieMatrix = new Vec4(); let scx = 1; let scy = 1; if (this.cookieScale) { scx = this.cookieScale.x; scy = this.cookieScale.y; } const c = Math.cos(newValue * math.DEG_TO_RAD); const s = Math.sin(newValue * math.DEG_TO_RAD); this._cookieMatrix.set(c / scx, -s / scx, s / scy, c / scy); this.light.cookieTransform = this._cookieMatrix; } else { this.light.cookieTransform = null; } }); } get cookieAngle() { return this.data.cookieAngle; } set cookieScale(arg) { this._setValue('cookieScale', arg, function(newValue, oldValue) { if (newValue !== null || this.cookieAngle !== 0) { if (!this._cookieMatrix) this._cookieMatrix = new Vec4(); const scx = newValue.x; const scy = newValue.y; const c = Math.cos(this.cookieAngle * math.DEG_TO_RAD); const s = Math.sin(this.cookieAngle * math.DEG_TO_RAD); this._cookieMatrix.set(c / scx, -s / scx, s / scy, c / scy); this.light.cookieTransform = this._cookieMatrix; } else { this.light.cookieTransform = null; } }, true); } get cookieScale() { return this.data.cookieScale; } set cookieOffset(arg) { this._setValue('cookieOffset', arg, function(newValue, oldValue) { this.light.cookieOffset = newValue; }, true); } get cookieOffset() { return this.data.cookieOffset; } set shadowUpdateMode(arg) { this._setValue('shadowUpdateMode', arg, function(newValue, oldValue) { this.light.shadowUpdateMode = newValue; }, true); } get shadowUpdateMode() { return this.data.shadowUpdateMode; } set mask(arg) { this._setValue('mask', arg, function(newValue, oldValue) { this.light.mask = newValue; }); } get mask() { return this.data.mask; } set affectDynamic(arg) { this._setValue('affectDynamic', arg, function(newValue, oldValue) { if (newValue) { this.light.mask |= MASK_AFFECT_DYNAMIC; } else { this.light.mask &= ~MASK_AFFECT_DYNAMIC; } this.light.layersDirty(); }); } get affectDynamic() { return this.data.affectDynamic; } set affectLightmapped(arg) { this._setValue('affectLightmapped', arg, function(newValue, oldValue) { if (newValue) { this.light.mask |= MASK_AFFECT_LIGHTMAPPED; if (this.bake) this.light.mask &= ~MASK_BAKE; } else { this.light.mask &= ~MASK_AFFECT_LIGHTMAPPED; if (this.bake) this.light.mask |= MASK_BAKE; } }); } get affectLightmapped() { return this.data.affectLightmapped; } set bake(arg) { this._setValue('bake', arg, function(newValue, oldValue) { if (newValue) { this.light.mask |= MASK_BAKE; if (this.affectLightmapped) this.light.mask &= ~MASK_AFFECT_LIGHTMAPPED; } else { this.light.mask &= ~MASK_BAKE; if (this.affectLightmapped) this.light.mask |= MASK_AFFECT_LIGHTMAPPED; } this.light.layersDirty(); }); } get bake() { return this.data.bake; } set bakeDir(arg) { this._setValue('bakeDir', arg, function(newValue, oldValue) { this.light.bakeDir = newValue; }); } get bakeDir() { return this.data.bakeDir; } set isStatic(arg) { this._setValue('isStatic', arg, function(newValue, oldValue) { this.light.isStatic = newValue; }); } get isStatic() { return this.data.isStatic; } set layers(arg) { this._setValue('layers', arg, function(newValue, oldValue) { for(let i = 0; i < oldValue.length; i++){ const layer = this.system.app.scene.layers.getLayerById(oldValue[i]); if (!layer) continue; layer.removeLight(this); this.light.removeLayer(layer); } for(let i = 0; i < newValue.length; i++){ const layer = this.system.app.scene.layers.getLayerById(newValue[i]); if (!layer) continue; if (this.enabled && this.entity.enabled) { layer.addLight(this); this.light.addLayer(layer); } } }); } get layers() { return this.data.layers; } set shadowUpdateOverrides(values) { this.light.shadowUpdateOverrides = values; } get shadowUpdateOverrides() { return this.light.shadowUpdateOverrides; } set shadowSamples(value) { this.light.shadowSamples = value; } get shadowSamples() { return this.light.shadowSamples; } set shadowBlockerSamples(value) { this.light.shadowBlockerSamples = value; } get shadowBlockerSamples() { return this.light.shadowBlockerSamples; } set penumbraSize(value) { this.light.penumbraSize = value; } get penumbraSize() { return this.light.penumbraSize; } set penumbraFalloff(value) { this.light.penumbraFalloff = value; } get penumbraFalloff() { return this.light.penumbraFalloff; } _setValue(name, value, setFunc, skipEqualsCheck) { const data = this.data; const oldValue = data[name]; if (!skipEqualsCheck && oldValue === value) return; data[name] = value; if (setFunc) setFunc.call(this, value, oldValue); } addLightToLayers() { for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (layer) { layer.addLight(this); this.light.addLayer(layer); } } } removeLightFromLayers() { for(let i = 0; i < this.layers.length; i++){ const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); if (layer) { layer.removeLight(this); this.light.removeLayer(layer); } } } onLayersChanged(oldComp, newComp) { if (this.enabled && this.entity.enabled) { this.addLightToLayers(); } oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index >= 0 && this.enabled && this.entity.enabled) { layer.addLight(this); this.light.addLayer(layer); } } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index >= 0) { layer.removeLight(this); this.light.removeLayer(layer); } } refreshProperties() { for(let i = 0; i < properties.length; i++){ const name = properties[i]; this[name] = this[name]; } if (this.enabled && this.entity.enabled) { this.onEnable(); } } onCookieAssetSet() { let forceLoad = false; if (this._cookieAsset.type === 'cubemap' && !this._cookieAsset.loadFaces) { this._cookieAsset.loadFaces = true; forceLoad = true; } if (!this._cookieAsset.resource || forceLoad) this.system.app.assets.load(this._cookieAsset); if (this._cookieAsset.resource) { this.onCookieAssetLoad(); } } onCookieAssetAdd(asset) { if (this._cookieAssetId !== asset.id) return; this._cookieAsset = asset; if (this.light.enabled) { this.onCookieAssetSet(); } this._cookieAsset.on('load', this.onCookieAssetLoad, this); this._cookieAsset.on('remove', this.onCookieAssetRemove, this); } onCookieAssetLoad() { if (!this._cookieAsset || !this._cookieAsset.resource) { return; } this.cookie = this._cookieAsset.resource; } onCookieAssetRemove() { if (!this._cookieAssetId) { return; } if (this._cookieAssetAdd) { this.system.app.assets.off(`add:${this._cookieAssetId}`, this.onCookieAssetAdd, this); this._cookieAssetAdd = false; } if (this._cookieAsset) { this._cookieAsset.off('load', this.onCookieAssetLoad, this); this._cookieAsset.off('remove', this.onCookieAssetRemove, this); this._cookieAsset = null; } this.cookie = null; } onEnable() { const scene = this.system.app.scene; const layers = scene.layers; this.light.enabled = true; this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this.enabled && this.entity.enabled) { this.addLightToLayers(); } if (this._cookieAsset && !this.cookie) { this.onCookieAssetSet(); } } onDisable() { const scene = this.system.app.scene; const layers = scene.layers; this.light.enabled = false; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } this.removeLightFromLayers(); } onRemove() { this.onDisable(); this.light.destroy(); this.cookieAsset = null; } constructor(...args){ super(...args), this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null, this._cookieAsset = null, this._cookieAssetId = null, this._cookieAssetAdd = false, this._cookieMatrix = null; } } class LightComponentSystem extends ComponentSystem { initializeComponentData(component, _data) { const data = { ..._data }; if (!data.type) { data.type = component.data.type; } component.data.type = data.type; if (data.layers && Array.isArray(data.layers)) { data.layers = data.layers.slice(0); } if (data.color && Array.isArray(data.color)) { data.color = new Color(data.color[0], data.color[1], data.color[2]); } if (data.cookieOffset && data.cookieOffset instanceof Array) { data.cookieOffset = new Vec2(data.cookieOffset[0], data.cookieOffset[1]); } if (data.cookieScale && data.cookieScale instanceof Array) { data.cookieScale = new Vec2(data.cookieScale[0], data.cookieScale[1]); } if (data.enable) { console.warn('WARNING: enable: Property is deprecated. Set enabled property instead.'); data.enabled = data.enable; } if (!data.shape) { data.shape = LIGHTSHAPE_PUNCTUAL; } const light = new Light(this.app.graphicsDevice, this.app.scene.clusteredLightingEnabled); light.type = lightTypes[data.type]; light._node = component.entity; component.data.light = light; super.initializeComponentData(component, data, properties); } _onRemoveComponent(entity, component) { component.onRemove(); } cloneComponent(entity, clone) { const light = entity.light; const data = []; let name; for(let i = 0; i < properties.length; i++){ name = properties[i]; if (name === 'light') { continue; } if (light[name] && light[name].clone) { data[name] = light[name].clone(); } else { data[name] = light[name]; } } return this.addComponent(clone, data); } changeType(component, oldValue, newValue) { if (oldValue !== newValue) { component.light.type = lightTypes[newValue]; } } constructor(app){ super(app); this.id = 'light'; this.ComponentType = LightComponent; this.DataType = LightComponentData; this.on('beforeremove', this._onRemoveComponent, this); } } const components = [ 'x', 'y', 'z', 'w' ]; const vecLookup = [ undefined, undefined, Vec2, Vec3, Vec4 ]; function rawToValue(app, args, value, old) { switch(args.type){ case 'boolean': return !!value; case 'number': if (typeof value === 'number') { return value; } else if (typeof value === 'string') { const v = parseInt(value, 10); if (isNaN(v)) return null; return v; } else if (typeof value === 'boolean') { return 0 + value; } return null; case 'json': { const result = {}; if (Array.isArray(args.schema)) { if (!value || typeof value !== 'object') { value = {}; } for(let i = 0; i < args.schema.length; i++){ const field = args.schema[i]; if (!field.name) continue; if (field.array) { result[field.name] = []; const arr = Array.isArray(value[field.name]) ? value[field.name] : []; for(let j = 0; j < arr.length; j++){ result[field.name].push(rawToValue(app, field, arr[j])); } } else { const val = value.hasOwnProperty(field.name) ? value[field.name] : field.default; result[field.name] = rawToValue(app, field, val); } } } return result; } case 'asset': if (value instanceof Asset) { return value; } else if (typeof value === 'number') { return app.assets.get(value) || null; } else if (typeof value === 'string') { return app.assets.get(parseInt(value, 10)) || null; } return null; case 'entity': if (value instanceof GraphNode) { return value; } else if (typeof value === 'string') { return app.getEntityFromIndex(value); } return null; case 'rgb': case 'rgba': if (value instanceof Color) { if (old instanceof Color) { old.copy(value); return old; } return value.clone(); } else if (value instanceof Array && value.length >= 3 && value.length <= 4) { for(let i = 0; i < value.length; i++){ if (typeof value[i] !== 'number') { return null; } } if (!old) old = new Color(); old.r = value[0]; old.g = value[1]; old.b = value[2]; old.a = value.length === 3 ? 1 : value[3]; return old; } else if (typeof value === 'string' && /#(?:[0-9a-f]{2}){3,4}/i.test(value)) { if (!old) { old = new Color(); } old.fromString(value); return old; } return null; case 'vec2': case 'vec3': case 'vec4': { const len = parseInt(args.type.slice(3), 10); const vecType = vecLookup[len]; if (value instanceof vecType) { if (old instanceof vecType) { old.copy(value); return old; } return value.clone(); } else if (value instanceof Array && value.length === len) { for(let i = 0; i < value.length; i++){ if (typeof value[i] !== 'number') { return null; } } if (!old) old = new vecType(); for(let i = 0; i < len; i++){ old[components[i]] = value[i]; } return old; } return null; } case 'curve': if (value) { let curve; if (value instanceof Curve || value instanceof CurveSet) { curve = value.clone(); } else { const CurveType = value.keys[0] instanceof Array ? CurveSet : Curve; curve = new CurveType(value.keys); curve.type = value.type; } return curve; } break; } return value; } function attributeToValue(app, schema, value, current) { if (schema.array) { return value.map((item, index)=>rawToValue(app, schema, item, current ? current[index] : null)); } return rawToValue(app, schema, value, current); } function assignAttributesToScript(app, attributeSchemaMap, data, script) { if (!data) return; for(const attributeName in attributeSchemaMap){ const attributeSchema = attributeSchemaMap[attributeName]; const dataToAssign = data[attributeName]; if (dataToAssign === undefined) continue; script[attributeName] = attributeToValue(app, attributeSchema, dataToAssign, script[attributeName]); } } class ScriptAttributes { add(name, args) { if (!args) { return; } if (!args.type) { return; } if (this.index[name]) { return; } else if (ScriptAttributes.reservedNames.has(name)) { return; } this.index[name] = args; Object.defineProperty(this.scriptType.prototype, name, { get: function() { return this.__attributes[name]; }, set: function(raw) { const evt = 'attr'; const evtName = `attr:${name}`; const old = this.__attributes[name]; let oldCopy = old; if (old && args.type !== 'json' && args.type !== 'entity' && old.clone) { if (this.hasEvent(evt) || this.hasEvent(evtName)) { oldCopy = old.clone(); } } if (args.array) { this.__attributes[name] = []; if (raw) { for(let i = 0, len = raw.length; i < len; i++){ this.__attributes[name].push(rawToValue(this.app, args, raw[i], old ? old[i] : null)); } } } else { this.__attributes[name] = rawToValue(this.app, args, raw, old); } this.fire(evt, name, this.__attributes[name], oldCopy); this.fire(evtName, this.__attributes[name], oldCopy); } }); } remove(name) { if (!this.index[name]) { return false; } delete this.index[name]; delete this.scriptType.prototype[name]; return true; } has(name) { return !!this.index[name]; } get(name) { return this.index[name] || null; } constructor(scriptType){ this.scriptType = scriptType; this.index = {}; } } ScriptAttributes.assignAttributesToScript = assignAttributesToScript; ScriptAttributes.attributeToValue = attributeToValue; ScriptAttributes.reservedNames = new Set([ 'app', 'entity', 'enabled', '_enabled', '_enabledOld', '_destroyed', '__attributes', '__attributesRaw', "__scriptType", '__executionOrder', '_callbacks', '_callbackActive', 'has', 'get', 'on', 'off', 'fire', 'once', 'hasEvent' ]); const SCRIPT_INITIALIZE = 'initialize'; const SCRIPT_POST_INITIALIZE = 'postInitialize'; const SCRIPT_UPDATE = 'update'; const SCRIPT_POST_UPDATE = 'postUpdate'; const SCRIPT_SWAP = 'swap'; class Script extends EventHandler { set enabled(value) { this._enabled = !!value; if (this.enabled === this._enabledOld) return; this._enabledOld = this.enabled; this.fire(this.enabled ? 'enable' : 'disable'); this.fire('state', this.enabled); if (!this._initialized && this.enabled) { this._initialized = true; this.fire('preInitialize'); if (this.initialize) { this.entity.script._scriptMethod(this, SCRIPT_INITIALIZE); } } if (this._initialized && !this._postInitialized && this.enabled && !this.entity.script._beingEnabled) { this._postInitialized = true; if (this.postInitialize) { this.entity.script._scriptMethod(this, SCRIPT_POST_INITIALIZE); } } } get enabled() { return this._enabled && !this._destroyed && this.entity.script.enabled && this.entity.enabled; } initScript(args) { const script = this.constructor; this.app = args.app; this.entity = args.entity; this._enabled = typeof args.enabled === 'boolean' ? args.enabled : true; this._enabledOld = this.enabled; this.__destroyed = false; this.__scriptType = script; this.__executionOrder = -1; } static set scriptName(value) { this.__name = value; } static get scriptName() { return this.__name; } constructor(args){ super(); this.initScript(args); } } Script.EVENT_ENABLE = 'enable'; Script.EVENT_DISABLE = 'disable'; Script.EVENT_STATE = 'state'; Script.EVENT_DESTROY = 'destroy'; Script.EVENT_ATTR = 'attr'; Script.EVENT_ERROR = 'error'; Script.__name = null; Script.__getScriptName = getScriptName; const funcNameRegex = /^\s*function(?:\s|\s*\/\*.*\*\/\s*)+([^(\s\/]*)\s*/; function getScriptName(constructorFn) { if (typeof constructorFn !== 'function') return undefined; if (constructorFn.scriptName) return constructorFn.scriptName; if ('name' in Function.prototype) return constructorFn.name; if (constructorFn === Function || constructorFn === Function.prototype.constructor) return 'Function'; const match = `${constructorFn}`.match(funcNameRegex); return match ? match[1] : undefined; } class ScriptType extends Script { static get attributes() { if (!this.hasOwnProperty('__attributes')) this.__attributes = new ScriptAttributes(this); return this.__attributes; } initScript(args) { Script.prototype.initScript.call(this, args); this.__attributes = {}; this.__attributesRaw = args.attributes || {}; } initScriptType(args) { this.initScript(args); } __initializeAttributes(force) { if (!force && !this.__attributesRaw) { return; } for(const key in this.__scriptType.attributes.index){ if (this.__attributesRaw && this.__attributesRaw.hasOwnProperty(key)) { this[key] = this.__attributesRaw[key]; } else if (!this.__attributes.hasOwnProperty(key)) { if (this.__scriptType.attributes.index[key].hasOwnProperty('default')) { this[key] = this.__scriptType.attributes.index[key].default; } else { this[key] = null; } } } this.__attributesRaw = null; } static extend(methods) { for(const key in methods){ if (!methods.hasOwnProperty(key)) { continue; } this.prototype[key] = methods[key]; } } constructor(args){ super(args); this.initScriptType(args); } } const toLowerCamelCase$1 = (str)=>str[0].toLowerCase() + str.substring(1); class ScriptComponent extends Component { set scripts(value) { this._scriptsData = value; for(const key in value){ if (!value.hasOwnProperty(key)) { continue; } const script = this._scriptsIndex[key]; if (script) { if (typeof value[key].enabled === 'boolean') { script.once('preInitialize', ()=>{ this.initializeAttributes(script); }); script.enabled = !!value[key].enabled; } if (typeof value[key].attributes === 'object') { for(const attr in value[key].attributes){ if (ScriptAttributes.reservedNames.has(attr)) { continue; } if (!script.__attributes.hasOwnProperty(attr)) { const scriptType = this.system.app.scripts.get(key); if (scriptType) { scriptType.attributes.add(attr, {}); } } script[attr] = value[key].attributes[attr]; } } } else { console.log(this.order); } } } get scripts() { return this._scripts; } set enabled(value) { const oldValue = this._enabled; this._enabled = value; this.fire('set', 'enabled', oldValue, value); } get enabled() { return this._enabled; } onEnable() { this._beingEnabled = true; this._checkState(); if (!this.entity._beingEnabled) { this.onPostStateChange(); } this._beingEnabled = false; } onDisable() { this._checkState(); } onPostStateChange() { const wasLooping = this._beginLooping(); for(let i = 0, len = this.scripts.length; i < len; i++){ const script = this.scripts[i]; if (script._initialized && !script._postInitialized && script.enabled) { script._postInitialized = true; if (script.postInitialize) { this._scriptMethod(script, SCRIPT_POST_INITIALIZE); } } } this._endLooping(wasLooping); } _beginLooping() { const looping = this._isLoopingThroughScripts; this._isLoopingThroughScripts = true; return looping; } _endLooping(wasLoopingBefore) { this._isLoopingThroughScripts = wasLoopingBefore; if (!this._isLoopingThroughScripts) { this._removeDestroyedScripts(); } } _onSetEnabled(prop, old, value) { this._beingEnabled = true; this._checkState(); this._beingEnabled = false; } _checkState() { const state = this.enabled && this.entity.enabled; if (state === this._oldState) { return; } this._oldState = state; this.fire(state ? 'enable' : 'disable'); this.fire('state', state); if (state) { this.system._addComponentToEnabled(this); } else { this.system._removeComponentFromEnabled(this); } const wasLooping = this._beginLooping(); for(let i = 0, len = this.scripts.length; i < len; i++){ const script = this.scripts[i]; script.once('preInitialize', ()=>{ this.initializeAttributes(script); }); script.enabled = script._enabled; } this._endLooping(wasLooping); } _onBeforeRemove() { this.fire('remove'); const wasLooping = this._beginLooping(); for(let i = 0; i < this.scripts.length; i++){ const script = this.scripts[i]; if (!script) continue; this.destroy(script.__scriptType.__name); } this._endLooping(wasLooping); } _removeDestroyedScripts() { const len = this._destroyedScripts.length; if (!len) return; for(let i = 0; i < len; i++){ const script = this._destroyedScripts[i]; this._removeScriptInstance(script); } this._destroyedScripts.length = 0; this._resetExecutionOrder(0, this._scripts.length); } _onInitializeAttributes() { for(let i = 0, len = this.scripts.length; i < len; i++){ const script = this.scripts[i]; this.initializeAttributes(script); } } initializeAttributes(script) { if (script instanceof ScriptType) { script.__initializeAttributes(); } else { const name = script.__scriptType.__name; const data = this._attributeDataMap.get(name); if (!data) { return; } const schema = this.system.app.scripts?.getSchema(name); assignAttributesToScript(this.system.app, schema.attributes, data, script); } } _scriptMethod(script, method, arg) { script[method](arg); } _onInitialize() { const scripts = this._scripts; const wasLooping = this._beginLooping(); for(let i = 0, len = scripts.length; i < len; i++){ const script = scripts[i]; if (!script._initialized && script.enabled) { script._initialized = true; if (script.initialize) { this._scriptMethod(script, SCRIPT_INITIALIZE); } } } this._endLooping(wasLooping); } _onPostInitialize() { this.onPostStateChange(); } _onUpdate(dt) { const list = this._updateList; if (!list.length) return; const wasLooping = this._beginLooping(); for(list.loopIndex = 0; list.loopIndex < list.length; list.loopIndex++){ const script = list.items[list.loopIndex]; if (script.enabled) { this._scriptMethod(script, SCRIPT_UPDATE, dt); } } this._endLooping(wasLooping); } _onPostUpdate(dt) { const list = this._postUpdateList; if (!list.length) return; const wasLooping = this._beginLooping(); for(list.loopIndex = 0; list.loopIndex < list.length; list.loopIndex++){ const script = list.items[list.loopIndex]; if (script.enabled) { this._scriptMethod(script, SCRIPT_POST_UPDATE, dt); } } this._endLooping(wasLooping); } _insertScriptInstance(scriptInstance, index, scriptsLength) { if (index === -1) { this._scripts.push(scriptInstance); scriptInstance.__executionOrder = scriptsLength; if (scriptInstance.update) { this._updateList.append(scriptInstance); } if (scriptInstance.postUpdate) { this._postUpdateList.append(scriptInstance); } } else { this._scripts.splice(index, 0, scriptInstance); scriptInstance.__executionOrder = index; this._resetExecutionOrder(index + 1, scriptsLength + 1); if (scriptInstance.update) { this._updateList.insert(scriptInstance); } if (scriptInstance.postUpdate) { this._postUpdateList.insert(scriptInstance); } } } _removeScriptInstance(scriptInstance) { const idx = this._scripts.indexOf(scriptInstance); if (idx === -1) return idx; this._scripts.splice(idx, 1); if (scriptInstance.update) { this._updateList.remove(scriptInstance); } if (scriptInstance.postUpdate) { this._postUpdateList.remove(scriptInstance); } return idx; } _resetExecutionOrder(startIndex, scriptsLength) { for(let i = startIndex; i < scriptsLength; i++){ this._scripts[i].__executionOrder = i; } } _resolveEntityScriptAttribute(attribute, attributeName, oldValue, useGuid, newAttributes, duplicatedIdsMap) { if (attribute.array) { const len = oldValue.length; if (!len) { return; } const newGuidArray = oldValue.slice(); for(let i = 0; i < len; i++){ const guid = newGuidArray[i] instanceof Entity ? newGuidArray[i].getGuid() : newGuidArray[i]; if (duplicatedIdsMap[guid]) { newGuidArray[i] = useGuid ? duplicatedIdsMap[guid].getGuid() : duplicatedIdsMap[guid]; } } newAttributes[attributeName] = newGuidArray; } else { if (oldValue instanceof Entity) { oldValue = oldValue.getGuid(); } else if (typeof oldValue !== 'string') { return; } if (duplicatedIdsMap[oldValue]) { newAttributes[attributeName] = duplicatedIdsMap[oldValue]; } } } has(nameOrType) { if (typeof nameOrType === 'string') { return !!this._scriptsIndex[nameOrType]; } if (!nameOrType) return false; const scriptType = nameOrType; const scriptName = scriptType.__name; const scriptData = this._scriptsIndex[scriptName]; const scriptInstance = scriptData && scriptData.instance; return scriptInstance instanceof scriptType; } get(nameOrType) { if (typeof nameOrType === 'string') { const data = this._scriptsIndex[nameOrType]; return data ? data.instance : null; } if (!nameOrType) return null; const scriptType = nameOrType; const scriptName = scriptType.__name; const scriptData = this._scriptsIndex[scriptName]; const scriptInstance = scriptData && scriptData.instance; return scriptInstance instanceof scriptType ? scriptInstance : null; } create(nameOrType, args = {}) { const self = this; let scriptType = nameOrType; let scriptName = nameOrType; if (typeof scriptType === 'string') { scriptType = this.system.app.scripts.get(scriptType); } else if (scriptType) { var _scriptType; const inferredScriptName = getScriptName(scriptType); const lowerInferredScriptName = toLowerCamelCase$1(inferredScriptName); if (!(scriptType.prototype instanceof ScriptType) && !scriptType.scriptName) ; (_scriptType = scriptType).__name ?? (_scriptType.__name = scriptType.scriptName ?? lowerInferredScriptName); scriptName = scriptType.__name; } if (scriptType) { if (!this._scriptsIndex[scriptName] || !this._scriptsIndex[scriptName].instance) { const scriptInstance = new scriptType({ app: this.system.app, entity: this.entity, enabled: args.hasOwnProperty('enabled') ? args.enabled : true, attributes: args.attributes || {} }); if (args.properties && typeof args.properties === 'object') { Object.assign(scriptInstance, args.properties); } if (!(scriptInstance instanceof ScriptType) && args.attributes) { this._attributeDataMap.set(scriptName, { ...args.attributes }); } const len = this._scripts.length; let ind = -1; if (typeof args.ind === 'number' && args.ind !== -1 && len > args.ind) { ind = args.ind; } this._insertScriptInstance(scriptInstance, ind, len); this._scriptsIndex[scriptName] = { instance: scriptInstance, onSwap: function() { self.swap(scriptName); } }; this[scriptName] = scriptInstance; if (!args.preloading) { this.initializeAttributes(scriptInstance); } this.fire('create', scriptName, scriptInstance); this.fire(`create:${scriptName}`, scriptInstance); this.system.app.scripts.on(`swap:${scriptName}`, this._scriptsIndex[scriptName].onSwap); if (!args.preloading) { if (scriptInstance.enabled && !scriptInstance._initialized) { scriptInstance._initialized = true; if (scriptInstance.initialize) { this._scriptMethod(scriptInstance, SCRIPT_INITIALIZE); } } if (scriptInstance.enabled && !scriptInstance._postInitialized) { scriptInstance._postInitialized = true; if (scriptInstance.postInitialize) { this._scriptMethod(scriptInstance, SCRIPT_POST_INITIALIZE); } } } return scriptInstance; } } else { this._scriptsIndex[scriptName] = { awaiting: true, ind: this._scripts.length }; } return null; } destroy(nameOrType) { let scriptName = nameOrType; let scriptType = nameOrType; if (typeof scriptType === 'string') { scriptType = this.system.app.scripts.get(scriptType); } else if (scriptType) { scriptName = scriptType.__name; } const scriptData = this._scriptsIndex[scriptName]; delete this._scriptsIndex[scriptName]; if (!scriptData) return false; this._attributeDataMap.delete(scriptName); const scriptInstance = scriptData.instance; if (scriptInstance && !scriptInstance._destroyed) { scriptInstance.enabled = false; scriptInstance._destroyed = true; if (!this._isLoopingThroughScripts) { const ind = this._removeScriptInstance(scriptInstance); if (ind >= 0) { this._resetExecutionOrder(ind, this._scripts.length); } } else { this._destroyedScripts.push(scriptInstance); } } this.system.app.scripts.off(`swap:${scriptName}`, scriptData.onSwap); delete this[scriptName]; this.fire('destroy', scriptName, scriptInstance || null); this.fire(`destroy:${scriptName}`, scriptInstance || null); if (scriptInstance) { scriptInstance.fire('destroy'); } return true; } swap(nameOrType) { let scriptName = nameOrType; let scriptType = nameOrType; if (typeof scriptType === 'string') { scriptType = this.system.app.scripts.get(scriptType); } else if (scriptType) { scriptName = scriptType.__name; } const old = this._scriptsIndex[scriptName]; if (!old || !old.instance) return false; const scriptInstanceOld = old.instance; const ind = this._scripts.indexOf(scriptInstanceOld); const scriptInstance = new scriptType({ app: this.system.app, entity: this.entity, enabled: scriptInstanceOld.enabled, attributes: scriptInstanceOld.__attributes }); if (!scriptInstance.swap) { return false; } this.initializeAttributes(scriptInstance); this._scripts[ind] = scriptInstance; this._scriptsIndex[scriptName].instance = scriptInstance; this[scriptName] = scriptInstance; scriptInstance.__executionOrder = ind; if (scriptInstanceOld.update) { this._updateList.remove(scriptInstanceOld); } if (scriptInstanceOld.postUpdate) { this._postUpdateList.remove(scriptInstanceOld); } if (scriptInstance.update) { this._updateList.insert(scriptInstance); } if (scriptInstance.postUpdate) { this._postUpdateList.insert(scriptInstance); } this._scriptMethod(scriptInstance, SCRIPT_SWAP, scriptInstanceOld); this.fire('swap', scriptName, scriptInstance); this.fire(`swap:${scriptName}`, scriptInstance); return true; } resolveDuplicatedEntityReferenceProperties(oldScriptComponent, duplicatedIdsMap) { const newScriptComponent = this.entity.script; for(const scriptName in oldScriptComponent._scriptsIndex){ const scriptType = this.system.app.scripts.get(scriptName); if (!scriptType) { continue; } const script = oldScriptComponent._scriptsIndex[scriptName]; if (!script || !script.instance) { continue; } const newAttributesRaw = newScriptComponent[scriptName].__attributesRaw ?? newScriptComponent._attributeDataMap.get(scriptName); const newAttributes = newScriptComponent[scriptName].__attributes; if (!newAttributesRaw && !newAttributes) { continue; } const useGuid = !!newAttributesRaw; const oldAttributes = script.instance.__attributes ?? newScriptComponent._attributeDataMap.get(scriptName); for(const attributeName in oldAttributes){ if (!oldAttributes[attributeName]) { continue; } const attribute = scriptType.attributes?.get(attributeName) ?? this.system.app.scripts.getSchema(scriptName)?.attributes?.[attributeName]; if (!attribute) { continue; } if (attribute.type === 'entity') { this._resolveEntityScriptAttribute(attribute, attributeName, oldAttributes[attributeName], useGuid, newAttributesRaw || newAttributes, duplicatedIdsMap); } else if (attribute.type === 'json' && Array.isArray(attribute.schema)) { const oldValue = oldAttributes[attributeName]; const newJsonValue = newAttributesRaw ? newAttributesRaw[attributeName] : newAttributes[attributeName]; for(let i = 0; i < attribute.schema.length; i++){ const field = attribute.schema[i]; if (field.type !== 'entity') { continue; } if (attribute.array) { for(let j = 0; j < oldValue.length; j++){ this._resolveEntityScriptAttribute(field, field.name, oldValue[j][field.name], useGuid, newJsonValue[j], duplicatedIdsMap); } } else { this._resolveEntityScriptAttribute(field, field.name, oldValue[field.name], useGuid, newJsonValue, duplicatedIdsMap); } } } } } } move(nameOrType, ind) { const len = this._scripts.length; if (ind >= len || ind < 0) { return false; } let scriptType = nameOrType; let scriptName = nameOrType; if (typeof scriptName !== 'string') { scriptName = nameOrType.__name; } else { scriptType = null; } const scriptData = this._scriptsIndex[scriptName]; if (!scriptData || !scriptData.instance) { return false; } const scriptInstance = scriptData.instance; if (scriptType && !(scriptInstance instanceof scriptType)) { return false; } const indOld = this._scripts.indexOf(scriptInstance); if (indOld === -1 || indOld === ind) { return false; } this._scripts.splice(ind, 0, this._scripts.splice(indOld, 1)[0]); this._resetExecutionOrder(0, len); this._updateList.sort(); this._postUpdateList.sort(); this.fire('move', scriptName, scriptInstance, ind, indOld); this.fire(`move:${scriptName}`, scriptInstance, ind, indOld); return true; } constructor(system, entity){ super(system, entity), this._attributeDataMap = new Map(); this._scripts = []; this._updateList = new SortedLoopArray({ sortBy: '__executionOrder' }); this._postUpdateList = new SortedLoopArray({ sortBy: '__executionOrder' }); this._scriptsIndex = {}; this._destroyedScripts = []; this._destroyed = false; this._scriptsData = null; this._oldState = true; this._enabled = true; this._beingEnabled = false; this._isLoopingThroughScripts = false; this._executionOrder = -1; this.on('set_enabled', this._onSetEnabled, this); } } ScriptComponent.EVENT_CREATE = 'create'; ScriptComponent.EVENT_DESTROY = 'destroy'; ScriptComponent.EVENT_ENABLE = 'enable'; ScriptComponent.EVENT_DISABLE = 'disable'; ScriptComponent.EVENT_REMOVE = 'remove'; ScriptComponent.EVENT_STATE = 'state'; ScriptComponent.EVENT_MOVE = 'move'; ScriptComponent.EVENT_ERROR = 'error'; class ScriptComponentData { constructor(){ this.enabled = true; } } const METHOD_INITIALIZE_ATTRIBUTES = '_onInitializeAttributes'; const METHOD_INITIALIZE = '_onInitialize'; const METHOD_POST_INITIALIZE = '_onPostInitialize'; const METHOD_UPDATE = '_onUpdate'; const METHOD_POST_UPDATE = '_onPostUpdate'; let executionOrderCounter = 0; class ScriptComponentSystem extends ComponentSystem { initializeComponentData(component, data) { component._executionOrder = executionOrderCounter++; this._components.append(component); if (executionOrderCounter > Number.MAX_SAFE_INTEGER) { this._resetExecutionOrder(); } component.enabled = data.hasOwnProperty('enabled') ? !!data.enabled : true; if (component.enabled && component.entity.enabled) { this._enabledComponents.append(component); } if (data.hasOwnProperty('order') && data.hasOwnProperty("scripts")) { component._scriptsData = data.scripts; for(let i = 0; i < data.order.length; i++){ component.create(data.order[i], { enabled: data.scripts[data.order[i]].enabled, attributes: data.scripts[data.order[i]].attributes, preloading: this.preloading }); } } } cloneComponent(entity, clone) { const order = []; const scripts = {}; for(let i = 0; i < entity.script._scripts.length; i++){ const scriptInstance = entity.script._scripts[i]; const scriptName = scriptInstance.__scriptType.__name; order.push(scriptName); const attributes = entity.script._attributeDataMap?.get(scriptName) || {}; for(const key in scriptInstance.__attributes){ attributes[key] = scriptInstance.__attributes[key]; } scripts[scriptName] = { enabled: scriptInstance._enabled, attributes: attributes }; } for(const key in entity.script._scriptsIndex){ if (key.awaiting) { order.splice(key.ind, 0, key); } } const data = { enabled: entity.script.enabled, order: order, scripts: scripts }; return this.addComponent(clone, data); } _resetExecutionOrder() { executionOrderCounter = 0; for(let i = 0, len = this._components.length; i < len; i++){ this._components.items[i]._executionOrder = executionOrderCounter++; } } _callComponentMethod(components, name, dt) { for(components.loopIndex = 0; components.loopIndex < components.length; components.loopIndex++){ components.items[components.loopIndex][name](dt); } } _onInitialize() { this.preloading = false; this._callComponentMethod(this._components, METHOD_INITIALIZE_ATTRIBUTES); this._callComponentMethod(this._enabledComponents, METHOD_INITIALIZE); } _onPostInitialize() { this._callComponentMethod(this._enabledComponents, METHOD_POST_INITIALIZE); } _onUpdate(dt) { this._callComponentMethod(this._enabledComponents, METHOD_UPDATE, dt); } _onPostUpdate(dt) { this._callComponentMethod(this._enabledComponents, METHOD_POST_UPDATE, dt); } _addComponentToEnabled(component) { this._enabledComponents.insert(component); } _removeComponentFromEnabled(component) { this._enabledComponents.remove(component); } _onBeforeRemove(entity, component) { const ind = this._components.items.indexOf(component); if (ind >= 0) { component._onBeforeRemove(); } this._removeComponentFromEnabled(component); this._components.remove(component); } destroy() { super.destroy(); this.app.systems.off('initialize', this._onInitialize, this); this.app.systems.off('postInitialize', this._onPostInitialize, this); this.app.systems.off('update', this._onUpdate, this); this.app.systems.off('postUpdate', this._onPostUpdate, this); } constructor(app){ super(app); this.id = "script"; this.ComponentType = ScriptComponent; this.DataType = ScriptComponentData; this._components = new SortedLoopArray({ sortBy: '_executionOrder' }); this._enabledComponents = new SortedLoopArray({ sortBy: '_executionOrder' }); this.preloading = true; this.on('beforeremove', this._onBeforeRemove, this); this.app.systems.on('initialize', this._onInitialize, this); this.app.systems.on('postInitialize', this._onPostInitialize, this); this.app.systems.on('update', this._onUpdate, this); this.app.systems.on('postUpdate', this._onPostUpdate, this); } } var gsplatIntervalTextureGLSL = ` precision highp usampler2D; uniform usampler2D uIntervalsTexture; uniform int uNumIntervals; uniform int uTextureWidth; uniform int uActiveSplats; ivec2 getCoordFromIndex(int index, int textureWidth) { return ivec2(index % textureWidth, index / textureWidth); } void main() { ivec2 coord = ivec2(gl_FragCoord.xy); int targetIndex = coord.y * uTextureWidth + coord.x; if (targetIndex >= uActiveSplats) { gl_FragColor = 0u; return; } int left = 0; int right = uNumIntervals - 1; int intervalIndex = 0; while (left <= right) { int mid = (left + right) / 2; int textureWidth = textureSize(uIntervalsTexture, 0).x; ivec2 intervalCoord = getCoordFromIndex(mid, textureWidth); uvec2 intervalData = texelFetch(uIntervalsTexture, intervalCoord, 0).rg; uint accumulatedSum = intervalData.g; if (uint(targetIndex) < accumulatedSum) { intervalIndex = mid; right = mid - 1; } else { left = mid + 1; } } int textureWidth = textureSize(uIntervalsTexture, 0).x; ivec2 intervalCoord = getCoordFromIndex(intervalIndex, textureWidth); uvec2 intervalData = texelFetch(uIntervalsTexture, intervalCoord, 0).rg; uint intervalStart = intervalData.r; uint currentAccSum = intervalData.g; uint prevAccSum = 0u; if (intervalIndex > 0) { ivec2 prevCoord = getCoordFromIndex(intervalIndex - 1, textureWidth); prevAccSum = texelFetch(uIntervalsTexture, prevCoord, 0).g; } uint offsetInInterval = uint(targetIndex) - prevAccSum; uint originalIndex = intervalStart + offsetInInterval; gl_FragColor = originalIndex; } `; var gsplatIntervalTextureWGSL = ` var uIntervalsTexture: texture_2d; uniform uNumIntervals: i32; uniform uTextureWidth: i32; uniform uActiveSplats: i32; fn getCoordFromIndex(index: i32, textureWidth: i32) -> vec2i { return vec2i(index % textureWidth, index / textureWidth); } @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let coord = vec2i(i32(input.position.x), i32(input.position.y)); let targetIndex = coord.y * uniform.uTextureWidth + coord.x; if (targetIndex >= uniform.uActiveSplats) { output.color = 0u; return output; } var left = 0i; var right = uniform.uNumIntervals - 1; var intervalIndex = 0i; while (left <= right) { let mid = (left + right) / 2; let textureWidth = i32(textureDimensions(uIntervalsTexture, 0).x); let intervalCoord = getCoordFromIndex(mid, textureWidth); let intervalData = textureLoad(uIntervalsTexture, intervalCoord, 0).rg; let accumulatedSum = intervalData.g; if (u32(targetIndex) < accumulatedSum) { intervalIndex = mid; right = mid - 1; } else { left = mid + 1; } } let textureWidth = i32(textureDimensions(uIntervalsTexture, 0).x); let intervalCoord = getCoordFromIndex(intervalIndex, textureWidth); let intervalData = textureLoad(uIntervalsTexture, intervalCoord, 0).rg; let intervalStart = intervalData.r; let currentAccSum = intervalData.g; var prevAccSum = 0u; if (intervalIndex > 0) { let prevCoord = getCoordFromIndex(intervalIndex - 1, textureWidth); prevAccSum = textureLoad(uIntervalsTexture, prevCoord, 0).g; } let offsetInInterval = u32(targetIndex) - prevAccSum; let originalIndex = intervalStart + offsetInInterval; output.color = originalIndex; return output; } `; class GSplatIntervalTexture { destroy() { this.texture?.destroy(); this.texture = null; this.rt?.destroy(); this.rt = null; this.intervalsDataTexture?.destroy(); this.intervalsDataTexture = null; this.shader = null; } getShader() { if (!this.shader) { this.shader = ShaderUtils.createShader(this.device, { uniqueName: 'GSplatIntervalsShader', attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentGLSL: gsplatIntervalTextureGLSL, fragmentWGSL: gsplatIntervalTextureWGSL, fragmentOutputTypes: [ 'uint' ] }); } return this.shader; } createTexture(name, format, width, height) { return new Texture(this.device, { name: name, width: width, height: height, format: format, cubemap: false, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); } update(intervals, totalIntervalSplats) { const maxTextureSize = this.device.maxTextureSize; let textureWidth = Math.ceil(Math.sqrt(totalIntervalSplats)); textureWidth = Math.min(textureWidth, maxTextureSize); const textureHeight = Math.ceil(totalIntervalSplats / textureWidth); this.texture = this.createTexture('intervalsTexture', PIXELFORMAT_R32U, textureWidth, textureHeight); this.rt = new RenderTarget({ colorBuffer: this.texture, depth: false }); const numIntervals = intervals.length / 2; const dataTextureSize = Math.ceil(Math.sqrt(numIntervals)); this.intervalsDataTexture = this.createTexture('intervalsData', PIXELFORMAT_RG32U, dataTextureSize, dataTextureSize); const intervalsData = this.intervalsDataTexture.lock(); let runningSum = 0; for(let i = 0; i < numIntervals; i++){ const start = intervals[i * 2]; const end = intervals[i * 2 + 1]; const intervalSize = end - start; runningSum += intervalSize; intervalsData[i * 2] = start; intervalsData[i * 2 + 1] = runningSum; } this.intervalsDataTexture.unlock(); const scope = this.device.scope; scope.resolve('uIntervalsTexture').setValue(this.intervalsDataTexture); scope.resolve('uNumIntervals').setValue(numIntervals); scope.resolve('uTextureWidth').setValue(textureWidth); scope.resolve('uActiveSplats').setValue(totalIntervalSplats); this.device.setCullMode(CULLFACE_NONE); this.device.setBlendState(BlendState.NOBLEND); this.device.setDepthState(DepthState.NODEPTH); drawQuadWithShader(this.device, this.rt, this.getShader()); return totalIntervalSplats; } constructor(device){ this.texture = null; this.rt = null; this.intervalsDataTexture = null; this.shader = null; this.device = device; } } const vecs = []; class GSplatInfo { destroy() { this.intervals.length = 0; this.intervalTexture?.destroy(); } setLines(start, count, textureSize, activeSplats) { this.lineStart = start; this.lineCount = count; this.padding = textureSize * count - activeSplats; this.viewport.set(0, start, textureSize, count); } updateIntervals(intervals) { const resource = this.resource; this.intervals.length = 0; this.activeSplats = resource.numSplats; if (intervals.size > 0) { let totalCount = 0; let used = 0; for (const interval of intervals.values()){ totalCount += interval.y - interval.x + 1; vecs[used++] = interval; } if (totalCount !== this.numSplats) { vecs.length = used; vecs.sort((a, b)=>a.x - b.x); this.intervals.length = used * 2; let k = 0; let currentStart = vecs[0].x; let currentEnd = vecs[0].y; for(let i = 1; i < used; i++){ const p = vecs[i]; if (p.x === currentEnd + 1) { currentEnd = p.y; } else { this.intervals[k++] = currentStart; this.intervals[k++] = currentEnd + 1; currentStart = p.x; currentEnd = p.y; } } this.intervals[k++] = currentStart; this.intervals[k++] = currentEnd + 1; this.intervals.length = k; this.intervalTexture = new GSplatIntervalTexture(this.device); this.activeSplats = this.intervalTexture.update(this.intervals, totalCount); } vecs.length = 0; } } update() { const worldMatrix = this.node.getWorldTransform(); const worldMatrixChanged = !this.previousWorldTransform.equals(worldMatrix); if (worldMatrixChanged) { this.previousWorldTransform.copy(worldMatrix); } return worldMatrixChanged; } resetColorAccumulators(colorUpdateAngle, colorUpdateDistance) { const randomFactor = Math.random(); this.colorAccumulatedRotation = randomFactor * colorUpdateAngle; this.colorAccumulatedTranslation = randomFactor * colorUpdateDistance; } get hasSphericalHarmonics() { return this.resource.gsplatData?.shBands > 0; } constructor(device, resource, placement){ this.activeSplats = 0; this.intervals = []; this.lineStart = 0; this.lineCount = 0; this.padding = 0; this.viewport = new Vec4(); this.previousWorldTransform = new Mat4(); this.aabb = new BoundingBox(); this.intervalTexture = null; this.colorAccumulatedRotation = 0; this.colorAccumulatedTranslation = 0; this.device = device; this.resource = resource; this.node = placement.node; this.lodIndex = placement.lodIndex; this.numSplats = resource.numSplats; this.aabb.copy(placement.aabb); this.updateIntervals(placement.intervals); } } function UnifiedSortWorker() { const myself = typeof self !== 'undefined' && self || require('node:worker_threads').parentPort; const centersMap = new Map(); let centersData; let distances; let countBuffer; let _radialSort = false; const numBins = 32; const binBase = new Float32Array(numBins + 1); const binDivider = new Float32Array(numBins + 1); const binWeightsUtil = new GSplatSortBinWeights(); const unpackBinWeights = (binWeights)=>{ for(let i = 0; i < numBins; i++){ binBase[i] = binWeights[i * 2]; binDivider[i] = binWeights[i * 2 + 1]; } binBase[numBins] = binBase[numBins - 1] + binDivider[numBins - 1]; binDivider[numBins] = 0; }; const evaluateSortKeysCommon = (sortParams, minDist, range, distances, countBuffer, centersData, processSplatFn)=>{ const { ids, lineStarts, padding, intervals, textureSize } = centersData; const invBinRange = numBins / range; for(let paramIdx = 0; paramIdx < sortParams.length; paramIdx++){ const params = sortParams[paramIdx]; const id = ids[paramIdx]; const centers = centersMap.get(id); if (!centers) { console.error('UnifiedSortWorker: No centers found for id', id); } let targetIndex = lineStarts[paramIdx] * textureSize; const intervalsArray = intervals[paramIdx].length > 0 ? intervals[paramIdx] : [ 0, centers.length / 3 ]; for(let i = 0; i < intervalsArray.length; i += 2){ const intervalStart = intervalsArray[i] * 3; const intervalEnd = intervalsArray[i + 1] * 3; targetIndex = processSplatFn(centers, params, intervalStart, intervalEnd, targetIndex, invBinRange, minDist, range, distances, countBuffer); } const pad = padding[paramIdx]; countBuffer[0] += pad; distances.fill(0, targetIndex, targetIndex + pad); targetIndex += pad; } }; const evaluateSortKeysLinear = (sortParams, minDist, range, distances, countBuffer, centersData)=>{ evaluateSortKeysCommon(sortParams, minDist, range, distances, countBuffer, centersData, (centers, params, intervalStart, intervalEnd, targetIndex, invBinRange, minDist, range, distances, countBuffer)=>{ const { transformedDirection, offset, scale } = params; const dx = transformedDirection.x; const dy = transformedDirection.y; const dz = transformedDirection.z; const sdx = dx * scale; const sdy = dy * scale; const sdz = dz * scale; const add = offset - minDist; for(let srcIndex = intervalStart; srcIndex < intervalEnd; srcIndex += 3){ const x = centers[srcIndex]; const y = centers[srcIndex + 1]; const z = centers[srcIndex + 2]; const dist = x * sdx + y * sdy + z * sdz + add; const d = dist * invBinRange; const bin = d >>> 0; const sortKey = binBase[bin] + binDivider[bin] * (d - bin) >>> 0; distances[targetIndex++] = sortKey; countBuffer[sortKey]++; } return targetIndex; }); }; const evaluateSortKeysRadial = (sortParams, minDist, range, distances, countBuffer, centersData)=>{ evaluateSortKeysCommon(sortParams, minDist, range, distances, countBuffer, centersData, (centers, params, intervalStart, intervalEnd, targetIndex, invBinRange, minDist, range, distances, countBuffer)=>{ const { transformedPosition, scale } = params; const cx = transformedPosition.x; const cy = transformedPosition.y; const cz = transformedPosition.z; for(let srcIndex = intervalStart; srcIndex < intervalEnd; srcIndex += 3){ const dx = centers[srcIndex] - cx; const dy = centers[srcIndex + 1] - cy; const dz = centers[srcIndex + 2] - cz; const distSq = dx * dx + dy * dy + dz * dz; const dist = Math.sqrt(distSq) * scale; const invertedDist = range - dist; const d = invertedDist * invBinRange; const bin = d >>> 0; const sortKey = binBase[bin] + binDivider[bin] * (d - bin) >>> 0; distances[targetIndex++] = sortKey; countBuffer[sortKey]++; } return targetIndex; }); }; const countingSort = (bucketCount, countBuffer, numVertices, distances, order)=>{ for(let i = 1; i < bucketCount; i++){ countBuffer[i] += countBuffer[i - 1]; } for(let i = 0; i < numVertices; i++){ const distance = distances[i]; const destIndex = --countBuffer[distance]; order[destIndex] = i; } }; const computeEffectiveDistanceRangeLinear = (sortParams)=>{ let minDist = Infinity; let maxDist = -Infinity; for(let paramIdx = 0; paramIdx < sortParams.length; paramIdx++){ const params = sortParams[paramIdx]; const { transformedDirection, offset, scale, aabbMin, aabbMax } = params; const dx = transformedDirection.x; const dy = transformedDirection.y; const dz = transformedDirection.z; const pxMin = dx >= 0 ? aabbMin[0] : aabbMax[0]; const pyMin = dy >= 0 ? aabbMin[1] : aabbMax[1]; const pzMin = dz >= 0 ? aabbMin[2] : aabbMax[2]; const pxMax = dx >= 0 ? aabbMax[0] : aabbMin[0]; const pyMax = dy >= 0 ? aabbMax[1] : aabbMin[1]; const pzMax = dz >= 0 ? aabbMax[2] : aabbMin[2]; const dMin = pxMin * dx + pyMin * dy + pzMin * dz; const dMax = pxMax * dx + pyMax * dy + pzMax * dz; const eMin = dMin * scale + offset; const eMax = dMax * scale + offset; const localMin = Math.min(eMin, eMax); const localMax = Math.max(eMin, eMax); if (localMin < minDist) minDist = localMin; if (localMax > maxDist) maxDist = localMax; } if (minDist === Infinity) { minDist = 0; maxDist = 0; } return { minDist, maxDist }; }; const computeEffectiveDistanceRangeRadial = (sortParams)=>{ let maxDist = -Infinity; for(let paramIdx = 0; paramIdx < sortParams.length; paramIdx++){ const params = sortParams[paramIdx]; const { transformedPosition, scale, aabbMin, aabbMax } = params; const cx = transformedPosition.x; const cy = transformedPosition.y; const cz = transformedPosition.z; for(let i = 0; i < 8; i++){ const px = i & 1 ? aabbMax[0] : aabbMin[0]; const py = i & 2 ? aabbMax[1] : aabbMin[1]; const pz = i & 4 ? aabbMax[2] : aabbMin[2]; const dx = px - cx; const dy = py - cy; const dz = pz - cz; const distSq = dx * dx + dy * dy + dz * dz; const dist = Math.sqrt(distSq) * scale; if (dist > maxDist) maxDist = dist; } } const minDist = 0; if (maxDist < 0) { maxDist = 0; } return { minDist, maxDist }; }; const sort = (sortParams, order, centersData)=>{ const sortStartTime = performance.now(); const { minDist, maxDist } = _radialSort ? computeEffectiveDistanceRangeRadial(sortParams) : computeEffectiveDistanceRangeLinear(sortParams); const numVertices = centersData.totalUsedPixels; const compareBits = Math.max(10, Math.min(20, Math.round(Math.log2(numVertices / 4)))); const bucketCount = 2 ** compareBits + 1; if (distances?.length !== numVertices) { distances = new Uint32Array(numVertices); } if (!countBuffer || countBuffer.length !== bucketCount) { countBuffer = new Uint32Array(bucketCount); } else { countBuffer.fill(0); } const range = maxDist - minDist; const cameraBin = GSplatSortBinWeights.computeCameraBin(_radialSort, minDist, range); const binWeights = binWeightsUtil.compute(cameraBin, bucketCount); unpackBinWeights(binWeights); if (_radialSort) { evaluateSortKeysRadial(sortParams, minDist, range, distances, countBuffer, centersData); } else { evaluateSortKeysLinear(sortParams, minDist, range, distances, countBuffer, centersData); } countingSort(bucketCount, countBuffer, numVertices, distances, order); const count = numVertices; const sortTime = performance.now() - sortStartTime; const transferList = [ order.buffer ]; const response = { order: order.buffer, count, version: centersData.version, sortTime: sortTime }; myself.postMessage(response, transferList); }; myself.addEventListener('message', (message)=>{ const msgData = message.data ?? message; switch(msgData.command){ case 'addCenters': { centersMap.set(msgData.id, new Float32Array(msgData.centers)); break; } case 'removeCenters': { centersMap.delete(msgData.id); break; } case 'sort': { _radialSort = msgData.radialSorting || false; const order = new Uint32Array(msgData.order); sort(msgData.sortParams, order, centersData); break; } case 'intervals': { centersData = msgData; break; } } }); } let GSplatSortBinWeights$1 = class GSplatSortBinWeights { static get NUM_BINS() { return 32; } static get WEIGHT_TIERS() { return [ { maxDistance: 0, weight: 40.0 }, { maxDistance: 2, weight: 20.0 }, { maxDistance: 5, weight: 8.0 }, { maxDistance: 10, weight: 3.0 }, { maxDistance: Infinity, weight: 1.0 } ]; } static computeCameraBin(radialSort, minDist, range) { const numBins = GSplatSortBinWeights.NUM_BINS; if (radialSort) { return numBins - 1; } const cameraOffsetFromRangeStart = -minDist; const cameraBinFloat = cameraOffsetFromRangeStart / range * numBins; return Math.max(0, Math.min(numBins - 1, Math.floor(cameraBinFloat))); } compute(cameraBin, bucketCount) { if (cameraBin === this.lastCameraBin && bucketCount === this.lastBucketCount) { return this.binWeights; } this.lastCameraBin = cameraBin; this.lastBucketCount = bucketCount; const numBins = GSplatSortBinWeights.NUM_BINS; const bitsPerBin = this.bitsPerBin; for(let i = 0; i < numBins; i++){ const distFromCamera = Math.abs(i - cameraBin); bitsPerBin[i] = this.weightByDistance[distFromCamera]; } let totalWeight = 0; for(let i = 0; i < numBins; i++){ totalWeight += bitsPerBin[i]; } let accumulated = 0; for(let i = 0; i < numBins; i++){ const divider = Math.max(1, Math.floor(bitsPerBin[i] / totalWeight * bucketCount)); this.binWeights[i * 2] = accumulated; this.binWeights[i * 2 + 1] = divider; accumulated += divider; } if (accumulated > bucketCount) { const excess = accumulated - bucketCount; const lastDividerIdx = (numBins - 1) * 2 + 1; this.binWeights[lastDividerIdx] = Math.max(1, this.binWeights[lastDividerIdx] - excess); } return this.binWeights; } constructor(){ this.binWeights = new Float32Array(GSplatSortBinWeights.NUM_BINS * 2); this.lastCameraBin = -1; this.lastBucketCount = -1; const numBins = GSplatSortBinWeights.NUM_BINS; const weightTiers = GSplatSortBinWeights.WEIGHT_TIERS; this.bitsPerBin = new Float32Array(numBins); this.weightByDistance = new Float32Array(numBins); for(let dist = 0; dist < numBins; dist++){ let weight = 1.0; for(let j = 0; j < weightTiers.length; j++){ if (dist <= weightTiers[j].maxDistance) { weight = weightTiers[j].weight; break; } } this.weightByDistance[dist] = weight; } } }; const _neededIds = new Set(); class GSplatUnifiedSorter extends EventHandler { onSorted(message) { if (this._destroyed) { return; } const msgData = message.data ?? message; if (this.scene && msgData.sortTime !== undefined) { this.scene.fire('gsplat:sorted', msgData.sortTime); } const orderData = new Uint32Array(msgData.order); this.jobsInFlight--; if (this.pendingSorted) { this.releaseOrderData(this.pendingSorted.orderData); } this.pendingSorted = { count: msgData.count, version: msgData.version, orderData: orderData }; } applyPendingSorted() { if (this.pendingSorted) { const { count, version, orderData } = this.pendingSorted; this.pendingSorted = null; this.fire('sorted', count, version, orderData); this.releaseOrderData(orderData); } } releaseOrderData(orderData) { if (orderData.length === this.bufferLength) { this.availableOrderData.push(orderData); } } destroy() { this._destroyed = true; this.pendingSorted = null; this.worker.terminate(); this.worker = null; } setCenters(id, centers) { if (centers) { if (!this.centersSet.has(id)) { this.centersSet.add(id); const centersBuffer = centers.buffer.slice(); this.worker.postMessage({ command: 'addCenters', id: id, centers: centersBuffer }, [ centersBuffer ]); } } else { if (this.centersSet.has(id)) { this.centersSet.delete(id); this.worker.postMessage({ command: 'removeCenters', id: id }); } } } updateCentersForSplats(splats) { for (const splat of splats){ const id = splat.resource.id; _neededIds.add(id); if (!this.centersSet.has(id)) { this.setCenters(id, splat.resource.centers); } } for (const id of this.centersSet){ if (!_neededIds.has(id)) { this.setCenters(id, null); } } _neededIds.clear(); } setSortParameters(payload) { this.hasNewVersion = true; const { textureSize } = payload; const newLength = textureSize * textureSize; if (newLength !== this.bufferLength) { this.bufferLength = newLength; this.availableOrderData.length = 0; } this.worker.postMessage(payload); } setSortParams(params, radialSorting) { if (this.hasNewVersion || this.jobsInFlight === 0) { let orderData = this.availableOrderData.pop(); if (!orderData) { orderData = new Uint32Array(this.bufferLength); } this.jobsInFlight++; this.hasNewVersion = false; this.worker.postMessage({ command: 'sort', sortParams: params, radialSorting: radialSorting, order: orderData.buffer }, [ orderData.buffer ]); } } constructor(scene){ super(), this.bufferLength = 0, this.availableOrderData = [], this.jobsInFlight = 0, this.hasNewVersion = false, this.pendingSorted = null, this.centersSet = new Set(), this._destroyed = false, this.scene = null; this.scene = scene ?? null; const workerSource = ` const GSplatSortBinWeights = ${GSplatSortBinWeights$1.toString()}; (${UnifiedSortWorker.toString()})() `; if (platform.environment === 'node') { this.worker = new Worker(workerSource, { eval: true }); this.worker.on('message', this.onSorted.bind(this)); } else { this.worker = new Worker(URL.createObjectURL(new Blob([ workerSource ], { type: "application/javascript" }))); this.worker.addEventListener('message', this.onSorted.bind(this)); } } } class GSplatRenderer { setRenderMode(renderMode) { const oldRenderMode = this.renderMode ?? 0; const wasForward = (oldRenderMode & GSPLAT_FORWARD) !== 0; const wasShadow = (oldRenderMode & GSPLAT_SHADOW) !== 0; const isForward = (renderMode & GSPLAT_FORWARD) !== 0; const isShadow = (renderMode & GSPLAT_SHADOW) !== 0; this.meshInstance.castShadow = isShadow; if (wasForward && !isForward) { this.layer.removeMeshInstances([ this.meshInstance ], true); } if (wasShadow && !isShadow) { this.layer.removeShadowCasters([ this.meshInstance ]); } if (!wasForward && isForward) { this.layer.addMeshInstances([ this.meshInstance ], true); } if (!wasShadow && isShadow) { this.layer.addShadowCasters([ this.meshInstance ]); } this.renderMode = renderMode; } destroy() { if (this.renderMode) { if (this.renderMode & GSPLAT_FORWARD) { this.layer.removeMeshInstances([ this.meshInstance ], true); } if (this.renderMode & GSPLAT_SHADOW) { this.layer.removeShadowCasters([ this.meshInstance ]); } } this._material.destroy(); this.meshInstance.destroy(); } get material() { return this._material; } configureMaterial() { const { device, workBuffer } = this; this._material.setDefine('GSPLAT_WORKBUFFER_DATA', true); this._material.setDefine('STORAGE_ORDER', device.isWebGPU); const isColorUint = workBuffer.colorTextureFormat === PIXELFORMAT_RGBA16U; this._material.setDefine('GSPLAT_COLOR_UINT', isColorUint); this._material.setParameter('splatColor', workBuffer.colorTexture); this._material.setParameter('splatTexture0', workBuffer.splatTexture0); this._material.setParameter('splatTexture1', workBuffer.splatTexture1); this._material.setDefine('SH_BANDS', '0'); this._material.setParameter('numSplats', 0); if (workBuffer.orderTexture) { this._material.setParameter('splatOrder', workBuffer.orderTexture); } this._material.setParameter('alphaClip', 0.3); this._material.setDefine(`DITHER_${'NONE'}`, ''); this._material.cull = CULLFACE_NONE; this._material.blendType = BLEND_PREMULTIPLIED; this._material.depthWrite = false; this._material.update(); } update(count, textureSize) { this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize); this._material.setParameter('numSplats', count); this._material.setParameter('splatTextureSize', textureSize); this.meshInstance.visible = count > 0; } setOrderData() { if (this.device.isWebGPU) { this._material.setParameter('splatOrder', this.workBuffer.orderBuffer); } else { this._material.setParameter('splatOrder', this.workBuffer.orderTexture); } } setOrderBuffer(buffer) { this._material.setParameter('splatOrder', buffer); } frameUpdate(params) { if (params.colorRamp) { this._material.setParameter('colorRampIntensity', params.colorRampIntensity); } if (this.forceCopyMaterial || params.material.dirty) { this.copyMaterialSettings(params.material); this.forceCopyMaterial = false; } } copyMaterialSettings(sourceMaterial) { const keysToDelete = []; this._material.defines.forEach((value, key)=>{ if (!this._internalDefines.has(key)) { keysToDelete.push(key); } }); keysToDelete.forEach((key)=>this._material.defines.delete(key)); sourceMaterial.defines.forEach((value, key)=>{ this._material.defines.set(key, value); }); const srcParams = sourceMaterial.parameters; for(const paramName in srcParams){ if (srcParams.hasOwnProperty(paramName)) { this._material.setParameter(paramName, srcParams[paramName].data); } } if (sourceMaterial.hasShaderChunks) { this._material.shaderChunks.copy(sourceMaterial.shaderChunks); } this._material.update(); } updateOverdrawMode(params) { const overdrawEnabled = !!params.colorRamp; const wasOverdrawEnabled = this._material.getDefine('GSPLAT_OVERDRAW'); if (overdrawEnabled) { this._material.setParameter('colorRamp', params.colorRamp); this._material.setParameter('colorRampIntensity', params.colorRampIntensity); } if (overdrawEnabled !== wasOverdrawEnabled) { this._material.setDefine('GSPLAT_OVERDRAW', overdrawEnabled); if (overdrawEnabled) { this.originalBlendType = this._material.blendType; this._material.blendType = BLEND_ADDITIVE; } else { this._material.blendType = this.originalBlendType; } this._material.update(); } } setMaxNumSplats(numSplats) { const roundedNumSplats = math.roundUp(numSplats, GSplatResourceBase.instanceSize); if (this.instanceIndicesCount < roundedNumSplats) { this.instanceIndicesCount = roundedNumSplats; this.instanceIndices?.destroy(); this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, numSplats); this.meshInstance.setInstancing(this.instanceIndices, true); this._material.setParameter('splatTextureSize', this.workBuffer.textureSize); } } createMeshInstance() { const mesh = GSplatResourceBase.createMesh(this.device); const textureSize = this.workBuffer.textureSize; const instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, textureSize * textureSize); const meshInstance = new MeshInstance(mesh, this._material); meshInstance.node = this.node; meshInstance.setInstancing(instanceIndices, true); meshInstance.instancingCount = 0; const thisCamera = this.cameraNode.camera; meshInstance.isVisibleFunc = (camera)=>{ const renderMode = this.renderMode ?? 0; if (thisCamera.camera === camera && renderMode & GSPLAT_FORWARD) { return true; } if (renderMode & GSPLAT_SHADOW) { return camera.node?.name === SHADOWCAMERA_NAME; } return false; }; return meshInstance; } constructor(device, node, cameraNode, layer, workBuffer){ this.instanceIndices = null; this.instanceIndicesCount = 0; this.originalBlendType = BLEND_ADDITIVE; this._internalDefines = new Set(); this.forceCopyMaterial = true; this.device = device; this.node = node; this.cameraNode = cameraNode; this.layer = layer; this.workBuffer = workBuffer; this._material = new ShaderMaterial({ uniqueName: 'UnifiedSplatMaterial', vertexGLSL: '#include "gsplatVS"', fragmentGLSL: '#include "gsplatPS"', vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION, vertex_id_attrib: SEMANTIC_ATTR13 } }); this.configureMaterial(); this._material.defines.forEach((value, key)=>{ this._internalDefines.add(key); }); this.meshInstance = this.createMeshInstance(); } } class GSplatPlacement { set aabb(aabb) { this._aabb.copy(aabb); } get aabb() { return this._aabb; } set lodDistances(distances) { const isOctree = !!(this.resource && this.resource.octree); if (isOctree) { if (distances) { this.resource.octree?.lodLevels ?? 1; this._lodDistances = distances.slice(); } else { this._lodDistances = null; } } } get lodDistances() { return this._lodDistances ? this._lodDistances.slice() : null; } constructor(resource, node, lodIndex = 0){ this.intervals = new Map(); this.lodIndex = 0; this._lodDistances = null; this.splatBudget = 0; this._aabb = new BoundingBox(); this.resource = resource; this.node = node; this.lodIndex = lodIndex; } } const _invWorldMat = new Mat4(); const _localCameraPos = new Vec3(); const _localCameraFwd = new Vec3(); const _dirToNode = new Vec3(); const _tempCompletedUrls = []; new BoundingBox(); [ new Color(1, 0, 0), new Color(0, 1, 0), new Color(0, 0, 1), new Color(1, 1, 0), new Color(1, 0, 1) ]; class NodeInfo { reset() { this.currentLod = -1; this.optimalLod = -1; this.importance = 0; } constructor(){ this.currentLod = -1; this.optimalLod = -1; this.importance = 0; } } class GSplatOctreeInstance { get pendingLoadCount() { let count = this.pending.size + this.prefetchPending.size; if (this.octree.environmentUrl && !this.environmentPlacement) { count++; } return count; } destroy() { if (this.octree && !this.octree.destroyed) { const filesToDecRef = this.getFileDecrements(); for (const fileIndex of filesToDecRef){ this.octree.decRefCount(fileIndex, 0); } for (const fileIndex of this.pending){ if (!this.filePlacements[fileIndex]) { this.octree.unloadResource(fileIndex); } } for (const fileIndex of this.prefetchPending){ if (!this.filePlacements[fileIndex]) { this.octree.unloadResource(fileIndex); } } if (this.environmentPlacement) { this.octree.decEnvironmentRefCount(); } } this.pending.clear(); this.pendingDecrements.clear(); this.filePlacements.length = 0; if (this.environmentPlacement) { this.activePlacements.delete(this.environmentPlacement); this.environmentPlacement = null; } this._deviceLostEvent?.off(); this._deviceLostEvent = null; } _onDeviceLost() { for(let i = 0; i < this.filePlacements.length; i++){ if (this.filePlacements[i]) { this.octree.decRefCount(i, 0); } } this.filePlacements.fill(null); this.activePlacements.clear(); this.pending.clear(); this.pendingDecrements.clear(); this.removedCandidates.clear(); this.prefetchPending.clear(); this.pendingVisibleAdds.clear(); for (const nodeInfo of this.nodeInfos){ nodeInfo.reset(); } if (this.environmentPlacement) { this.activePlacements.delete(this.environmentPlacement); this.environmentPlacement = null; this.octree.unloadEnvironmentResource(); } this.dirtyModifiedPlacements = true; this.needsLodUpdate = true; } getFileDecrements() { const toRelease = []; for(let i = 0; i < this.filePlacements.length; i++){ if (this.filePlacements[i]) { toRelease.push(i); } } return toRelease; } calculateNodeLod(localCameraPosition, localCameraForward, nodeIndex, maxLod, lodDistances, lodBehindPenalty) { const node = this.octree.nodes[nodeIndex]; node.bounds.closestPoint(localCameraPosition, _dirToNode); _dirToNode.sub(localCameraPosition); let distance = _dirToNode.length(); if (lodBehindPenalty > 1 && distance > 0.01) { const dotOverDistance = localCameraForward.dot(_dirToNode) / distance; if (dotOverDistance < 0) { const t = -dotOverDistance; const factor = 1 + t * (lodBehindPenalty - 1); distance *= factor; } } for(let lod = 0; lod < maxLod; lod++){ if (distance < lodDistances[lod]) { return lod; } } return maxLod; } selectDesiredLodIndex(node, optimalLodIndex, maxLod, lodUnderfillLimit) { if (lodUnderfillLimit > 0) { const allowedMaxCoarseLod = Math.min(maxLod, optimalLodIndex + lodUnderfillLimit); for(let lod = optimalLodIndex; lod <= allowedMaxCoarseLod; lod++){ const fi = node.lods[lod].fileIndex; if (fi !== -1 && this.octree.getFileResource(fi)) { return lod; } } for(let lod = allowedMaxCoarseLod; lod >= optimalLodIndex; lod--){ const fi = node.lods[lod].fileIndex; if (fi !== -1) { return lod; } } } return optimalLodIndex; } prefetchNextLod(node, desiredLodIndex, optimalLodIndex) { if (desiredLodIndex === -1 || optimalLodIndex === -1) return; if (desiredLodIndex === optimalLodIndex) { const fi = node.lods[optimalLodIndex].fileIndex; if (fi !== -1) { this.octree.ensureFileResource(fi); if (!this.octree.getFileResource(fi)) { this.prefetchPending.add(fi); } } return; } const targetLod = Math.max(optimalLodIndex, desiredLodIndex - 1); for(let lod = targetLod; lod >= optimalLodIndex; lod--){ const fi = node.lods[lod].fileIndex; if (fi !== -1) { this.octree.ensureFileResource(fi); if (!this.octree.getFileResource(fi)) { this.prefetchPending.add(fi); } break; } } } updateLod(cameraNode, params) { const maxLod = this.octree.lodLevels - 1; const lodDistances = this.placement.lodDistances || [ 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60 ]; const { lodRangeMin, lodRangeMax } = params; const rangeMin = Math.max(0, Math.min(lodRangeMin ?? 0, maxLod)); const rangeMax = Math.max(rangeMin, Math.min(lodRangeMax ?? maxLod, maxLod)); const totalOptimalSplats = this.evaluateNodeLods(cameraNode, maxLod, lodDistances, rangeMin, rangeMax, params); if (this.splatBudget > 0) { this.enforceSplatBudget(totalOptimalSplats, this.splatBudget, rangeMin, rangeMax); } this.applyLodChanges(maxLod, params); } evaluateNodeLods(cameraNode, maxLod, lodDistances, rangeMin, rangeMax, params) { const { lodBehindPenalty } = params; const worldCameraPosition = cameraNode.getPosition(); const octreeWorldTransform = this.placement.node.getWorldTransform(); _invWorldMat.copy(octreeWorldTransform).invert(); const localCameraPosition = _invWorldMat.transformPoint(worldCameraPosition, _localCameraPos); const worldCameraForward = cameraNode.forward; const localCameraForward = _invWorldMat.transformVector(worldCameraForward, _localCameraFwd).normalize(); const nodes = this.octree.nodes; const nodeInfos = this.nodeInfos; let totalSplats = 0; const maxDistance = lodDistances[rangeMax] || 100; for(let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++){ const node = nodes[nodeIndex]; node.bounds.closestPoint(localCameraPosition, _dirToNode); _dirToNode.sub(localCameraPosition); const actualDistance = _dirToNode.length(); let penalizedDistance = actualDistance; let importanceMultiplier = 1.0; if (lodBehindPenalty > 1 && actualDistance > 0.01) { const dotOverDistance = localCameraForward.dot(_dirToNode) / actualDistance; if (dotOverDistance < 0) { const t = -dotOverDistance; const factor = 1 + t * (lodBehindPenalty - 1); penalizedDistance = actualDistance * factor; importanceMultiplier = 1.0 / factor; } } let optimalLodIndex = maxLod; for(let lod = 0; lod < maxLod; lod++){ if (penalizedDistance < lodDistances[lod]) { optimalLodIndex = lod; break; } } if (optimalLodIndex < rangeMin) optimalLodIndex = rangeMin; if (optimalLodIndex > rangeMax) optimalLodIndex = rangeMax; const normalizedDistance = Math.min(actualDistance / maxDistance, 1.0); const importance = (1.0 - normalizedDistance) * importanceMultiplier; nodeInfos[nodeIndex].optimalLod = optimalLodIndex; nodeInfos[nodeIndex].importance = importance; const lod = nodes[nodeIndex].lods[optimalLodIndex]; if (lod && lod.count) { totalSplats += lod.count; } } return totalSplats; } enforceSplatBudget(totalSplats, splatBudget, rangeMin, rangeMax) { const nodes = this.octree.nodes; const nodeInfos = this.nodeInfos; if (!this._nodeIndices) { this._nodeIndices = new Uint32Array(nodes.length); for(let i = 0; i < nodes.length; i++){ this._nodeIndices[i] = i; } } const nodeIndices = this._nodeIndices; nodeIndices.sort((a, b)=>nodeInfos[a].importance - nodeInfos[b].importance); let currentSplats = totalSplats; if (currentSplats === splatBudget) { return; } const isOverBudget = currentSplats > splatBudget; const lodDelta = isOverBudget ? 1 : -1; while(isOverBudget ? currentSplats > splatBudget : currentSplats < splatBudget){ let modified = false; if (isOverBudget) { for(let i = 0; i < nodeIndices.length; i++){ const nodeIndex = nodeIndices[i]; const nodeInfo = nodeInfos[nodeIndex]; const node = nodes[nodeIndex]; const currentOptimalLod = nodeInfo.optimalLod; if (currentOptimalLod < rangeMax) { const currentLod = node.lods[currentOptimalLod]; const nextLod = node.lods[currentOptimalLod + 1]; const splatsSaved = currentLod.count - nextLod.count; nodeInfo.optimalLod += lodDelta; currentSplats -= splatsSaved; modified = true; if (currentSplats <= splatBudget) { break; } } } } else { for(let i = nodeIndices.length - 1; i >= 0; i--){ const nodeIndex = nodeIndices[i]; const nodeInfo = nodeInfos[nodeIndex]; const node = nodes[nodeIndex]; const currentOptimalLod = nodeInfo.optimalLod; if (currentOptimalLod > rangeMin) { const currentLod = node.lods[currentOptimalLod]; const nextLod = node.lods[currentOptimalLod - 1]; const splatsAdded = nextLod.count - currentLod.count; if (currentSplats + splatsAdded <= splatBudget) { nodeInfo.optimalLod += lodDelta; currentSplats += splatsAdded; modified = true; if (currentSplats >= splatBudget) { break; } } } } } if (!modified) { break; } } } applyLodChanges(maxLod, params) { const nodes = this.octree.nodes; const { lodUnderfillLimit = 0 } = params; for(let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++){ const node = nodes[nodeIndex]; const nodeInfo = this.nodeInfos[nodeIndex]; const optimalLodIndex = nodeInfo.optimalLod; const currentLodIndex = nodeInfo.currentLod; const desiredLodIndex = this.selectDesiredLodIndex(node, optimalLodIndex, maxLod, lodUnderfillLimit); if (desiredLodIndex !== currentLodIndex) { const currentFileIndex = currentLodIndex >= 0 ? node.lods[currentLodIndex].fileIndex : -1; const desiredFileIndex = desiredLodIndex >= 0 ? node.lods[desiredLodIndex].fileIndex : -1; const wasVisible = currentFileIndex !== -1; const willBeVisible = desiredFileIndex !== -1; const pendingEntry = this.pendingDecrements.get(nodeIndex); if (pendingEntry) { if (pendingEntry.newFileIndex !== desiredFileIndex) { const prevPendingPlacement = this.filePlacements[pendingEntry.newFileIndex]; if (prevPendingPlacement) { this.decrementFileRef(pendingEntry.newFileIndex, nodeIndex); } if (wasVisible && willBeVisible) { this.pendingDecrements.set(nodeIndex, { oldFileIndex: pendingEntry.oldFileIndex, newFileIndex: desiredFileIndex }); } else { this.pendingDecrements.delete(nodeIndex); } } } if (!wasVisible && willBeVisible) { const prevPendingFi = this.pendingVisibleAdds.get(nodeIndex); if (prevPendingFi !== undefined && prevPendingFi !== desiredFileIndex) { this.decrementFileRef(prevPendingFi, nodeIndex); this.pendingVisibleAdds.delete(nodeIndex); } this.incrementFileRef(desiredFileIndex, nodeIndex, desiredLodIndex); const newPlacement = this.filePlacements[desiredFileIndex]; if (newPlacement?.resource) { nodeInfo.currentLod = desiredLodIndex; this.pendingVisibleAdds.delete(nodeIndex); } else { this.pendingVisibleAdds.set(nodeIndex, desiredFileIndex); } } else if (wasVisible && !willBeVisible) { const pendingEntry2 = this.pendingDecrements.get(nodeIndex); if (pendingEntry2) { this.decrementFileRef(pendingEntry2.newFileIndex, nodeIndex); this.pendingDecrements.delete(nodeIndex); } this.decrementFileRef(currentFileIndex, nodeIndex); nodeInfo.currentLod = -1; this.pendingVisibleAdds.delete(nodeIndex); } else if (wasVisible && willBeVisible) { this.incrementFileRef(desiredFileIndex, nodeIndex, desiredLodIndex); const newPlacement = this.filePlacements[desiredFileIndex]; if (newPlacement?.resource) { this.decrementFileRef(currentFileIndex, nodeIndex); this.pendingDecrements.delete(nodeIndex); nodeInfo.currentLod = desiredLodIndex; this.pendingVisibleAdds.delete(nodeIndex); } else { this.pendingDecrements.set(nodeIndex, { oldFileIndex: currentFileIndex, newFileIndex: desiredFileIndex }); this.pendingVisibleAdds.delete(nodeIndex); } } } this.prefetchNextLod(node, desiredLodIndex, optimalLodIndex); } } incrementFileRef(fileIndex, nodeIndex, lodIndex) { if (fileIndex === -1) return; let placement = this.filePlacements[fileIndex]; if (!placement) { placement = new GSplatPlacement(null, this.placement.node, lodIndex); this.filePlacements[fileIndex] = placement; const removeScheduled = this.removedCandidates.delete(fileIndex); if (!removeScheduled) { this.octree.incRefCount(fileIndex); } if (!this.addFilePlacement(fileIndex)) { this.octree.ensureFileResource(fileIndex); this.pending.add(fileIndex); } } const nodes = this.octree.nodes; const node = nodes[nodeIndex]; const lod = node.lods[lodIndex]; const interval = new Vec2(lod.offset, lod.offset + lod.count - 1); placement.intervals.set(nodeIndex, interval); this.dirtyModifiedPlacements = true; } decrementFileRef(fileIndex, nodeIndex) { if (fileIndex === -1) return; const placement = this.filePlacements[fileIndex]; if (!placement) { return; } if (placement) { placement.intervals.delete(nodeIndex); this.dirtyModifiedPlacements = true; if (placement.intervals.size === 0) { if (placement.resource) { this.activePlacements.delete(placement); } this.removedCandidates.add(fileIndex); this.filePlacements[fileIndex] = null; this.pending.delete(fileIndex); } } } addFilePlacement(fileIndex) { const res = this.octree.getFileResource(fileIndex); if (res) { const placement = this.filePlacements[fileIndex]; if (placement) { placement.resource = res; placement.aabb.copy(res.aabb); this.activePlacements.add(placement); this.dirtyModifiedPlacements = true; this.removedCandidates.delete(fileIndex); return true; } } return false; } testMoved(threshold) { const position = this.placement.node.getPosition(); const length = position.distance(this.previousPosition); if (length > threshold) { return true; } return false; } updateMoved() { this.previousPosition.copy(this.placement.node.getPosition()); } update(scene) { const currentBudget = this.placement.splatBudget; if (currentBudget !== this.splatBudget) { this.splatBudget = currentBudget; this.needsLodUpdate = true; } if (this.pending.size) { for (const fileIndex of this.pending){ this.octree.ensureFileResource(fileIndex); if (this.addFilePlacement(fileIndex)) { _tempCompletedUrls.push(fileIndex); for (const [nodeIndex, { oldFileIndex, newFileIndex }] of this.pendingDecrements){ if (newFileIndex === fileIndex) { this.decrementFileRef(oldFileIndex, nodeIndex); this.pendingDecrements.delete(nodeIndex); let newLodIndex = 0; const nodeLods = this.octree.nodes[nodeIndex].lods; for(let li = 0; li < nodeLods.length; li++){ if (nodeLods[li].fileIndex === newFileIndex) { newLodIndex = li; break; } } this.nodeInfos[nodeIndex].currentLod = newLodIndex; } } } } if (_tempCompletedUrls.length > 0) { this.needsLodUpdate = true; } for (const fileIndex of _tempCompletedUrls){ this.pending.delete(fileIndex); } _tempCompletedUrls.length = 0; } this.pollPrefetchCompletions(); if (this.octree.environmentUrl && !this.environmentPlacement) { this.octree.ensureEnvironmentResource(); const envResource = this.octree.environmentResource; if (envResource) { this.environmentPlacement = new GSplatPlacement(envResource, this.placement.node, 0); this.environmentPlacement.aabb.copy(envResource.aabb); this.activePlacements.add(this.environmentPlacement); this.dirtyModifiedPlacements = true; } } const dirty = this.dirtyModifiedPlacements; this.dirtyModifiedPlacements = false; return dirty; } debugRender(scene) {} consumeNeedsLodUpdate() { const v = this.needsLodUpdate; this.needsLodUpdate = false; return v; } pollPrefetchCompletions() { if (this.prefetchPending.size) { for (const fileIndex of this.prefetchPending){ this.octree.ensureFileResource(fileIndex); if (this.octree.getFileResource(fileIndex)) { _tempCompletedUrls.push(fileIndex); } } if (_tempCompletedUrls.length > 0) { this.needsLodUpdate = true; } for (const fileIndex of _tempCompletedUrls){ this.prefetchPending.delete(fileIndex); } _tempCompletedUrls.length = 0; } } constructor(device, octree, placement){ this.activePlacements = new Set(); this.dirtyModifiedPlacements = false; this.pending = new Set(); this.pendingDecrements = new Map(); this.removedCandidates = new Set(); this.previousPosition = new Vec3(); this.needsLodUpdate = false; this.prefetchPending = new Set(); this.pendingVisibleAdds = new Map(); this.splatBudget = 0; this._nodeIndices = null; this.environmentPlacement = null; this._deviceLostEvent = null; this.device = device; this.octree = octree; this.placement = placement; this.nodeInfos = new Array(octree.nodes.length); for(let i = 0; i < octree.nodes.length; i++){ this.nodeInfos[i] = new NodeInfo(); } const numFiles = octree.files.length; this.filePlacements = new Array(numFiles).fill(null); if (octree.environmentUrl) { octree.incEnvironmentRefCount(); octree.ensureEnvironmentResource(); } this._deviceLostEvent = device.on('devicelost', this._onDeviceLost, this); } } const tmpMin = new Vec3(); const tmpMax = new Vec3(); class GSplatOctreeNode { constructor(lods, boundData){ this.bounds = new BoundingBox(); this.lods = lods; tmpMin.set(boundData.min[0], boundData.min[1], boundData.min[2]); tmpMax.set(boundData.max[0], boundData.max[1], boundData.max[2]); this.bounds.setMinMax(tmpMin, tmpMax); } } const _toDelete = []; class GSplatOctree { destroy() { this.destroyed = true; this.fileResources.clear(); this.cooldowns.clear(); this.assetLoader?.destroy(); this.assetLoader = null; this.environmentResource = null; } _traceLodCounts() {} _extractLeafNodes(node, leafNodes) { if (node.lods) { leafNodes.push({ lods: node.lods, bound: node.bound }); } else if (node.children) { for (const child of node.children){ this._extractLeafNodes(child, leafNodes); } } } getFileResource(fileIndex) { return this.fileResources.get(fileIndex); } incRefCount(fileIndex) { const count = this.fileRefCounts[fileIndex] + 1; this.fileRefCounts[fileIndex] = count; this.cooldowns.delete(fileIndex); } decRefCount(fileIndex, cooldownTicks) { const count = this.fileRefCounts[fileIndex] - 1; this.fileRefCounts[fileIndex] = count; if (count === 0) { if (cooldownTicks === 0) { this.unloadResource(fileIndex); } else { this.cooldowns.set(fileIndex, cooldownTicks); } } } unloadResource(fileIndex) { if (!this.assetLoader) { return; } const fullUrl = this.files[fileIndex].url; this.assetLoader.unload(fullUrl); if (this.fileResources.has(fileIndex)) { this.fileResources.delete(fileIndex); this._traceLodCounts(); } } updateCooldownTick(cooldownTicks) { this.cooldownTicks = cooldownTicks; if (this.cooldowns.size > 0) { this.cooldowns.forEach((remaining, fileIndex)=>{ if (remaining <= 1) { if (this.fileRefCounts[fileIndex] === 0) { this.unloadResource(fileIndex); } _toDelete.push(fileIndex); } else { this.cooldowns.set(fileIndex, remaining - 1); } }); _toDelete.forEach((idx)=>this.cooldowns.delete(idx)); _toDelete.length = 0; } } ensureFileResource(fileIndex) { if (this.fileResources.has(fileIndex)) { return; } const fullUrl = this.files[fileIndex].url; const res = this.assetLoader?.getResource(fullUrl); if (res) { this.fileResources.set(fileIndex, res); if (this.fileRefCounts[fileIndex] === 0) { this.cooldowns.set(fileIndex, this.cooldownTicks); } this._traceLodCounts(); return; } this.assetLoader?.load(fullUrl); } incEnvironmentRefCount() { this.environmentRefCount++; } decEnvironmentRefCount() { this.environmentRefCount--; if (this.environmentRefCount === 0) { this.unloadEnvironmentResource(); } } ensureEnvironmentResource() { if (!this.assetLoader) { return; } if (!this.environmentUrl) { return; } if (this.environmentResource) { return; } const res = this.assetLoader.getResource(this.environmentUrl); if (res) { this.environmentResource = res; if (this.environmentRefCount === 0) { this.unloadEnvironmentResource(); } return; } this.assetLoader.load(this.environmentUrl); } unloadEnvironmentResource() { if (!this.assetLoader) { return; } if (this.environmentResource && this.environmentUrl) { this.assetLoader.unload(this.environmentUrl); this.environmentResource = null; } } constructor(assetFileUrl, data){ this.fileResources = new Map(); this.cooldowns = new Map(); this.environmentUrl = null; this.environmentResource = null; this.environmentRefCount = 0; this.assetLoader = null; this.destroyed = false; this.cooldownTicks = 100; this.lodLevels = data.lodLevels; this.assetFileUrl = assetFileUrl; const baseDir = path.getDirectory(assetFileUrl); this.files = data.filenames.map((url)=>({ url: path.isRelativePath(url) ? path.join(baseDir, url) : url, lodLevel: -1 })); this.fileRefCounts = new Int32Array(this.files.length); if (data.environment) { this.environmentUrl = path.isRelativePath(data.environment) ? path.join(baseDir, data.environment) : data.environment; } const leafNodes = []; this._extractLeafNodes(data.tree, leafNodes); this.nodes = leafNodes.map((nodeData)=>{ const lods = []; for(let i = 0; i < this.lodLevels; i++){ const lodData = nodeData.lods[i.toString()]; if (lodData) { lods.push({ file: this.files[lodData.file].url || '', fileIndex: lodData.file, offset: lodData.offset || 0, count: lodData.count || 0 }); this.files[lodData.file].lodLevel = i; } else { lods.push({ file: '', fileIndex: -1, offset: 0, count: 0 }); } } return new GSplatOctreeNode(lods, nodeData.bound); }); } } class GSplatOctreeResource { destroy() { this.octree?.destroy(); this.octree = null; } constructor(assetFileUrl, data, assetLoader){ this.aabb = new BoundingBox(); this.octree = new GSplatOctree(assetFileUrl, data); this.octree.assetLoader = assetLoader; this.aabb.setMinMax(new Vec3(data.tree.bound.min), new Vec3(data.tree.bound.max)); } } class GSplatWorldState { estimateTextureSize(splats, maxSize) { const fits = (size)=>{ let rows = 0; for (const splat of splats){ rows += Math.ceil(splat.activeSplats / size); if (rows > size) return false; } return true; }; let low = 1; let high = maxSize; let bestSize = null; while(low <= high){ const mid = Math.floor((low + high) / 2); if (fits(mid)) { bestSize = mid; high = mid - 1; } else { low = mid + 1; } } if (bestSize === null) { this.textureSize = 0; return false; } this.textureSize = bestSize; return true; } destroy() { this.splats.forEach((splat)=>splat.destroy()); this.splats.length = 0; } assignLines(splats, size) { if (splats.length === 0) { this.totalUsedPixels = 0; return; } let start = 0; for (const splat of splats){ const activeSplats = splat.activeSplats; const numLines = Math.ceil(activeSplats / size); splat.setLines(start, numLines, size, activeSplats); start += numLines; } this.totalUsedPixels = start * size; } constructor(device, version, splats){ this.version = 0; this.sortParametersSet = false; this.sortedBefore = false; this.splats = []; this.textureSize = 0; this.totalUsedPixels = 0; this.pendingReleases = []; this.version = version; this.splats = splats; this.estimateTextureSize(this.splats, device.maxTextureSize); this.assignLines(this.splats, this.textureSize); } } const computeGsplatSortKeySource = ` @group(0) @binding(0) var splatTexture0: texture_2d; @group(0) @binding(1) var sortKeys: array; struct SortKeyUniforms { cameraPosition: vec3f, elementCount: u32, cameraDirection: vec3f, numBits: u32, textureSize: u32, minDist: f32, invRange: f32, numWorkgroupsX: u32, numBins: u32 }; @group(0) @binding(2) var uniforms: SortKeyUniforms; struct BinWeight { base: f32, divider: f32 }; @group(0) @binding(3) var binWeights: array; @compute @workgroup_size({WORKGROUP_SIZE_X}, {WORKGROUP_SIZE_Y}, 1) fn computeSortKey(@builtin(global_invocation_id) global_id: vec3u) { let gid = global_id.x + global_id.y * ({WORKGROUP_SIZE_X} * uniforms.numWorkgroupsX); if (gid >= uniforms.elementCount) { return; } let textureSize = uniforms.textureSize; let uv = vec2i(i32(gid % textureSize), i32(gid / textureSize)); let packed = textureLoad(splatTexture0, uv, 0); let worldCenter = vec3f( bitcast(packed.r), bitcast(packed.g), bitcast(packed.b) ); var dist: f32; #ifdef RADIAL_SORT let delta = worldCenter - uniforms.cameraPosition; let radialDist = length(delta); dist = (1.0 / uniforms.invRange) - radialDist - uniforms.minDist; #else let toSplat = worldCenter - uniforms.cameraPosition; dist = dot(toSplat, uniforms.cameraDirection) - uniforms.minDist; #endif let numBins = uniforms.numBins; let d = dist * uniforms.invRange * f32(numBins); let binFloat = clamp(d, 0.0, f32(numBins) - 0.001); let bin = u32(binFloat); let binFrac = binFloat - f32(bin); let sortKey = u32(binWeights[bin].base + binWeights[bin].divider * binFrac); sortKeys[gid] = sortKey; } `; const WORKGROUP_SIZE_X = 16; const WORKGROUP_SIZE_Y = 16; const THREADS_PER_WORKGROUP = WORKGROUP_SIZE_X * WORKGROUP_SIZE_Y; const _cameraDir = new Vec3(); class GSplatSortKeyCompute { destroy() { this.keysBuffer?.destroy(); this.binWeightsBuffer?.destroy(); this.compute?.shader?.destroy(); this.bindGroupFormat?.destroy(); this.keysBuffer = null; this.binWeightsBuffer = null; this.compute = null; this.bindGroupFormat = null; this.uniformBufferFormat = null; } _getCompute(radialSort) { if (!this.compute || this.computeRadialSort !== radialSort) { this.compute?.shader?.destroy(); const name = radialSort ? 'GSplatSortKeyCompute-Radial' : 'GSplatSortKeyCompute-Linear'; const cdefines = new Map([ [ '{WORKGROUP_SIZE_X}', `${WORKGROUP_SIZE_X}` ], [ '{WORKGROUP_SIZE_Y}', `${WORKGROUP_SIZE_Y}` ] ]); if (radialSort) { cdefines.set('RADIAL_SORT', ''); } const shader = new Shader(this.device, { name: name, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatSortKeySource, cdefines: cdefines, computeEntryPoint: 'computeSortKey', computeBindGroupFormat: this.bindGroupFormat, computeUniformBufferFormats: { uniforms: this.uniformBufferFormat } }); this.compute = new Compute(this.device, shader, name); this.computeRadialSort = radialSort; } return this.compute; } _createBindGroupFormat() { const device = this.device; this.uniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat('cameraPosition', UNIFORMTYPE_VEC3), new UniformFormat('elementCount', UNIFORMTYPE_UINT), new UniformFormat('cameraDirection', UNIFORMTYPE_VEC3), new UniformFormat('numBits', UNIFORMTYPE_UINT), new UniformFormat('textureSize', UNIFORMTYPE_UINT), new UniformFormat('minDist', UNIFORMTYPE_FLOAT), new UniformFormat('invRange', UNIFORMTYPE_FLOAT), new UniformFormat('numWorkgroupsX', UNIFORMTYPE_UINT), new UniformFormat('numBins', UNIFORMTYPE_UINT) ]); this.bindGroupFormat = new BindGroupFormat(device, [ new BindTextureFormat('splatTexture0', SHADERSTAGE_COMPUTE, undefined, SAMPLETYPE_UINT, false), new BindStorageBufferFormat('sortKeys', SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE), new BindStorageBufferFormat('binWeights', SHADERSTAGE_COMPUTE, true) ]); } _ensureCapacity(elementCount) { if (elementCount > this.allocatedCount) { this.keysBuffer?.destroy(); this.allocatedCount = elementCount; this.keysBuffer = new StorageBuffer(this.device, elementCount * 4, BUFFERUSAGE_COPY_SRC); } } generate(workBuffer, cameraNode, radialSort, elementCount, numBits, minDist, maxDist) { this._ensureCapacity(elementCount); const workgroupCount = Math.ceil(elementCount / THREADS_PER_WORKGROUP); const numWorkgroupsX = Math.min(workgroupCount, this.device.limits.maxComputeWorkgroupsPerDimension || 65535); const numWorkgroupsY = Math.ceil(workgroupCount / numWorkgroupsX); const compute = this._getCompute(radialSort); const cameraPos = cameraNode.getPosition(); const cameraMat = cameraNode.getWorldTransform(); const cameraDir = cameraMat.getZ(_cameraDir).normalize(); const range = maxDist - minDist; const invRange = range > 0 ? 1.0 / range : 1.0; const bucketCount = 1 << numBits; const cameraBin = GSplatSortBinWeights$1.computeCameraBin(radialSort, minDist, range); const binWeights = this.binWeightsUtil.compute(cameraBin, bucketCount); this.binWeightsBuffer.write(0, binWeights); compute.setParameter('splatTexture0', workBuffer.splatTexture0); compute.setParameter('sortKeys', this.keysBuffer); compute.setParameter('binWeights', this.binWeightsBuffer); this.cameraPositionData[0] = cameraPos.x; this.cameraPositionData[1] = cameraPos.y; this.cameraPositionData[2] = cameraPos.z; compute.setParameter('cameraPosition', this.cameraPositionData); this.cameraDirectionData[0] = cameraDir.x; this.cameraDirectionData[1] = cameraDir.y; this.cameraDirectionData[2] = cameraDir.z; compute.setParameter('cameraDirection', this.cameraDirectionData); compute.setParameter('elementCount', elementCount); compute.setParameter('numBits', numBits); compute.setParameter('textureSize', workBuffer.textureSize); compute.setParameter('minDist', minDist); compute.setParameter('invRange', invRange); compute.setParameter('numWorkgroupsX', numWorkgroupsX); compute.setParameter('numBins', GSplatSortBinWeights$1.NUM_BINS); compute.setupDispatch(numWorkgroupsX, numWorkgroupsY, 1); this.device.computeDispatch([ compute ], 'GSplatSortKeyCompute'); return this.keysBuffer; } constructor(device){ this.allocatedCount = 0; this.keysBuffer = null; this.binWeightsBuffer = null; this.compute = null; this.computeRadialSort = false; this.bindGroupFormat = null; this.uniformBufferFormat = null; this.cameraPositionData = new Float32Array(3); this.cameraDirectionData = new Float32Array(3); this.device = device; this.binWeightsUtil = new GSplatSortBinWeights$1(); this.binWeightsBuffer = new StorageBuffer(device, GSplatSortBinWeights$1.NUM_BINS * 2 * 4, BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST); this._createBindGroupFormat(); } } const cameraPosition = new Vec3(); const cameraDirection = new Vec3(); const translation = new Vec3(); const _cornerPoint = new Vec3(); const invModelMat = new Mat4(); const tempNonOctreePlacements = new Set(); const tempOctreePlacements = new Set(); const _updatedSplats = []; const _splatsNeedingColorUpdate = []; const _cameraDeltas = { rotationDelta: 0, translationDelta: 0 }; const tempOctreesTicked = new Set(); const _lodColorsRaw = [ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 1, 0 ], [ 1, 0, 1 ], [ 0, 1, 1 ], [ 1, 0.5, 0 ], [ 0.5, 0, 1 ] ]; [ new Color(1, 0, 0), new Color(0, 1, 0), new Color(0, 0, 1), new Color(1, 1, 0), new Color(1, 0, 1), new Color(0, 1, 1), new Color(1, 0.5, 0), new Color(0.5, 0, 1) ]; let _randomColorRaw = null; class GSplatManager { setRenderMode(renderMode) { this.renderMode = renderMode; this.renderer.setRenderMode(renderMode); } destroy() { this._destroyed = true; for (const [, worldState] of this.worldStates){ for (const splat of worldState.splats){ splat.resource.decRefCount(); } worldState.destroy(); } this.worldStates.clear(); for (const [, instance] of this.octreeInstances){ instance.destroy(); } this.octreeInstances.clear(); for (const instance of this.octreeInstancesToDestroy){ instance.destroy(); } this.octreeInstancesToDestroy.length = 0; this.workBuffer.destroy(); this.renderer.destroy(); this.keyGenerator?.destroy(); this.gpuSorter?.destroy(); this.cpuSorter?.destroy(); } get material() { return this.renderer.material; } createSorter() { const sorter = new GSplatUnifiedSorter(this.scene); sorter.on('sorted', (count, version, orderData)=>{ this.onSorted(count, version, orderData); }); return sorter; } reconcile(placements) { tempNonOctreePlacements.clear(); for (const p of placements){ if (p.resource instanceof GSplatOctreeResource) { if (!this.octreeInstances.has(p)) { this.octreeInstances.set(p, new GSplatOctreeInstance(this.device, p.resource.octree, p)); this.hasNewOctreeInstances = true; } tempOctreePlacements.add(p); } else { tempNonOctreePlacements.add(p); } } for (const [placement, inst] of this.octreeInstances){ if (!tempOctreePlacements.has(placement)) { this.octreeInstances.delete(placement); this.layerPlacementsDirty = true; this.octreeInstancesToDestroy.push(inst); } } this.layerPlacementsDirty = this.layerPlacements.length !== tempNonOctreePlacements.size; if (!this.layerPlacementsDirty) { for(let i = 0; i < this.layerPlacements.length; i++){ const existing = this.layerPlacements[i]; if (!tempNonOctreePlacements.has(existing)) { this.layerPlacementsDirty = true; break; } } } this.layerPlacements.length = 0; for (const p of tempNonOctreePlacements){ this.layerPlacements.push(p); } tempNonOctreePlacements.clear(); tempOctreePlacements.clear(); } updateWorldState() { const worldChanged = this.layerPlacementsDirty || this.worldStates.size === 0; if (worldChanged) { this.lastWorldStateVersion++; const splats = []; const { colorUpdateAngle, colorUpdateDistance } = this.scene.gsplat; for (const p of this.layerPlacements){ const splatInfo = new GSplatInfo(this.device, p.resource, p); splatInfo.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance); splats.push(splatInfo); } for (const [, inst] of this.octreeInstances){ inst.activePlacements.forEach((p)=>{ if (p.resource) { const splatInfo = new GSplatInfo(this.device, p.resource, p); splatInfo.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance); splats.push(splatInfo); } }); } this.cpuSorter?.updateCentersForSplats(splats); const newState = new GSplatWorldState(this.device, this.lastWorldStateVersion, splats); for (const splat of newState.splats){ splat.resource.incRefCount(); } for (const [, inst] of this.octreeInstances){ if (inst.removedCandidates && inst.removedCandidates.size) { for (const fileIndex of inst.removedCandidates){ newState.pendingReleases.push([ inst.octree, fileIndex ]); } inst.removedCandidates.clear(); } } if (this.octreeInstancesToDestroy.length) { for (const inst of this.octreeInstancesToDestroy){ const toRelease = inst.getFileDecrements(); for (const fileIndex of toRelease){ newState.pendingReleases.push([ inst.octree, fileIndex ]); } inst.destroy(); } this.octreeInstancesToDestroy.length = 0; } this.worldStates.set(this.lastWorldStateVersion, newState); this.layerPlacementsDirty = false; this.sortNeeded = true; } } onSorted(count, version, orderData) { this.cleanupOldWorldStates(version); this.sortedVersion = version; const worldState = this.worldStates.get(version); if (worldState) { if (!worldState.sortedBefore) { worldState.sortedBefore = true; this.rebuildWorkBuffer(worldState, count); } this.workBuffer.setOrderData(orderData); this.renderer.setOrderData(); } } rebuildWorkBuffer(worldState, count) { const textureSize = worldState.textureSize; if (textureSize !== this.workBuffer.textureSize) { this.workBuffer.resize(textureSize); this.renderer.setMaxNumSplats(textureSize * textureSize); } this.workBuffer.render(worldState.splats, this.cameraNode, this.getDebugColors()); const { colorUpdateAngle, colorUpdateDistance } = this.scene.gsplat; worldState.splats.forEach((splat)=>{ splat.update(); splat.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance); }); this.updateColorCameraTracking(); if (worldState.pendingReleases && worldState.pendingReleases.length) { const cooldownTicks = this.scene.gsplat.cooldownTicks; for (const [octree, fileIndex] of worldState.pendingReleases){ octree.decRefCount(fileIndex, cooldownTicks); } worldState.pendingReleases.length = 0; } this.renderer.update(count, textureSize); } cleanupOldWorldStates(newVersion) { for(let v = this.sortedVersion; v < newVersion; v++){ const oldState = this.worldStates.get(v); if (oldState) { for (const splat of oldState.splats){ splat.resource.decRefCount(); } this.worldStates.delete(v); oldState.destroy(); } } } applyWorkBufferUpdates(state) { const { colorUpdateAngle, colorUpdateDistance, colorUpdateDistanceLodScale, colorUpdateAngleLodScale } = this.scene.gsplat; const { rotationDelta, translationDelta } = this.calculateColorCameraDeltas(); state.splats.forEach((splat)=>{ if (splat.update()) { _updatedSplats.push(splat); splat.resetColorAccumulators(colorUpdateAngle, colorUpdateDistance); this.sortNeeded = true; } else if (splat.hasSphericalHarmonics) { splat.colorAccumulatedRotation += rotationDelta; splat.colorAccumulatedTranslation += translationDelta; const lodIndex = splat.lodIndex ?? 0; const distThreshold = colorUpdateDistance * Math.pow(colorUpdateDistanceLodScale, lodIndex); const angleThreshold = colorUpdateAngle * Math.pow(colorUpdateAngleLodScale, lodIndex); if (splat.colorAccumulatedRotation >= angleThreshold || splat.colorAccumulatedTranslation >= distThreshold) { _splatsNeedingColorUpdate.push(splat); splat.resetColorAccumulators(angleThreshold, distThreshold); } } }); if (_updatedSplats.length > 0) { this.workBuffer.render(_updatedSplats, this.cameraNode, this.getDebugColors()); _updatedSplats.length = 0; } if (_splatsNeedingColorUpdate.length > 0) { this.workBuffer.renderColor(_splatsNeedingColorUpdate, this.cameraNode, this.getDebugColors()); _splatsNeedingColorUpdate.length = 0; } } testCameraMovedForLod() { const distanceThreshold = this.scene.gsplat.lodUpdateDistance; const currentCameraPos = this.cameraNode.getPosition(); const cameraMoved = this.lastLodCameraPos.distance(currentCameraPos) > distanceThreshold; if (cameraMoved) { return true; } let cameraRotated = false; const lodUpdateAngleDeg = this.scene.gsplat.lodUpdateAngle; if (lodUpdateAngleDeg > 0) { if (Number.isFinite(this.lastLodCameraFwd.x)) { const currentCameraFwd = this.cameraNode.forward; const dot = Math.min(1, Math.max(-1, this.lastLodCameraFwd.dot(currentCameraFwd))); const angle = Math.acos(dot); const rotThreshold = lodUpdateAngleDeg * math.DEG_TO_RAD; cameraRotated = angle > rotThreshold; } else { cameraRotated = true; } } return cameraMoved || cameraRotated; } testCameraMovedForSort() { const epsilon = 0.001; if (this.scene.gsplat.radialSorting) { const currentCameraPos = this.cameraNode.getPosition(); const distance = this.lastSortCameraPos.distance(currentCameraPos); return distance > epsilon; } if (Number.isFinite(this.lastSortCameraFwd.x)) { const currentCameraFwd = this.cameraNode.forward; const dot = Math.min(1, Math.max(-1, this.lastSortCameraFwd.dot(currentCameraFwd))); const angle = Math.acos(dot); return angle > epsilon; } return true; } updateColorCameraTracking() { this.lastColorUpdateCameraPos.copy(this.cameraNode.getPosition()); this.lastColorUpdateCameraFwd.copy(this.cameraNode.forward); } getDebugColors() { if (this.scene.gsplat.colorizeColorUpdate) { _randomColorRaw ?? (_randomColorRaw = []); const r = Math.random(); const g = Math.random(); const b = Math.random(); for(let i = 0; i < _lodColorsRaw.length; i++){ var _randomColorRaw1, _i; (_randomColorRaw1 = _randomColorRaw)[_i = i] ?? (_randomColorRaw1[_i] = [ 0, 0, 0 ]); _randomColorRaw[i][0] = r; _randomColorRaw[i][1] = g; _randomColorRaw[i][2] = b; } return _randomColorRaw; } else if (this.scene.gsplat.colorizeLod) { return _lodColorsRaw; } return undefined; } calculateColorCameraDeltas() { _cameraDeltas.rotationDelta = 0; _cameraDeltas.translationDelta = 0; if (isFinite(this.lastColorUpdateCameraPos.x)) { const currentCameraFwd = this.cameraNode.forward; const dot = Math.min(1, Math.max(-1, this.lastColorUpdateCameraFwd.dot(currentCameraFwd))); _cameraDeltas.rotationDelta = Math.acos(dot) * math.RAD_TO_DEG; const currentCameraPos = this.cameraNode.getPosition(); _cameraDeltas.translationDelta = this.lastColorUpdateCameraPos.distance(currentCameraPos); } return _cameraDeltas; } fireFrameReadyEvent() { const ready = this.sortedVersion === this.lastWorldStateVersion; let loadingCount = 0; for (const [, inst] of this.octreeInstances){ loadingCount += inst.pendingLoadCount; } this.director.eventHandler.fire('frame:ready', this.cameraNode.camera, this.renderer.layer, ready, loadingCount); } update() { if (this.cpuSorter) { this.cpuSorter.applyPendingSorted(); } const sorterAvailable = this.useGpuSorting || this.cpuSorter && this.cpuSorter.jobsInFlight < 3; let fullUpdate = false; this.framesTillFullUpdate--; if (this.framesTillFullUpdate <= 0) { this.framesTillFullUpdate = 10; if (sorterAvailable) { fullUpdate = true; } } const hasNewInstances = this.hasNewOctreeInstances && sorterAvailable; if (hasNewInstances) this.hasNewOctreeInstances = false; let anyInstanceNeedsLodUpdate = false; let anyOctreeMoved = false; let cameraMovedOrRotatedForLod = false; if (fullUpdate) { for (const [, inst] of this.octreeInstances){ const isDirty = inst.update(this.scene); this.layerPlacementsDirty || (this.layerPlacementsDirty = isDirty); const instNeeds = inst.consumeNeedsLodUpdate(); anyInstanceNeedsLodUpdate || (anyInstanceNeedsLodUpdate = instNeeds); } const threshold = this.scene.gsplat.lodUpdateDistance; for (const [, inst] of this.octreeInstances){ const moved = inst.testMoved(threshold); anyOctreeMoved || (anyOctreeMoved = moved); } cameraMovedOrRotatedForLod = this.testCameraMovedForLod(); } if (this.testCameraMovedForSort()) { this.sortNeeded = true; } if (this.scene.gsplat.dirty) { this.layerPlacementsDirty = true; this.renderer.updateOverdrawMode(this.scene.gsplat); } if (cameraMovedOrRotatedForLod || anyOctreeMoved || this.scene.gsplat.dirty || anyInstanceNeedsLodUpdate || hasNewInstances) { for (const [, inst] of this.octreeInstances){ inst.updateMoved(); } this.lastLodCameraPos.copy(this.cameraNode.getPosition()); this.lastLodCameraFwd.copy(this.cameraNode.forward); for (const [, inst] of this.octreeInstances){ inst.updateLod(this.cameraNode, this.scene.gsplat); } } this.updateWorldState(); const lastState = this.worldStates.get(this.lastWorldStateVersion); if (lastState) { if (this.cpuSorter && !lastState.sortParametersSet) { lastState.sortParametersSet = true; const payload = this.prepareSortParameters(lastState); this.cpuSorter.setSortParameters(payload); } } const sortedState = this.worldStates.get(this.sortedVersion); if (sortedState) { this.applyWorkBufferUpdates(sortedState); } if (this.sortNeeded && lastState) { if (this.useGpuSorting) { this.sortGpu(lastState); } else { this.sortCpu(lastState); } this.sortNeeded = false; this.lastSortCameraPos.copy(this.cameraNode.getPosition()); this.lastSortCameraFwd.copy(this.cameraNode.forward); } if (sortedState) { this.renderer.frameUpdate(this.scene.gsplat); this.updateColorCameraTracking(); } if (this.octreeInstances.size) { const cooldownTicks = this.scene.gsplat.cooldownTicks; for (const [, inst] of this.octreeInstances){ const octree = inst.octree; if (!tempOctreesTicked.has(octree)) { tempOctreesTicked.add(octree); octree.updateCooldownTick(cooldownTicks); } } tempOctreesTicked.clear(); } this.fireFrameReadyEvent(); const { textureSize } = this.workBuffer; return textureSize * textureSize; } sortGpu(worldState) { const keyGenerator = this.keyGenerator; const gpuSorter = this.gpuSorter; if (!keyGenerator || !gpuSorter) return; const elementCount = worldState.totalUsedPixels; if (elementCount === 0) return; if (!worldState.sortedBefore) { worldState.sortedBefore = true; this.rebuildWorkBuffer(worldState, elementCount); this.cleanupOldWorldStates(worldState.version); this.sortedVersion = worldState.version; } const numBits = Math.max(10, Math.min(20, Math.round(Math.log2(elementCount / 4)))); const roundedNumBits = Math.ceil(numBits / 4) * 4; const { minDist, maxDist } = this.computeDistanceRange(worldState); const keysBuffer = keyGenerator.generate(this.workBuffer, this.cameraNode, this.scene.gsplat.radialSorting, elementCount, roundedNumBits, minDist, maxDist); const sortedIndices = gpuSorter.sort(keysBuffer, elementCount, roundedNumBits); this.renderer.setOrderBuffer(sortedIndices); this.renderer.update(elementCount, worldState.textureSize); } computeDistanceRange(worldState) { const cameraNode = this.cameraNode; const cameraMat = cameraNode.getWorldTransform(); cameraMat.getTranslation(cameraPosition); cameraMat.getZ(cameraDirection).normalize(); const radialSort = this.scene.gsplat.radialSorting; let minDist = radialSort ? 0 : Infinity; let maxDist = radialSort ? 0 : -Infinity; for (const splat of worldState.splats){ const modelMat = splat.node.getWorldTransform(); const aabbMin = splat.aabb.getMin(); const aabbMax = splat.aabb.getMax(); for(let i = 0; i < 8; i++){ _cornerPoint.x = i & 1 ? aabbMax.x : aabbMin.x; _cornerPoint.y = i & 2 ? aabbMax.y : aabbMin.y; _cornerPoint.z = i & 4 ? aabbMax.z : aabbMin.z; modelMat.transformPoint(_cornerPoint, _cornerPoint); if (radialSort) { const dist = _cornerPoint.distance(cameraPosition); if (dist > maxDist) maxDist = dist; } else { const dist = _cornerPoint.sub(cameraPosition).dot(cameraDirection); if (dist < minDist) minDist = dist; if (dist > maxDist) maxDist = dist; } } } if (maxDist === 0 || maxDist === -Infinity) { return { minDist: 0, maxDist: 1 }; } return { minDist, maxDist }; } sortCpu(lastState) { if (!this.cpuSorter) return; const cameraNode = this.cameraNode; const cameraMat = cameraNode.getWorldTransform(); cameraMat.getTranslation(cameraPosition); cameraMat.getZ(cameraDirection).normalize(); const sorterRequest = []; lastState.splats.forEach((splat)=>{ const modelMat = splat.node.getWorldTransform(); invModelMat.copy(modelMat).invert(); const uniformScale = modelMat.getScale().x; const transformedDirection = invModelMat.transformVector(cameraDirection).normalize(); const transformedPosition = invModelMat.transformPoint(cameraPosition); modelMat.getTranslation(translation); const offset = translation.sub(cameraPosition).dot(cameraDirection); const aabbMin = splat.aabb.getMin(); const aabbMax = splat.aabb.getMax(); sorterRequest.push({ transformedDirection, transformedPosition, offset, scale: uniformScale, modelMat: modelMat.data.slice(), aabbMin: [ aabbMin.x, aabbMin.y, aabbMin.z ], aabbMax: [ aabbMax.x, aabbMax.y, aabbMax.z ] }); }); this.cpuSorter.setSortParams(sorterRequest, this.scene.gsplat.radialSorting); } prepareSortParameters(worldState) { return { command: 'intervals', textureSize: worldState.textureSize, totalUsedPixels: worldState.totalUsedPixels, version: worldState.version, ids: worldState.splats.map((splat)=>splat.resource.id), lineStarts: worldState.splats.map((splat)=>splat.lineStart), padding: worldState.splats.map((splat)=>splat.padding), intervals: worldState.splats.map((splat)=>splat.intervals) }; } constructor(device, director, layer, cameraNode){ this.node = new GraphNode('GSplatManager'); this.worldStates = new Map(); this.lastWorldStateVersion = 0; this.useGpuSorting = false; this.cpuSorter = null; this.keyGenerator = null; this.gpuSorter = null; this.sortedVersion = 0; this.framesTillFullUpdate = 0; this.lastLodCameraPos = new Vec3(Infinity, Infinity, Infinity); this.lastLodCameraFwd = new Vec3(Infinity, Infinity, Infinity); this.lastSortCameraPos = new Vec3(Infinity, Infinity, Infinity); this.lastSortCameraFwd = new Vec3(Infinity, Infinity, Infinity); this.sortNeeded = true; this.lastColorUpdateCameraPos = new Vec3(Infinity, Infinity, Infinity); this.lastColorUpdateCameraFwd = new Vec3(Infinity, Infinity, Infinity); this.layerPlacements = []; this.layerPlacementsDirty = false; this.octreeInstances = new Map(); this.octreeInstancesToDestroy = []; this.hasNewOctreeInstances = false; this.device = device; this.scene = director.scene; this.director = director; this.cameraNode = cameraNode; this.workBuffer = new GSplatWorkBuffer(device); this.renderer = new GSplatRenderer(device, this.node, this.cameraNode, layer, this.workBuffer); this.useGpuSorting = device.isWebGPU && director.scene.gsplat.gpuSorting; if (this.useGpuSorting) { this.keyGenerator = new GSplatSortKeyCompute(device); this.gpuSorter = new ComputeRadixSort(device); } else { this.cpuSorter = this.createSorter(); } } } class SetUtils { static equals(setA, setB) { if (setA.size !== setB.size) { return false; } for (const elem of setA){ if (!setB.has(elem)) { return false; } } return true; } } const tempLayersToRemove = []; class GSplatLayerData { createManager(device, director, layer, cameraNode, camera, renderMode) { const manager = new GSplatManager(device, director, layer, cameraNode); manager.setRenderMode(renderMode); if (director.eventHandler) { director.eventHandler.fire('material:created', manager.material, camera, layer); } return manager; } updateConfiguration(device, director, layer, camera) { const cameraNode = camera.node; const hasNormalPlacements = layer.gsplatPlacements.length > 0; const hasShadowCasters = layer.gsplatShadowCasters.length > 0; const setsEqual = SetUtils.equals(layer.gsplatPlacementsSet, layer.gsplatShadowCastersSet); const useSharedManager = setsEqual && hasNormalPlacements; const desiredMainMode = useSharedManager ? GSPLAT_FORWARD | GSPLAT_SHADOW : hasNormalPlacements ? GSPLAT_FORWARD : 0; const desiredShadowMode = useSharedManager ? 0 : hasShadowCasters ? GSPLAT_SHADOW : 0; if (desiredMainMode) { if (this.gsplatManager) { this.gsplatManager.setRenderMode(desiredMainMode); } else { this.gsplatManager = this.createManager(device, director, layer, cameraNode, camera, desiredMainMode); } } else if (this.gsplatManager) { this.gsplatManager.destroy(); this.gsplatManager = null; } if (desiredShadowMode) { if (this.gsplatManagerShadow) { this.gsplatManagerShadow.setRenderMode(desiredShadowMode); } else { this.gsplatManagerShadow = this.createManager(device, director, layer, cameraNode, camera, desiredShadowMode); } } else if (this.gsplatManagerShadow) { this.gsplatManagerShadow.destroy(); this.gsplatManagerShadow = null; } } destroy() { this.gsplatManager?.destroy(); this.gsplatManager = null; this.gsplatManagerShadow?.destroy(); this.gsplatManagerShadow = null; } constructor(device, director, layer, camera){ this.gsplatManager = null; this.gsplatManagerShadow = null; this.updateConfiguration(device, director, layer, camera); } } class GSplatCameraData { destroy() { this.layersMap.forEach((layerData)=>layerData.destroy()); this.layersMap.clear(); } removeLayerData(layer) { const layerData = this.layersMap.get(layer); if (layerData) { layerData.destroy(); this.layersMap.delete(layer); } } getLayerData(device, director, layer, camera) { let layerData = this.layersMap.get(layer); if (!layerData) { layerData = new GSplatLayerData(device, director, layer, camera); this.layersMap.set(layer, layerData); } return layerData; } constructor(){ this.layersMap = new Map(); } } class GSplatDirector { destroy() { this.camerasMap.forEach((cameraData)=>cameraData.destroy()); this.camerasMap.clear(); } getCameraData(camera) { let cameraData = this.camerasMap.get(camera); if (!cameraData) { cameraData = new GSplatCameraData(); this.camerasMap.set(camera, cameraData); } return cameraData; } update(comp) { this.camerasMap.forEach((cameraData, camera)=>{ if (!comp.camerasSet.has(camera)) { cameraData.destroy(); this.camerasMap.delete(camera); } else { cameraData.layersMap.forEach((layerData, layer)=>{ if (!camera.layersSet.has(layer.id) || !layer.enabled) { tempLayersToRemove.push(layer); } }); for(let i = 0; i < tempLayersToRemove.length; i++){ const layer = tempLayersToRemove[i]; const layerData = cameraData.layersMap.get(layer); if (layerData) { layerData.destroy(); cameraData.layersMap.delete(layer); } } tempLayersToRemove.length = 0; } }); let gsplatCount = 0; const camerasComponents = comp.cameras; for(let i = 0; i < camerasComponents.length; i++){ const camera = camerasComponents[i].camera; let cameraData = this.camerasMap.get(camera); const layerIds = camera.layers; for(let j = 0; j < layerIds.length; j++){ const layer = comp.getLayerById(layerIds[j]); if (layer?.enabled) { if (layer.gsplatPlacementsDirty || !cameraData) { const hasNormalPlacements = layer.gsplatPlacements.length > 0; const hasShadowCasters = layer.gsplatShadowCasters.length > 0; if (!hasNormalPlacements && !hasShadowCasters) { if (cameraData) { cameraData.removeLayerData(layer); } } else { cameraData ?? (cameraData = this.getCameraData(camera)); const layerData = cameraData.getLayerData(this.device, this, layer, camera); layerData.updateConfiguration(this.device, this, layer, camera); if (layerData.gsplatManager) { layerData.gsplatManager.reconcile(layer.gsplatPlacements); } if (layerData.gsplatManagerShadow) { layerData.gsplatManagerShadow.reconcile(layer.gsplatShadowCasters); } } } } } if (cameraData) { for (const layerData of cameraData.layersMap.values()){ if (layerData.gsplatManager) { gsplatCount += layerData.gsplatManager.update(); } if (layerData.gsplatManagerShadow) { gsplatCount += layerData.gsplatManagerShadow.update(); } } } } this.renderer._gsplatCount = gsplatCount; this.scene.gsplat.frameEnd(); for(let i = 0; i < comp.layerList.length; i++){ comp.layerList[i].gsplatPlacementsDirty = false; } } constructor(device, renderer, scene, eventHandler){ this.camerasMap = new Map(); this.device = device; this.renderer = renderer; this.scene = scene; this.eventHandler = eventHandler; } } class GSplatComponent extends Component { set customAabb(value) { this._customAabb = value; this._instance?.meshInstance?.setCustomAabb(this._customAabb); if (this._placement && this._customAabb) { this._placement.aabb = this._customAabb; } } get customAabb() { return this._customAabb; } set instance(value) { if (this.unified) { return; } this.destroyInstance(); this._instance = value; if (this._instance) { const mi = this._instance.meshInstance; if (!mi.node) { mi.node = this.entity; } mi.castShadow = this._castShadows; mi.setCustomAabb(this._customAabb); if (this.enabled && this.entity.enabled) { this.addToLayers(); } } } get instance() { return this._instance; } set material(value) { if (this.unified) { return; } if (this._instance) { this._instance.material = value; } else { this._materialTmp = value; } } get material() { if (this.unified) { return null; } return this._instance?.material ?? this._materialTmp ?? null; } set highQualitySH(value) { if (value !== this._highQualitySH) { this._highQualitySH = value; this._instance?.setHighQualitySH(value); } } get highQualitySH() { return this._highQualitySH; } set castShadows(value) { if (this._castShadows !== value) { const layers = this.layers; const scene = this.system.app.scene; if (this._placement) { if (value) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); layer?.addGSplatShadowCaster(this._placement); } } else { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); layer?.removeGSplatShadowCaster(this._placement); } } } const mi = this.instance?.meshInstance; if (mi) { if (this._castShadows && !value) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(this.layers[i]); layer?.removeShadowCasters([ mi ]); } } mi.castShadow = value; if (!this._castShadows && value) { for(let i = 0; i < layers.length; i++){ const layer = scene.layers.getLayerById(layers[i]); layer?.addShadowCasters([ mi ]); } } } this._castShadows = value; } } get castShadows() { return this._castShadows; } set lodDistances(value) { this._lodDistances = Array.isArray(value) ? value.slice() : null; if (this._placement) { this._placement.lodDistances = this._lodDistances; } } get lodDistances() { return this._lodDistances ? this._lodDistances.slice() : null; } set splatBudget(value) { this._splatBudget = value; if (this._placement) { this._placement.splatBudget = this._splatBudget; } } get splatBudget() { return this._splatBudget; } set unified(value) { if (this._unified !== value) { this._unified = value; this._onGSplatAssetAdded(); } } get unified() { return this._unified; } set layers(value) { this.removeFromLayers(); this._layers.length = 0; for(let i = 0; i < value.length; i++){ this._layers[i] = value[i]; } if (!this.enabled || !this.entity.enabled) { return; } this.addToLayers(); } get layers() { return this._layers; } set asset(value) { const id = value instanceof Asset ? value.id : value; if (this._assetReference.id === id) return; if (this._assetReference.asset && this._assetReference.asset.resource) { this._onGSplatAssetRemove(); } this._assetReference.id = id; if (this._assetReference.asset) { this._onGSplatAssetAdded(); } } get asset() { return this._assetReference.id; } destroyInstance() { if (this._placement) { this.removeFromLayers(); this._placement = null; } if (this._instance) { this.removeFromLayers(); this._instance?.destroy(); this._instance = null; } } addToLayers() { if (this._placement) { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (layer) { layer.addGSplatPlacement(this._placement); if (this._castShadows) { layer.addGSplatShadowCaster(this._placement); } } } return; } const meshInstance = this.instance?.meshInstance; if (meshInstance) { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ layers.getLayerById(this._layers[i])?.addMeshInstances([ meshInstance ]); } } } removeFromLayers() { if (this._placement) { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ const layer = layers.getLayerById(this._layers[i]); if (layer) { layer.removeGSplatPlacement(this._placement); layer.removeGSplatShadowCaster(this._placement); } } return; } const meshInstance = this.instance?.meshInstance; if (meshInstance) { const layers = this.system.app.scene.layers; for(let i = 0; i < this._layers.length; i++){ layers.getLayerById(this._layers[i])?.removeMeshInstances([ meshInstance ]); } } } onRemoveChild() { this.removeFromLayers(); } onInsertChild() { if (this.enabled && this.entity.enabled) { if (this._instance || this._placement) { this.addToLayers(); } } } onRemove() { this.destroyInstance(); this.asset = null; this._assetReference.id = null; this.entity.off('remove', this.onRemoveChild, this); this.entity.off('insert', this.onInsertChild, this); } onLayersChanged(oldComp, newComp) { this.addToLayers(); oldComp.off('add', this.onLayerAdded, this); oldComp.off('remove', this.onLayerRemoved, this); newComp.on('add', this.onLayerAdded, this); newComp.on('remove', this.onLayerRemoved, this); } onLayerAdded(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; if (this.unified) { return; } if (this._instance) { layer.addMeshInstances(this._instance.meshInstance); } } onLayerRemoved(layer) { const index = this.layers.indexOf(layer.id); if (index < 0) return; if (this.unified) { return; } if (this._instance) { layer.removeMeshInstances(this._instance.meshInstance); } } onEnable() { const scene = this.system.app.scene; const layers = scene.layers; this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this); if (layers) { this._evtLayerAdded = layers.on('add', this.onLayerAdded, this); this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } if (this._instance || this._placement) { this.addToLayers(); } else if (this.asset) { this._onGSplatAssetAdded(); } } onDisable() { const scene = this.system.app.scene; const layers = scene.layers; this._evtLayersChanged?.off(); this._evtLayersChanged = null; if (layers) { this._evtLayerAdded?.off(); this._evtLayerAdded = null; this._evtLayerRemoved?.off(); this._evtLayerRemoved = null; } this.removeFromLayers(); } hide() { if (this._instance) { this._instance.meshInstance.visible = false; } } show() { if (this._instance) { this._instance.meshInstance.visible = true; } } _onGSplatAssetAdded() { if (!this._assetReference.asset) { return; } if (this._assetReference.asset.resource) { this._onGSplatAssetLoad(); } else if (this.enabled && this.entity.enabled) { this.system.app.assets.load(this._assetReference.asset); } } _onGSplatAssetLoad() { this.destroyInstance(); const asset = this._assetReference.asset; if (this.unified) { this._placement = null; if (asset) { this._placement = new GSplatPlacement(asset.resource, this.entity); this._placement.lodDistances = this._lodDistances; this._placement.splatBudget = this._splatBudget; if (this.enabled && this.entity.enabled) { this.addToLayers(); } } } else { if (asset) { this.instance = new GSplatInstance(asset.resource, { material: this._materialTmp, highQualitySH: this._highQualitySH, scene: this.system.app.scene }); this._materialTmp = null; } } if (asset) { this.customAabb = asset.resource.aabb.clone(); } } _onGSplatAssetUnload() { this.destroyInstance(); } _onGSplatAssetRemove() { this._onGSplatAssetUnload(); } constructor(system, entity){ super(system, entity), this._layers = [ LAYERID_WORLD ], this._instance = null, this._placement = null, this._materialTmp = null, this._highQualitySH = true, this._lodDistances = [ 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60 ], this._splatBudget = 0, this._customAabb = null, this._evtLayersChanged = null, this._evtLayerAdded = null, this._evtLayerRemoved = null, this._castShadows = false, this._unified = false; this._assetReference = new AssetReference('asset', this, system.app.assets, { add: this._onGSplatAssetAdded, load: this._onGSplatAssetLoad, remove: this._onGSplatAssetRemove, unload: this._onGSplatAssetUnload }, this); entity.on('remove', this.onRemoveChild, this); entity.on('removehierarchy', this.onRemoveChild, this); entity.on('insert', this.onInsertChild, this); entity.on('inserthierarchy', this.onInsertChild, this); } } class GSplatComponentData { constructor(){ this.enabled = true; } } var gsplatCenterVS$1 = ` uniform mat4 matrix_model; uniform mat4 matrix_view; #ifndef GSPLAT_CENTER_NOPROJ uniform vec4 camera_params; uniform mat4 matrix_projection; #endif bool initCenter(vec3 modelCenter, inout SplatCenter center) { mat4 modelView = matrix_view * matrix_model; vec4 centerView = modelView * vec4(modelCenter, 1.0); #ifndef GSPLAT_CENTER_NOPROJ if (camera_params.w != 1.0 && centerView.z > 0.0) { return false; } vec4 centerProj = matrix_projection * centerView; #if WEBGPU centerProj.z = clamp(centerProj.z, 0, abs(centerProj.w)); #else centerProj.z = clamp(centerProj.z, -abs(centerProj.w), abs(centerProj.w)); #endif center.proj = centerProj; center.projMat00 = matrix_projection[0][0]; #endif center.view = centerView.xyz / centerView.w; center.modelView = modelView; return true; } `; var gsplatColorVS$1 = ` uniform mediump sampler2D splatColor; vec4 readColor(in SplatSource source) { return texelFetch(splatColor, source.uv, 0); } `; var gsplatCommonVS$1 = ` #include "gsplatHelpersVS" #include "gsplatCustomizeVS" #include "gsplatModifyVS" #include "gsplatStructsVS" #include "gsplatEvalSHVS" #include "gsplatQuatToMat3VS" #include "gsplatSourceFormatVS" #include "gsplatSourceVS" #include "gsplatCenterVS" #include "gsplatCornerVS" #include "gsplatOutputVS" void clipCorner(inout SplatCorner corner, float alpha) { float clip = min(1.0, sqrt(-log(1.0 / (255.0 * alpha))) / 2.0); corner.offset *= clip; corner.uv *= clip; } `; var gsplatCompressedDataVS$1 = ` #include "gsplatPackingPS" uniform highp usampler2D packedTexture; uniform highp sampler2D chunkTexture; vec4 chunkDataA; vec4 chunkDataB; vec4 chunkDataC; vec4 chunkDataD; vec4 chunkDataE; uvec4 packedData; vec3 unpack111011(uint bits) { return vec3( float(bits >> 21u) / 2047.0, float((bits >> 11u) & 0x3ffu) / 1023.0, float(bits & 0x7ffu) / 2047.0 ); } const float norm = sqrt(2.0); vec4 unpackRotation(uint bits) { float a = (float((bits >> 20u) & 0x3ffu) / 1023.0 - 0.5) * norm; float b = (float((bits >> 10u) & 0x3ffu) / 1023.0 - 0.5) * norm; float c = (float(bits & 0x3ffu) / 1023.0 - 0.5) * norm; float m = sqrt(1.0 - (a * a + b * b + c * c)); uint mode = bits >> 30u; if (mode == 0u) return vec4(m, a, b, c); if (mode == 1u) return vec4(a, m, b, c); if (mode == 2u) return vec4(a, b, m, c); return vec4(a, b, c, m); } vec3 readCenter(SplatSource source) { uint w = uint(textureSize(chunkTexture, 0).x) / 5u; uint chunkId = source.id / 256u; ivec2 chunkUV = ivec2((chunkId % w) * 5u, chunkId / w); chunkDataA = texelFetch(chunkTexture, chunkUV, 0); chunkDataB = texelFetch(chunkTexture, chunkUV + ivec2(1, 0), 0); chunkDataC = texelFetch(chunkTexture, chunkUV + ivec2(2, 0), 0); chunkDataD = texelFetch(chunkTexture, chunkUV + ivec2(3, 0), 0); chunkDataE = texelFetch(chunkTexture, chunkUV + ivec2(4, 0), 0); packedData = texelFetch(packedTexture, source.uv, 0); return mix(chunkDataA.xyz, vec3(chunkDataA.w, chunkDataB.xy), unpack111011(packedData.x)); } vec4 readColor(in SplatSource source) { vec4 r = unpack8888(packedData.w); return vec4(mix(chunkDataD.xyz, vec3(chunkDataD.w, chunkDataE.xy), r.rgb), r.w); } vec4 getRotation() { return unpackRotation(packedData.y); } vec3 getScale() { return exp(mix(vec3(chunkDataB.zw, chunkDataC.x), chunkDataC.yzw, unpack111011(packedData.z))); } `; var gsplatCompressedSHVS$1 = ` #if SH_BANDS > 0 uniform highp usampler2D shTexture0; uniform highp usampler2D shTexture1; uniform highp usampler2D shTexture2; vec4 unpack8888s(in uint bits) { return vec4((uvec4(bits) >> uvec4(0u, 8u, 16u, 24u)) & 0xffu) * (8.0 / 255.0) - 4.0; } void readSHData(in SplatSource source, out vec3 sh[15], out float scale) { uvec4 shData0 = texelFetch(shTexture0, source.uv, 0); uvec4 shData1 = texelFetch(shTexture1, source.uv, 0); uvec4 shData2 = texelFetch(shTexture2, source.uv, 0); vec4 r0 = unpack8888s(shData0.x); vec4 r1 = unpack8888s(shData0.y); vec4 r2 = unpack8888s(shData0.z); vec4 r3 = unpack8888s(shData0.w); vec4 g0 = unpack8888s(shData1.x); vec4 g1 = unpack8888s(shData1.y); vec4 g2 = unpack8888s(shData1.z); vec4 g3 = unpack8888s(shData1.w); vec4 b0 = unpack8888s(shData2.x); vec4 b1 = unpack8888s(shData2.y); vec4 b2 = unpack8888s(shData2.z); vec4 b3 = unpack8888s(shData2.w); sh[0] = vec3(r0.x, g0.x, b0.x); sh[1] = vec3(r0.y, g0.y, b0.y); sh[2] = vec3(r0.z, g0.z, b0.z); sh[3] = vec3(r0.w, g0.w, b0.w); sh[4] = vec3(r1.x, g1.x, b1.x); sh[5] = vec3(r1.y, g1.y, b1.y); sh[6] = vec3(r1.z, g1.z, b1.z); sh[7] = vec3(r1.w, g1.w, b1.w); sh[8] = vec3(r2.x, g2.x, b2.x); sh[9] = vec3(r2.y, g2.y, b2.y); sh[10] = vec3(r2.z, g2.z, b2.z); sh[11] = vec3(r2.w, g2.w, b2.w); sh[12] = vec3(r3.x, g3.x, b3.x); sh[13] = vec3(r3.y, g3.y, b3.y); sh[14] = vec3(r3.z, g3.z, b3.z); scale = 1.0; } #endif `; var gsplatCustomizeVS$1 = ` void modifyCenter(inout vec3 center) { } void modifyCovariance(vec3 originalCenter, vec3 modifiedCenter, inout vec3 covA, inout vec3 covB) { } void modifyColor(vec3 center, inout vec4 color) { } `; var gsplatEvalSHVS$1 = ` #if SH_BANDS == 1 #define SH_COEFFS 3 #elif SH_BANDS == 2 #define SH_COEFFS 8 #elif SH_BANDS == 3 #define SH_COEFFS 15 #else #define SH_COEFFS 0 #endif #if SH_BANDS > 0 const float SH_C1 = 0.4886025119029199f; #if SH_BANDS > 1 const float SH_C2_0 = 1.0925484305920792f; const float SH_C2_1 = -1.0925484305920792f; const float SH_C2_2 = 0.31539156525252005f; const float SH_C2_3 = -1.0925484305920792f; const float SH_C2_4 = 0.5462742152960396f; #endif #if SH_BANDS > 2 const float SH_C3_0 = -0.5900435899266435f; const float SH_C3_1 = 2.890611442640554f; const float SH_C3_2 = -0.4570457994644658f; const float SH_C3_3 = 0.3731763325901154f; const float SH_C3_4 = -0.4570457994644658f; const float SH_C3_5 = 1.445305721320277f; const float SH_C3_6 = -0.5900435899266435f; #endif vec3 evalSH(in vec3 sh[SH_COEFFS], in vec3 dir) { float x = dir.x; float y = dir.y; float z = dir.z; vec3 result = SH_C1 * (-sh[0] * y + sh[1] * z - sh[2] * x); #if SH_BANDS > 1 float xx = x * x; float yy = y * y; float zz = z * z; float xy = x * y; float yz = y * z; float xz = x * z; result += sh[3] * (SH_C2_0 * xy) + sh[4] * (SH_C2_1 * yz) + sh[5] * (SH_C2_2 * (2.0 * zz - xx - yy)) + sh[6] * (SH_C2_3 * xz) + sh[7] * (SH_C2_4 * (xx - yy)); #endif #if SH_BANDS > 2 result += sh[8] * (SH_C3_0 * y * (3.0 * xx - yy)) + sh[9] * (SH_C3_1 * xy * z) + sh[10] * (SH_C3_2 * y * (4.0 * zz - xx - yy)) + sh[11] * (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) + sh[12] * (SH_C3_4 * x * (4.0 * zz - xx - yy)) + sh[13] * (SH_C3_5 * z * (xx - yy)) + sh[14] * (SH_C3_6 * x * (xx - 3.0 * yy)); #endif return result; } #endif `; var gsplatHelpersVS$1 = ` float gsplatExtractSize(vec3 covA, vec3 covB) { float tr = covA.x + covB.x + covB.z; return sqrt(max(tr, 0.0) / 3.0); } void gsplatApplyUniformScale(inout vec3 covA, inout vec3 covB, float scale) { float s2 = scale * scale; covA *= s2; covB *= s2; } void gsplatMakeRound(inout vec3 covA, inout vec3 covB, float size) { float s2 = size * size; covA = vec3(s2, 0.0, 0.0); covB = vec3(s2, 0.0, s2); } void gsplatMakeSpherical(inout vec3 scale, float size) { scale = vec3(size); } float gsplatGetSizeFromScale(vec3 scale) { return sqrt((scale.x * scale.x + scale.y * scale.y + scale.z * scale.z) / 3.0); } `; var gsplatModifyVS$1 = ` void modifySplatCenter(inout vec3 center) { } void modifySplatRotationScale(vec3 originalCenter, vec3 modifiedCenter, inout vec4 rotation, inout vec3 scale) { } void modifySplatColor(vec3 center, inout vec4 color) { } `; var gsplatQuatToMat3VS$1 = ` mat3 quatToMat3(vec4 R) { vec4 R2 = R + R; float X = R2.x * R.w; vec4 Y = R2.y * R; vec4 Z = R2.z * R; float W = R2.w * R.w; return mat3( 1.0 - Z.z - W, Y.z + X, Y.w - Z.x, Y.z - X, 1.0 - Y.y - W, Z.w + Y.x, Y.w + Z.x, Z.w - Y.x, 1.0 - Y.y - Z.z ); } vec4 quatMul(vec4 a, vec4 b) { return vec4( a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z ); } `; var gsplatSogsColorVS$1 = ` uniform highp sampler2D packedSh0; uniform float sh0_mins; uniform float sh0_maxs; const float SH_C0 = 0.28209479177387814; vec4 readColor(in SplatSource source) { vec3 clr = mix(vec3(sh0_mins), vec3(sh0_maxs), unpack111110(pack8888(texelFetch(packedSh0, source.uv, 0)))); float alpha = float(packedSample.z & 0xffu) / 255.0; return vec4(vec3(0.5) + clr.xyz * SH_C0, alpha); } `; var gsplatSogsDataVS$1 = ` #include "gsplatPackingPS" uniform highp usampler2D packedTexture; uniform vec3 means_mins; uniform vec3 means_maxs; uniform float scales_mins; uniform float scales_maxs; uvec4 packedSample; vec3 readCenter(SplatSource source) { packedSample = texelFetch(packedTexture, source.uv, 0); vec3 l = unpack8888(packedSample.x).xyz; vec3 u = unpack8888(packedSample.y).xyz; vec3 n = (l + u * 256.0) / 257.0; vec3 v = mix(means_mins, means_maxs, n); return sign(v) * (exp(abs(v)) - 1.0); } const float norm = sqrt(2.0); vec4 getRotation() { vec3 qdata = unpack8888(packedSample.z).xyz; uint qmode = packedSample.w & 0x3u; vec3 abc = (qdata - 0.5) * norm; float d = sqrt(max(0.0, 1.0 - dot(abc, abc))); return (qmode == 0u) ? vec4(d, abc) : ((qmode == 1u) ? vec4(abc.x, d, abc.yz) : ((qmode == 2u) ? vec4(abc.xy, d, abc.z) : vec4(abc, d))); } vec3 getScale() { vec3 sdata = unpack101010(packedSample.w >> 2u); return exp(mix(vec3(scales_mins), vec3(scales_maxs), sdata)); } `; var gsplatSogsSHVS$1 = ` uniform highp sampler2D packedShN; uniform float shN_mins; uniform float shN_maxs; void readSHData(in SplatSource source, out vec3 sh[SH_COEFFS], out float scale) { ivec2 t = ivec2(packedSample.xy & 255u); int n = t.x + t.y * 256; int u = (n % 64) * SH_COEFFS; int v = n / 64; for (int i = 0; i < SH_COEFFS; i++) { sh[i] = mix(vec3(shN_mins), vec3(shN_maxs), unpack111110(pack8888(texelFetch(packedShN, ivec2(u + i, v), 0)))); } scale = 1.0; } `; var gsplatSourceFormatVS$1 = ` #if defined(GSPLAT_WORKBUFFER_DATA) #include "gsplatWorkBufferVS" #elif GSPLAT_COMPRESSED_DATA == true #include "gsplatCompressedDataVS" #if SH_COEFFS > 0 #include "gsplatCompressedSHVS" #endif #elif GSPLAT_SOGS_DATA == true #include "gsplatSogsDataVS" #include "gsplatSogsColorVS" #if SH_COEFFS > 0 #include "gsplatSogsSHVS" #endif #else #include "gsplatDataVS" #include "gsplatColorVS" #if SH_COEFFS > 0 #include "gsplatSHVS" #endif #endif `; var gsplatStructsVS$1 = ` struct SplatSource { uint order; uint id; ivec2 uv; vec2 cornerUV; }; struct SplatCenter { vec3 view; vec4 proj; mat4 modelView; float projMat00; vec3 modelCenterOriginal; vec3 modelCenterModified; }; struct SplatCorner { vec2 offset; vec2 uv; #if GSPLAT_AA float aaFactor; #endif vec2 v; float dlen; }; `; var gsplatCornerVS$1 = ` uniform vec4 viewport_size; void computeCovariance(vec4 rotation, vec3 scale, out vec3 covA, out vec3 covB) { mat3 rot = quatToMat3(rotation); mat3 M = transpose(mat3( scale.x * rot[0], scale.y * rot[1], scale.z * rot[2] )); covA = vec3(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2])); covB = vec3(dot(M[1], M[1]), dot(M[1], M[2]), dot(M[2], M[2])); } bool initCornerCov(SplatSource source, SplatCenter center, out SplatCorner corner, vec3 covA, vec3 covB) { mat3 Vrk = mat3( covA.x, covA.y, covA.z, covA.y, covB.x, covB.y, covA.z, covB.y, covB.z ); float focal = viewport_size.x * center.projMat00; vec3 v = camera_params.w == 1.0 ? vec3(0.0, 0.0, 1.0) : center.view.xyz; float J1 = focal / v.z; vec2 J2 = -J1 / v.z * v.xy; mat3 J = mat3( J1, 0.0, J2.x, 0.0, J1, J2.y, 0.0, 0.0, 0.0 ); mat3 W = transpose(mat3(center.modelView)); mat3 T = W * J; mat3 cov = transpose(T) * Vrk * T; #if GSPLAT_AA float detOrig = cov[0][0] * cov[1][1] - cov[0][1] * cov[0][1]; float detBlur = (cov[0][0] + 0.3) * (cov[1][1] + 0.3) - cov[0][1] * cov[0][1]; corner.aaFactor = sqrt(max(detOrig / detBlur, 0.0)); #endif float diagonal1 = cov[0][0] + 0.3; float offDiagonal = cov[0][1]; float diagonal2 = cov[1][1] + 0.3; float mid = 0.5 * (diagonal1 + diagonal2); float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal)); float lambda1 = mid + radius; float lambda2 = max(mid - radius, 0.1); float vmin = min(1024.0, min(viewport_size.x, viewport_size.y)); float l1 = 2.0 * min(sqrt(2.0 * lambda1), vmin); float l2 = 2.0 * min(sqrt(2.0 * lambda2), vmin); if (l1 < 2.0 && l2 < 2.0) { return false; } vec2 c = center.proj.ww * viewport_size.zw; if (any(greaterThan(abs(center.proj.xy) - vec2(max(l1, l2)) * c, center.proj.ww))) { return false; } vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1)); vec2 v1 = l1 * diagonalVector; vec2 v2 = l2 * vec2(diagonalVector.y, -diagonalVector.x); corner.offset = (source.cornerUV.x * v1 + source.cornerUV.y * v2) * c; corner.uv = source.cornerUV; return true; } bool initCorner(SplatSource source, SplatCenter center, out SplatCorner corner) { vec4 rotation = getRotation().yzwx; vec3 scale = getScale(); modifySplatRotationScale(center.modelCenterOriginal, center.modelCenterModified, rotation, scale); vec3 covA, covB; computeCovariance(rotation.wxyz, scale, covA, covB); modifyCovariance(center.modelCenterOriginal, center.modelCenterModified, covA, covB); return initCornerCov(source, center, corner, covA, covB); } `; var gsplatDataVS$1 = ` uniform highp usampler2D transformA; uniform highp sampler2D transformB; uint tAw; vec4 tBcached; vec3 readCenter(SplatSource source) { uvec4 tA = texelFetch(transformA, source.uv, 0); tAw = tA.w; tBcached = texelFetch(transformB, source.uv, 0); return uintBitsToFloat(tA.xyz); } vec4 unpackRotation(vec3 packed) { return vec4(packed.xyz, sqrt(max(0.0, 1.0 - dot(packed, packed)))); } vec4 getRotation() { return unpackRotation(vec3(unpackHalf2x16(tAw), tBcached.w)).wxyz; } vec3 getScale() { return tBcached.xyz; } `; var gsplatOutputVS$1 = ` #include "tonemappingPS" #include "decodePS" #include "gammaPS" vec3 prepareOutputFromGamma(vec3 gammaColor) { #if TONEMAP == NONE #if GAMMA == NONE return decodeGamma(gammaColor); #else return gammaColor; #endif #else return gammaCorrectOutput(toneMap(decodeGamma(gammaColor))); #endif } `; var gsplatPS$1 = ` #ifndef DITHER_NONE #include "bayerPS" #include "opacityDitherPS" varying float id; #endif #if defined(SHADOW_PASS) || defined(PICK_PASS) || defined(PREPASS_PASS) uniform float alphaClip; #endif #ifdef PREPASS_PASS varying float vLinearDepth; #include "floatAsUintPS" #endif varying mediump vec2 gaussianUV; varying mediump vec4 gaussianColor; #ifdef PICK_PASS #include "pickPS" #endif const float EXP4 = exp(-4.0); const float INV_EXP4 = 1.0 / (1.0 - EXP4); float normExp(float x) { return (exp(x * -4.0) - EXP4) * INV_EXP4; } void main(void) { mediump float A = dot(gaussianUV, gaussianUV); if (A > 1.0) { discard; } mediump float alpha = normExp(A) * gaussianColor.a; #if defined(SHADOW_PASS) || defined(PICK_PASS) || defined(PREPASS_PASS) if (alpha < alphaClip) { discard; } #endif #ifdef PICK_PASS pcFragColor0 = getPickOutput(); #ifdef DEPTH_PICK_PASS pcFragColor1 = getPickDepth(); #endif #elif SHADOW_PASS gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); #elif PREPASS_PASS gl_FragColor = float2vec4(vLinearDepth); #else if (alpha < 1.0 / 255.0) { discard; } #ifndef DITHER_NONE opacityDither(alpha, id * 0.013); #endif gl_FragColor = vec4(gaussianColor.xyz * alpha, alpha); #endif } `; var gsplatSHVS$1 = ` #if SH_BANDS > 0 vec3 unpack111011s(uint bits) { return vec3((uvec3(bits) >> uvec3(21u, 11u, 0u)) & uvec3(0x7ffu, 0x3ffu, 0x7ffu)) / vec3(2047.0, 1023.0, 2047.0) * 2.0 - 1.0; } void fetchScale(in uvec4 t, out float scale, out vec3 a, out vec3 b, out vec3 c) { scale = uintBitsToFloat(t.x); a = unpack111011s(t.y); b = unpack111011s(t.z); c = unpack111011s(t.w); } void fetch(in uvec4 t, out vec3 a, out vec3 b, out vec3 c, out vec3 d) { a = unpack111011s(t.x); b = unpack111011s(t.y); c = unpack111011s(t.z); d = unpack111011s(t.w); } void fetch(in uint t, out vec3 a) { a = unpack111011s(t); } #if SH_BANDS == 1 uniform highp usampler2D splatSH_1to3; void readSHData(in SplatSource source, out vec3 sh[3], out float scale) { fetchScale(texelFetch(splatSH_1to3, source.uv, 0), scale, sh[0], sh[1], sh[2]); } #elif SH_BANDS == 2 uniform highp usampler2D splatSH_1to3; uniform highp usampler2D splatSH_4to7; uniform highp usampler2D splatSH_8to11; void readSHData(in SplatSource source, out vec3 sh[8], out float scale) { fetchScale(texelFetch(splatSH_1to3, source.uv, 0), scale, sh[0], sh[1], sh[2]); fetch(texelFetch(splatSH_4to7, source.uv, 0), sh[3], sh[4], sh[5], sh[6]); fetch(texelFetch(splatSH_8to11, source.uv, 0).x, sh[7]); } #else uniform highp usampler2D splatSH_1to3; uniform highp usampler2D splatSH_4to7; uniform highp usampler2D splatSH_8to11; uniform highp usampler2D splatSH_12to15; void readSHData(in SplatSource source, out vec3 sh[15], out float scale) { fetchScale(texelFetch(splatSH_1to3, source.uv, 0), scale, sh[0], sh[1], sh[2]); fetch(texelFetch(splatSH_4to7, source.uv, 0), sh[3], sh[4], sh[5], sh[6]); fetch(texelFetch(splatSH_8to11, source.uv, 0), sh[7], sh[8], sh[9], sh[10]); fetch(texelFetch(splatSH_12to15, source.uv, 0), sh[11], sh[12], sh[13], sh[14]); } #endif #endif `; var gsplatSourceVS$1 = ` attribute vec3 vertex_position; attribute uint vertex_id_attrib; uniform uint numSplats; uniform uint splatTextureSize; uniform highp usampler2D splatOrder; bool initSource(out SplatSource source) { source.order = vertex_id_attrib + uint(vertex_position.z); if (source.order >= numSplats) { return false; } ivec2 orderUV = ivec2(source.order % splatTextureSize, source.order / splatTextureSize); source.id = texelFetch(splatOrder, orderUV, 0).r; source.uv = ivec2(source.id % splatTextureSize, source.id / splatTextureSize); source.cornerUV = vertex_position.xy; return true; } `; var gsplatVS$1 = ` #include "gsplatCommonVS" varying mediump vec2 gaussianUV; varying mediump vec4 gaussianColor; #ifndef DITHER_NONE varying float id; #endif mediump vec4 discardVec = vec4(0.0, 0.0, 2.0, 1.0); #ifdef PREPASS_PASS varying float vLinearDepth; #endif #ifdef GSPLAT_OVERDRAW uniform sampler2D colorRamp; uniform float colorRampIntensity; #endif void main(void) { SplatSource source; if (!initSource(source)) { gl_Position = discardVec; return; } vec3 modelCenter = readCenter(source); SplatCenter center; center.modelCenterOriginal = modelCenter; modifyCenter(modelCenter); modifySplatCenter(modelCenter); center.modelCenterModified = modelCenter; if (!initCenter(modelCenter, center)) { gl_Position = discardVec; return; } SplatCorner corner; if (!initCorner(source, center, corner)) { gl_Position = discardVec; return; } vec4 clr = readColor(source); #if GSPLAT_AA clr.a *= corner.aaFactor; #endif #if SH_BANDS > 0 vec3 dir = normalize(center.view * mat3(center.modelView)); vec3 sh[SH_COEFFS]; float scale; readSHData(source, sh, scale); clr.xyz += evalSH(sh, dir) * scale; #endif modifyColor(modelCenter, clr); modifySplatColor(modelCenter, clr); clipCorner(corner, clr.w); gl_Position = center.proj + vec4(corner.offset, 0, 0); gaussianUV = corner.uv; #ifdef GSPLAT_OVERDRAW float t = clamp(modelCenter.y / 20.0, 0.0, 1.0); vec3 rampColor = textureLod(colorRamp, vec2(t, 0.5), 0.0).rgb; clr.a *= (1.0 / 32.0) * colorRampIntensity; gaussianColor = vec4(rampColor, clr.a); #else gaussianColor = vec4(prepareOutputFromGamma(max(clr.xyz, 0.0)), clr.w); #endif #ifndef DITHER_NONE id = float(source.id); #endif #ifdef PREPASS_PASS vLinearDepth = -center.view.z; #endif } `; var gsplatWorkBufferVS$1 = ` uniform highp usampler2D splatTexture0; uniform highp usampler2D splatTexture1; #ifdef GSPLAT_COLOR_UINT uniform highp usampler2D splatColor; #else uniform mediump sampler2D splatColor; #endif uvec4 cachedSplatTexture0Data; uvec2 cachedSplatTexture1Data; vec3 readCenter(SplatSource source) { cachedSplatTexture0Data = texelFetch(splatTexture0, source.uv, 0); cachedSplatTexture1Data = texelFetch(splatTexture1, source.uv, 0).xy; return vec3(uintBitsToFloat(cachedSplatTexture0Data.r), uintBitsToFloat(cachedSplatTexture0Data.g), uintBitsToFloat(cachedSplatTexture0Data.b)); } vec4 getRotation() { vec2 rotXY = unpackHalf2x16(cachedSplatTexture0Data.a); vec2 rotZscaleX = unpackHalf2x16(cachedSplatTexture1Data.x); vec3 rotXYZ = vec3(rotXY, rotZscaleX.x); return vec4(rotXYZ, sqrt(max(0.0, 1.0 - dot(rotXYZ, rotXYZ)))).wxyz; } vec3 getScale() { vec2 rotZscaleX = unpackHalf2x16(cachedSplatTexture1Data.x); vec2 scaleYZ = unpackHalf2x16(cachedSplatTexture1Data.y); return vec3(rotZscaleX.y, scaleYZ); } vec4 readColor(in SplatSource source) { #ifdef GSPLAT_COLOR_UINT uvec4 packed = texelFetch(splatColor, source.uv, 0); uint packed_rg = packed.r | (packed.g << 16u); uint packed_ba = packed.b | (packed.a << 16u); return vec4(unpackHalf2x16(packed_rg), unpackHalf2x16(packed_ba)); #else return texelFetch(splatColor, source.uv, 0); #endif } `; const gsplatChunksGLSL = { gsplatCenterVS: gsplatCenterVS$1, gsplatCornerVS: gsplatCornerVS$1, gsplatColorVS: gsplatColorVS$1, gsplatCommonVS: gsplatCommonVS$1, gsplatCompressedDataVS: gsplatCompressedDataVS$1, gsplatCompressedSHVS: gsplatCompressedSHVS$1, gsplatCustomizeVS: gsplatCustomizeVS$1, gsplatEvalSHVS: gsplatEvalSHVS$1, gsplatHelpersVS: gsplatHelpersVS$1, gsplatModifyVS: gsplatModifyVS$1, gsplatQuatToMat3VS: gsplatQuatToMat3VS$1, gsplatSogsColorVS: gsplatSogsColorVS$1, gsplatSogsDataVS: gsplatSogsDataVS$1, gsplatSogsSHVS: gsplatSogsSHVS$1, gsplatSourceFormatVS: gsplatSourceFormatVS$1, gsplatStructsVS: gsplatStructsVS$1, gsplatDataVS: gsplatDataVS$1, gsplatOutputVS: gsplatOutputVS$1, gsplatPS: gsplatPS$1, gsplatSHVS: gsplatSHVS$1, gsplatSourceVS: gsplatSourceVS$1, gsplatVS: gsplatVS$1, gsplatWorkBufferVS: gsplatWorkBufferVS$1, gsplatPackingPS: gsplatPackingPS$1 }; var gsplatCenterVS = ` uniform matrix_model: mat4x4f; uniform matrix_view: mat4x4f; #ifndef GSPLAT_CENTER_NOPROJ uniform camera_params: vec4f; uniform matrix_projection: mat4x4f; #endif fn initCenter(modelCenter: vec3f, center: ptr) -> bool { let modelView: mat4x4f = uniform.matrix_view * uniform.matrix_model; let centerView: vec4f = modelView * vec4f(modelCenter, 1.0); #ifndef GSPLAT_CENTER_NOPROJ if (uniform.camera_params.w != 1.0 && centerView.z > 0.0) { return false; } var centerProj: vec4f = uniform.matrix_projection * centerView; centerProj.z = clamp(centerProj.z, 0.0, abs(centerProj.w)); center.proj = centerProj; center.projMat00 = uniform.matrix_projection[0][0]; #endif center.view = centerView.xyz / centerView.w; center.modelView = modelView; return true; } `; var gsplatColorVS = ` var splatColor: texture_2d; fn readColor(source: ptr) -> vec4f { return textureLoad(splatColor, source.uv, 0); } `; var gsplatCommonVS = ` #include "gsplatHelpersVS" #include "gsplatCustomizeVS" #include "gsplatModifyVS" #include "gsplatStructsVS" #include "gsplatEvalSHVS" #include "gsplatQuatToMat3VS" #include "gsplatSourceFormatVS" #include "gsplatSourceVS" #include "gsplatCenterVS" #include "gsplatCornerVS" #include "gsplatOutputVS" fn clipCorner(corner: ptr, alpha: f32) { let clip: f32 = min(1.0, sqrt(-log(1.0 / (255.0 * alpha))) / 2.0); corner.offset = corner.offset * clip; corner.uv = corner.uv * clip; } `; var gsplatCompressedDataVS = ` #include "gsplatPackingPS" var packedTexture: texture_2d; var chunkTexture: texture_2d; var chunkDataA: vec4f; var chunkDataB: vec4f; var chunkDataC: vec4f; var chunkDataD: vec4f; var chunkDataE: vec4f; var packedData: vec4u; fn unpack111011(bits: u32) -> vec3f { return (vec3f((vec3(bits) >> vec3(21u, 11u, 0u)) & vec3(0x7ffu, 0x3ffu, 0x7ffu))) / vec3f(2047.0, 1023.0, 2047.0); } const norm_const: f32 = sqrt(2.0); fn unpackRotation(bits: u32) -> vec4f { let a = (f32((bits >> 20u) & 0x3ffu) / 1023.0 - 0.5) * norm_const; let b = (f32((bits >> 10u) & 0x3ffu) / 1023.0 - 0.5) * norm_const; let c = (f32(bits & 0x3ffu) / 1023.0 - 0.5) * norm_const; let m = sqrt(1.0 - (a * a + b * b + c * c)); let mode = bits >> 30u; if (mode == 0u) { return vec4f(m, a, b, c); } if (mode == 1u) { return vec4f(a, m, b, c); } if (mode == 2u) { return vec4f(a, b, m, c); } return vec4f(a, b, c, m); } fn readCenter(source: ptr) -> vec3f { let tex_size_u = textureDimensions(chunkTexture, 0); let w: u32 = tex_size_u.x / 5u; let chunkId: u32 = source.id / 256u; let chunkUV: vec2 = vec2(i32((chunkId % w) * 5u), i32(chunkId / w)); chunkDataA = textureLoad(chunkTexture, chunkUV + vec2(0, 0), 0); chunkDataB = textureLoad(chunkTexture, chunkUV + vec2(1, 0), 0); chunkDataC = textureLoad(chunkTexture, chunkUV + vec2(2, 0), 0); chunkDataD = textureLoad(chunkTexture, chunkUV + vec2(3, 0), 0); chunkDataE = textureLoad(chunkTexture, chunkUV + vec2(4, 0), 0); packedData = textureLoad(packedTexture, source.uv, 0); return mix(chunkDataA.xyz, vec3f(chunkDataA.w, chunkDataB.xy), unpack111011(packedData.x)); } fn readColor(source: ptr) -> vec4f { let r = unpack8888(packedData.w); return vec4f(mix(chunkDataD.xyz, vec3f(chunkDataD.w, chunkDataE.xy), r.rgb), r.w); } fn getRotation() -> vec4f { return unpackRotation(packedData.y); } fn getScale() -> vec3f { return exp(mix(vec3f(chunkDataB.zw, chunkDataC.x), chunkDataC.yzw, unpack111011(packedData.z))); } `; var gsplatCompressedSHVS = ` #if SH_BANDS > 0 var shTexture0: texture_2d; var shTexture1: texture_2d; var shTexture2: texture_2d; fn unpack8888s(bits: u32) -> vec4f { let unpacked_u = (vec4(bits) >> vec4(0u, 8u, 16u, 24u)) & vec4(0xffu); return vec4f(unpacked_u) * (8.0 / 255.0) - 4.0; } fn readSHData(source: ptr, sh: ptr>, scale: ptr) { let shData0: vec4 = textureLoad(shTexture0, source.uv, 0); let shData1: vec4 = textureLoad(shTexture1, source.uv, 0); let shData2: vec4 = textureLoad(shTexture2, source.uv, 0); let r0 = unpack8888s(shData0.x); let r1 = unpack8888s(shData0.y); let r2 = unpack8888s(shData0.z); let r3 = unpack8888s(shData0.w); let g0 = unpack8888s(shData1.x); let g1 = unpack8888s(shData1.y); let g2 = unpack8888s(shData1.z); let g3 = unpack8888s(shData1.w); let b0 = unpack8888s(shData2.x); let b1 = unpack8888s(shData2.y); let b2 = unpack8888s(shData2.z); let b3 = unpack8888s(shData2.w); sh[0] = vec3f(r0.x, g0.x, b0.x); sh[1] = vec3f(r0.y, g0.y, b0.y); sh[2] = vec3f(r0.z, g0.z, b0.z); sh[3] = vec3f(r0.w, g0.w, b0.w); sh[4] = vec3f(r1.x, g1.x, b1.x); sh[5] = vec3f(r1.y, g1.y, b1.y); sh[6] = vec3f(r1.z, g1.z, b1.z); sh[7] = vec3f(r1.w, g1.w, b1.w); sh[8] = vec3f(r2.x, g2.x, b2.x); sh[9] = vec3f(r2.y, g2.y, b2.y); sh[10] = vec3f(r2.z, g2.z, b2.z); sh[11] = vec3f(r2.w, g2.w, b2.w); sh[12] = vec3f(r3.x, g3.x, b3.x); sh[13] = vec3f(r3.y, g3.y, b3.y); sh[14] = vec3f(r3.z, g3.z, b3.z); *scale = 1.0; } #endif `; var gsplatCustomizeVS = ` fn modifyCenter(center: ptr) { } fn modifyCovariance(originalCenter: vec3f, modifiedCenter: vec3f, covA: ptr, covB: ptr) { } fn modifyColor(center: vec3f, color: ptr) { } `; var gsplatEvalSHVS = ` #if SH_BANDS == 1 const SH_COEFFS: i32 = 3; #elif SH_BANDS == 2 const SH_COEFFS: i32 = 8; #elif SH_BANDS == 3 const SH_COEFFS: i32 = 15; #else const SH_COEFFS: i32 = 0; #endif #if SH_BANDS > 0 const SH_C1: f32 = 0.4886025119029199; #if SH_BANDS > 1 const SH_C2_0: f32 = 1.0925484305920792; const SH_C2_1: f32 = -1.0925484305920792; const SH_C2_2: f32 = 0.31539156525252005; const SH_C2_3: f32 = -1.0925484305920792; const SH_C2_4: f32 = 0.5462742152960396; #endif #if SH_BANDS > 2 const SH_C3_0: f32 = -0.5900435899266435; const SH_C3_1: f32 = 2.890611442640554; const SH_C3_2: f32 = -0.4570457994644658; const SH_C3_3: f32 = 0.3731763325901154; const SH_C3_4: f32 = -0.4570457994644658; const SH_C3_5: f32 = 1.445305721320277; const SH_C3_6: f32 = -0.5900435899266435; #endif fn evalSH(sh: ptr>, dir: vec3f) -> vec3f { let x = dir.x; let y = dir.y; let z = dir.z; var result = SH_C1 * (-sh[0] * y + sh[1] * z - sh[2] * x); #if SH_BANDS > 1 let xx = x * x; let yy = y * y; let zz = z * z; let xy = x * y; let yz = y * z; let xz = x * z; result = result + ( sh[3] * (SH_C2_0 * xy) + sh[4] * (SH_C2_1 * yz) + sh[5] * (SH_C2_2 * (2.0 * zz - xx - yy)) + sh[6] * (SH_C2_3 * xz) + sh[7] * (SH_C2_4 * (xx - yy)) ); #endif #if SH_BANDS > 2 result = result + ( sh[8] * (SH_C3_0 * y * (3.0 * xx - yy)) + sh[9] * (SH_C3_1 * xy * z) + sh[10] * (SH_C3_2 * y * (4.0 * zz - xx - yy)) + sh[11] * (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) + sh[12] * (SH_C3_4 * x * (4.0 * zz - xx - yy)) + sh[13] * (SH_C3_5 * z * (xx - yy)) + sh[14] * (SH_C3_6 * x * (xx - 3.0 * yy)) ); #endif return result; } #endif `; var gsplatHelpersVS = ` fn gsplatExtractSize(covA: vec3f, covB: vec3f) -> f32 { let tr = covA.x + covB.x + covB.z; return sqrt(max(tr, 0.0) / 3.0); } fn gsplatApplyUniformScale(covA: ptr, covB: ptr, scale: f32) { let s2 = scale * scale; *covA = *covA * s2; *covB = *covB * s2; } fn gsplatMakeRound(covA: ptr, covB: ptr, size: f32) { let s2 = size * size; *covA = vec3f(s2, 0.0, 0.0); *covB = vec3f(s2, 0.0, s2); } fn gsplatMakeSpherical(scale: ptr, size: f32) { *scale = vec3f(size); } fn gsplatGetSizeFromScale(scale: vec3f) -> f32 { return sqrt((scale.x * scale.x + scale.y * scale.y + scale.z * scale.z) / 3.0); } `; var gsplatModifyVS = ` fn modifySplatCenter(center: ptr) { } fn modifySplatRotationScale(originalCenter: vec3f, modifiedCenter: vec3f, rotation: ptr, scale: ptr) { } fn modifySplatColor(center: vec3f, color: ptr) { } `; var gsplatQuatToMat3VS = ` fn quatToMat3(R: vec4) -> mat3x3 { let R2: vec4 = R + R; let X: f32 = R2.x * R.w; let Y: vec4 = R2.y * R; let Z: vec4 = R2.z * R; let W: f32 = R2.w * R.w; return mat3x3( 1.0 - Z.z - W, Y.z + X, Y.w - Z.x, Y.z - X, 1.0 - Y.y - W, Z.w + Y.x, Y.w + Z.x, Z.w - Y.x, 1.0 - Y.y - Z.z ); } fn quatMul(a: vec4, b: vec4) -> vec4 { return vec4( a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z ); } `; var gsplatSogsColorVS = ` var packedSh0: texture_2d; uniform sh0_mins: f32; uniform sh0_maxs: f32; const SH_C0: f32 = 0.28209479177387814; fn readColor(source: ptr) -> vec4f { let clr = mix(vec3f(uniform.sh0_mins), vec3f(uniform.sh0_maxs), unpack111110(pack8888(textureLoad(packedSh0, source.uv, 0)))); let alpha = f32(packedSample.z & 0xffu) / 255.0; return vec4f(vec3f(0.5) + clr.xyz * SH_C0, alpha); } `; var gsplatSogsDataVS = ` #include "gsplatPackingPS" var packedTexture: texture_2d; uniform means_mins: vec3f; uniform means_maxs: vec3f; uniform scales_mins: f32; uniform scales_maxs: f32; var packedSample: vec4; fn readCenter(source: ptr) -> vec3f { packedSample = textureLoad(packedTexture, source.uv, 0); let l = unpack8888(packedSample.x).xyz; let u = unpack8888(packedSample.y).xyz; let n = (l + u * 256.0) / 257.0; let v = mix(uniform.means_mins, uniform.means_maxs, n); return sign(v) * (exp(abs(v)) - 1.0); } const norm: f32 = sqrt(2.0); fn getRotation() -> vec4f { let qdata = unpack8888(packedSample.z).xyz; let qmode = packedSample.w & 0x3u; let abc = (qdata - 0.5) * norm; let d = sqrt(max(0.0, 1.0 - dot(abc, abc))); var quat: vec4f; if (qmode == 0u) { quat = vec4f(d, abc); } else if (qmode == 1u) { quat = vec4f(abc.x, d, abc.y, abc.z); } else if (qmode == 2u) { quat = vec4f(abc.x, abc.y, d, abc.z); } else { quat = vec4f(abc.x, abc.y, abc.z, d); } return quat; } fn getScale() -> vec3f { let sdata = unpack101010(packedSample.w >> 2u); return exp(mix(vec3f(uniform.scales_mins), vec3f(uniform.scales_maxs), sdata)); } `; var gsplatSogsSHVS = ` var packedShN: texture_2d; uniform shN_mins: f32; uniform shN_maxs: f32; fn readSHData(source: ptr, sh: ptr>, scale: ptr) { let t = vec2i(packedSample.xy & vec2u(255u)); let n = t.x + t.y * 256; let u = (n % 64) * SH_COEFFS; let v = n / 64; for (var i: i32 = 0; i < SH_COEFFS; i = i + 1) { sh[i] = mix(vec3f(uniform.shN_mins), vec3f(uniform.shN_maxs), unpack111110(pack8888(textureLoad(packedShN, vec2i(u + i, v), 0)))); } *scale = 1.0; } `; var gsplatSourceFormatVS = ` #if defined(GSPLAT_WORKBUFFER_DATA) #include "gsplatWorkBufferVS" #elif GSPLAT_COMPRESSED_DATA == true #include "gsplatCompressedDataVS" #if SH_BANDS > 0 #include "gsplatCompressedSHVS" #endif #elif GSPLAT_SOGS_DATA #include "gsplatSogsDataVS" #include "gsplatSogsColorVS" #if SH_BANDS > 0 #include "gsplatSogsSHVS" #endif #else #include "gsplatDataVS" #include "gsplatColorVS" #if SH_BANDS > 0 #include "gsplatSHVS" #endif #endif `; var gsplatStructsVS = ` struct SplatSource { order: u32, id: u32, uv: vec2, cornerUV: vec2f, } struct SplatCenter { view: vec3f, proj: vec4f, modelView: mat4x4f, projMat00: f32, modelCenterOriginal: vec3f, modelCenterModified: vec3f, } struct SplatCorner { offset: vec2f, uv: vec2f, #if GSPLAT_AA aaFactor: f32, #endif } `; var gsplatCornerVS = ` uniform viewport_size: vec4f; fn computeCovariance(rotation: vec4f, scale: vec3f, covA_ptr: ptr, covB_ptr: ptr) { let rot = quatToMat3(rotation); let M = transpose(mat3x3f( scale.x * rot[0], scale.y * rot[1], scale.z * rot[2] )); *covA_ptr = vec3f(dot(M[0], M[0]), dot(M[0], M[1]), dot(M[0], M[2])); *covB_ptr = vec3f(dot(M[1], M[1]), dot(M[1], M[2]), dot(M[2], M[2])); } fn initCornerCov(source: ptr, center: ptr, corner: ptr, covA: vec3f, covB: vec3f) -> bool { let Vrk = mat3x3f( vec3f(covA.x, covA.y, covA.z), vec3f(covA.y, covB.x, covB.y), vec3f(covA.z, covB.y, covB.z) ); let focal = uniform.viewport_size.x * center.projMat00; let v = select(center.view.xyz, vec3f(0.0, 0.0, 1.0), uniform.camera_params.w == 1.0); let J1 = focal / v.z; let J2 = -J1 / v.z * v.xy; let J = mat3x3f( vec3f(J1, 0.0, J2.x), vec3f(0.0, J1, J2.y), vec3f(0.0, 0.0, 0.0) ); let W = transpose(mat3x3f(center.modelView[0].xyz, center.modelView[1].xyz, center.modelView[2].xyz)); let T = W * J; let cov = transpose(T) * Vrk * T; #if GSPLAT_AA let detOrig = cov[0][0] * cov[1][1] - cov[0][1] * cov[1][0]; let detBlur = (cov[0][0] + 0.3) * (cov[1][1] + 0.3) - cov[0][1] * cov[1][0]; corner.aaFactor = sqrt(detOrig / detBlur); #endif let diagonal1 = cov[0][0] + 0.3; let offDiagonal = cov[0][1]; let diagonal2 = cov[1][1] + 0.3; let mid = 0.5 * (diagonal1 + diagonal2); let radius = length(vec2f((diagonal1 - diagonal2) / 2.0, offDiagonal)); let lambda1 = mid + radius; let lambda2 = max(mid - radius, 0.1); let vmin = min(1024.0, min(uniform.viewport_size.x, uniform.viewport_size.y)); let l1 = 2.0 * min(sqrt(2.0 * lambda1), vmin); let l2 = 2.0 * min(sqrt(2.0 * lambda2), vmin); if (l1 < 2.0 && l2 < 2.0) { return false; } let c = center.proj.ww * uniform.viewport_size.zw; if (any((abs(center.proj.xy) - vec2f(max(l1, l2)) * c) > center.proj.ww)) { return false; } let diagonalVector = normalize(vec2f(offDiagonal, lambda1 - diagonal1)); let v1 = l1 * diagonalVector; let v2 = l2 * vec2f(diagonalVector.y, -diagonalVector.x); corner.offset = (source.cornerUV.x * v1 + source.cornerUV.y * v2) * c; corner.uv = source.cornerUV; return true; } fn initCorner(source: ptr, center: ptr, corner: ptr) -> bool { var rotation: vec4f = getRotation().yzwx; var scale: vec3f = getScale(); modifySplatRotationScale(center.modelCenterOriginal, center.modelCenterModified, &rotation, &scale); var covA: vec3f; var covB: vec3f; computeCovariance(rotation.wxyz, scale, &covA, &covB); modifyCovariance(center.modelCenterOriginal, center.modelCenterModified, &covA, &covB); return initCornerCov(source, center, corner, covA, covB); } `; var gsplatDataVS = ` var transformA: texture_2d; var transformB: texture_2d; var tAw: u32; var tBcached: vec4f; fn readCenter(source: ptr) -> vec3f { let tA: vec4 = textureLoad(transformA, source.uv, 0); tAw = tA.w; tBcached = textureLoad(transformB, source.uv, 0); return bitcast(tA.xyz); } fn unpackRotation(packed: vec3f) -> vec4f { return vec4f(packed.xyz, sqrt(max(0.0, 1.0 - dot(packed, packed)))); } fn getRotation() -> vec4f { return unpackRotation(vec3f(unpack2x16float(tAw), tBcached.w)).wxyz; } fn getScale() -> vec3f { return tBcached.xyz; } `; var gsplatOutputVS = ` #include "tonemappingPS" #include "decodePS" #include "gammaPS" fn prepareOutputFromGamma(gammaColor: vec3f) -> vec3f { #if TONEMAP == NONE #if GAMMA == NONE return decodeGamma3(gammaColor); #else return gammaColor; #endif #else return gammaCorrectOutput(toneMap(decodeGamma3(gammaColor))); #endif } `; var gsplatPS = ` #ifndef DITHER_NONE #include "bayerPS" #include "opacityDitherPS" varying id: f32; #endif #if defined(SHADOW_PASS) || defined(PICK_PASS) || defined(PREPASS_PASS) uniform alphaClip: f32; #endif #ifdef PREPASS_PASS varying vLinearDepth: f32; #include "floatAsUintPS" #endif const EXP4 = exp(-4.0); const INV_EXP4 = 1.0 / (1.0 - EXP4); fn normExp(x: f32) -> f32 { return (exp(x * -4.0) - EXP4) * INV_EXP4; } varying gaussianUV: vec2f; varying gaussianColor: vec4f; #ifdef PICK_PASS #include "pickPS" #endif @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let A: f32 = dot(gaussianUV, gaussianUV); if (A > 1.0) { discard; return output; } var alpha = normExp(A) * gaussianColor.a; #if defined(SHADOW_PASS) || defined(PICK_PASS) || defined(PREPASS_PASS) if (alpha < uniform.alphaClip) { discard; return output; } #endif #ifdef PICK_PASS output.color = getPickOutput(); #ifdef DEPTH_PICK_PASS output.color1 = getPickDepth(); #endif #elif SHADOW_PASS output.color = vec4f(0.0, 0.0, 0.0, 1.0); #elif PREPASS_PASS output.color = float2vec4(vLinearDepth); #else if (alpha < (1.0 / 255.0)) { discard; return output; } #ifndef DITHER_NONE opacityDither(&alpha, id * 0.013); #endif output.color = vec4f(input.gaussianColor.xyz * alpha, alpha); #endif return output; }`; var gsplatSHVS = ` #if SH_BANDS > 0 fn unpack111011s(bits: u32) -> vec3f { return (vec3f((vec3(bits) >> vec3(21u, 11u, 0u)) & vec3(0x7ffu, 0x3ffu, 0x7ffu)) / vec3f(2047.0, 1023.0, 2047.0)) * 2.0 - 1.0; } struct ScaleAndSH { scale: f32, a: vec3f, b: vec3f, c: vec3f }; fn fetchScale(t_in: vec4) -> ScaleAndSH { var result: ScaleAndSH; result.scale = bitcast(t_in.x); result.a = unpack111011s(t_in.y); result.b = unpack111011s(t_in.z); result.c = unpack111011s(t_in.w); return result; } struct SH { a: vec3f, b: vec3f, c: vec3f, d: vec3f }; fn fetch4(t_in: vec4) -> SH { var result: SH; result.a = unpack111011s(t_in.x); result.b = unpack111011s(t_in.y); result.c = unpack111011s(t_in.z); result.d = unpack111011s(t_in.w); return result; } fn fetch1(t_in: u32) -> vec3f { return unpack111011s(t_in); } #if SH_BANDS == 1 var splatSH_1to3: texture_2d; fn readSHData(source: ptr, sh: ptr>, scale: ptr) { let result = fetchScale(textureLoad(splatSH_1to3, source.uv, 0)); *scale = result.scale; sh[0] = result.a; sh[1] = result.b; sh[2] = result.c; } #elif SH_BANDS == 2 var splatSH_1to3: texture_2d; var splatSH_4to7: texture_2d; var splatSH_8to11: texture_2d; fn readSHData(source: ptr, sh: ptr>, scale: ptr) { let first: ScaleAndSH = fetchScale(textureLoad(splatSH_1to3, source.uv, 0)); *scale = first.scale; sh[0] = first.a; sh[1] = first.b; sh[2] = first.c; let second: SH = fetch4(textureLoad(splatSH_4to7, source.uv, 0)); sh[3] = second.a; sh[4] = second.b; sh[5] = second.c; sh[6] = second.d; sh[7] = fetch1(textureLoad(splatSH_8to11, source.uv, 0).x); } #else var splatSH_1to3: texture_2d; var splatSH_4to7: texture_2d; var splatSH_8to11: texture_2d; var splatSH_12to15: texture_2d; fn readSHData(source: ptr, sh: ptr>, scale: ptr) { let first: ScaleAndSH = fetchScale(textureLoad(splatSH_1to3, source.uv, 0)); *scale = first.scale; sh[0] = first.a; sh[1] = first.b; sh[2] = first.c; let second: SH = fetch4(textureLoad(splatSH_4to7, source.uv, 0)); sh[3] = second.a; sh[4] = second.b; sh[5] = second.c; sh[6] = second.d; let third: SH = fetch4(textureLoad(splatSH_8to11, source.uv, 0)); sh[7] = third.a; sh[8] = third.b; sh[9] = third.c; sh[10] = third.d; let fourth: SH = fetch4(textureLoad(splatSH_12to15, source.uv, 0)); sh[11] = fourth.a; sh[12] = fourth.b; sh[13] = fourth.c; sh[14] = fourth.d; } #endif #endif `; var gsplatSourceVS = ` attribute vertex_position: vec3f; attribute vertex_id_attrib: u32; uniform numSplats: u32; uniform splatTextureSize: u32; #ifdef STORAGE_ORDER var splatOrder: array; #else var splatOrder: texture_2d; #endif fn initSource(source: ptr) -> bool { source.order = vertex_id_attrib + u32(vertex_position.z); if (source.order >= uniform.numSplats) { return false; } #ifdef STORAGE_ORDER source.id = splatOrder[source.order]; #else let uv = vec2u(source.order % uniform.splatTextureSize, source.order / uniform.splatTextureSize); source.id = textureLoad(splatOrder, vec2i(uv), 0).r; #endif source.uv = vec2i(vec2u(source.id % uniform.splatTextureSize, source.id / uniform.splatTextureSize)); source.cornerUV = vertex_position.xy; return true; } `; var gsplatVS = ` #include "gsplatCommonVS" varying gaussianUV: vec2f; varying gaussianColor: vec4f; #ifndef DITHER_NONE varying id: f32; #endif const discardVec: vec4f = vec4f(0.0, 0.0, 2.0, 1.0); #ifdef PREPASS_PASS varying vLinearDepth: f32; #endif #ifdef GSPLAT_OVERDRAW uniform colorRampIntensity: f32; var colorRamp: texture_2d; var colorRampSampler: sampler; #endif @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; var source: SplatSource; if (!initSource(&source)) { output.position = discardVec; return output; } var modelCenter: vec3f = readCenter(&source); var center: SplatCenter; center.modelCenterOriginal = modelCenter; modifyCenter(&modelCenter); modifySplatCenter(&modelCenter); center.modelCenterModified = modelCenter; if (!initCenter(modelCenter, ¢er)) { output.position = discardVec; return output; } var corner: SplatCorner; if (!initCorner(&source, ¢er, &corner)) { output.position = discardVec; return output; } var clr: vec4f = readColor(&source); #if GSPLAT_AA clr.a = clr.a * corner.aaFactor; #endif #if SH_BANDS > 0 let modelView3x3 = mat3x3f(center.modelView[0].xyz, center.modelView[1].xyz, center.modelView[2].xyz); let dir = normalize(center.view * modelView3x3); var sh: array; var scale: f32; readSHData(&source, &sh, &scale); clr = vec4f(clr.xyz + evalSH(&sh, dir) * scale, clr.a); #endif modifyColor(modelCenter, &clr); modifySplatColor(modelCenter, &clr); clipCorner(&corner, clr.w); output.position = center.proj + vec4f(corner.offset, 0.0, 0.0); output.gaussianUV = corner.uv; #ifdef GSPLAT_OVERDRAW let t: f32 = clamp(originalCenter.y / 20.0, 0.0, 1.0); let rampColor: vec3f = textureSampleLevel(colorRamp, colorRampSampler, vec2f(t, 0.5), 0.0).rgb; clr.a = clr.a * (1.0 / 32.0) * uniform.colorRampIntensity; output.gaussianColor = vec4f(rampColor, clr.a); #else output.gaussianColor = vec4f(prepareOutputFromGamma(max(clr.xyz, vec3f(0.0))), clr.w); #endif #ifndef DITHER_NONE output.id = f32(source.id); #endif #ifdef PREPASS_PASS output.vLinearDepth = -center.view.z; #endif return output; } `; var gsplatWorkBufferVS = ` var splatTexture0: texture_2d; var splatTexture1: texture_2d; var splatColor: texture_2d; var cachedSplatTexture0Data: vec4u; var cachedSplatTexture1Data: vec2u; fn readCenter(source: ptr) -> vec3f { cachedSplatTexture0Data = textureLoad(splatTexture0, source.uv, 0); cachedSplatTexture1Data = textureLoad(splatTexture1, source.uv, 0).xy; return vec3f(bitcast(cachedSplatTexture0Data.r), bitcast(cachedSplatTexture0Data.g), bitcast(cachedSplatTexture0Data.b)); } fn getRotation() -> vec4f { let rotXY = unpack2x16float(cachedSplatTexture0Data.a); let rotZscaleX = unpack2x16float(cachedSplatTexture1Data.x); let rotXYZ = vec3f(rotXY, rotZscaleX.x); return vec4f(rotXYZ, sqrt(max(0.0, 1.0 - dot(rotXYZ, rotXYZ)))).wxyz; } fn getScale() -> vec3f { let rotZscaleX = unpack2x16float(cachedSplatTexture1Data.x); let scaleYZ = unpack2x16float(cachedSplatTexture1Data.y); return vec3f(rotZscaleX.y, scaleYZ); } fn readColor(source: ptr) -> vec4f { return textureLoad(splatColor, source.uv, 0); } `; const gsplatChunksWGSL = { gsplatCenterVS, gsplatCornerVS, gsplatColorVS, gsplatCommonVS, gsplatCompressedDataVS, gsplatCompressedSHVS, gsplatCustomizeVS, gsplatEvalSHVS, gsplatHelpersVS, gsplatModifyVS, gsplatSourceFormatVS, gsplatStructsVS, gsplatQuatToMat3VS, gsplatSogsColorVS, gsplatSogsDataVS, gsplatSogsSHVS, gsplatDataVS, gsplatOutputVS, gsplatPS, gsplatSHVS, gsplatSourceVS, gsplatVS, gsplatWorkBufferVS, gsplatPackingPS }; const _schema = [ 'enabled' ]; const _properties = [ 'unified', 'lodDistances', 'castShadows', 'material', 'highQualitySH', 'asset', 'layers' ]; class GSplatComponentSystem extends ComponentSystem { initializeComponentData(component, _data, properties) { if (_data.layers && _data.layers.length) { _data.layers = _data.layers.slice(0); } for(let i = 0; i < _properties.length; i++){ if (_data.hasOwnProperty(_properties[i])) { component[_properties[i]] = _data[_properties[i]]; } } if (_data.aabbCenter && _data.aabbHalfExtents) { component.customAabb = new BoundingBox(new Vec3(_data.aabbCenter), new Vec3(_data.aabbHalfExtents)); } super.initializeComponentData(component, _data, _schema); } cloneComponent(entity, clone) { const gSplatComponent = entity.gsplat; const data = {}; _properties.forEach((prop)=>{ if (prop === 'material') { if (!gSplatComponent.unified) { const srcMaterial = gSplatComponent[prop]; if (srcMaterial) { data[prop] = srcMaterial.clone(); } } } else { data[prop] = gSplatComponent[prop]; } }); data.enabled = gSplatComponent.enabled; const component = this.addComponent(clone, data); if (gSplatComponent.customAabb) { component.customAabb = gSplatComponent.customAabb.clone(); } return component; } onRemove(entity, component) { component.onRemove(); } getMaterial(camera, layer) { const director = this.app.renderer.gsplatDirector; if (!director) return null; const cameraData = director.camerasMap.get(camera); if (!cameraData) return null; const layerData = cameraData.layersMap.get(layer); return layerData?.gsplatManager?.material ?? null; } getGSplatMaterial(camera, layer) { return this.getMaterial(camera, layer); } constructor(app){ super(app); this.id = 'gsplat'; this.ComponentType = GSplatComponent; this.DataType = GSplatComponentData; this.schema = _schema; app.renderer.gsplatDirector = new GSplatDirector(app.graphicsDevice, app.renderer, app.scene, this); ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_GLSL).add(gsplatChunksGLSL); ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_WGSL).add(gsplatChunksWGSL); this.on('beforeremove', this.onRemove, this); } } GSplatComponentSystem.EVENT_MATERIALCREATED = 'material:created'; GSplatComponentSystem.EVENT_FRAMEREADY = 'frame:ready'; Component._buildAccessors(GSplatComponent.prototype, _schema); class Render extends EventHandler { set meshes(value) { this.decRefMeshes(); this._meshes = value; this.incRefMeshes(); this.fire('set:meshes', value); } get meshes() { return this._meshes; } destroy() { this.meshes = null; } decRefMeshes() { this._meshes?.forEach((mesh, index)=>{ if (mesh) { mesh.decRefCount(); if (mesh.refCount < 1) { mesh.destroy(); this._meshes[index] = null; } } }); } incRefMeshes() { this._meshes?.forEach((mesh)=>{ mesh?.incRefCount(); }); } constructor(...args){ super(...args), this._meshes = null; } } Render.EVENT_SETMESHES = 'set:meshes'; function onContainerAssetLoaded(containerAsset) { const renderAsset = this; if (!renderAsset.resource) return; const containerResource = containerAsset.resource; const render = containerResource.renders && containerResource.renders[renderAsset.data.renderIndex]; if (render) { renderAsset.resource.meshes = render.resource.meshes; } } function onContainerAssetAdded(containerAsset) { const renderAsset = this; renderAsset.registry.off(`load:${containerAsset.id}`, onContainerAssetLoaded, renderAsset); renderAsset.registry.on(`load:${containerAsset.id}`, onContainerAssetLoaded, renderAsset); renderAsset.registry.off(`remove:${containerAsset.id}`, onContainerAssetRemoved, renderAsset); renderAsset.registry.once(`remove:${containerAsset.id}`, onContainerAssetRemoved, renderAsset); if (!containerAsset.resource) { renderAsset.registry.load(containerAsset); } else { onContainerAssetLoaded.call(renderAsset, containerAsset); } } function onContainerAssetRemoved(containerAsset) { const renderAsset = this; renderAsset.registry.off(`load:${containerAsset.id}`, onContainerAssetLoaded, renderAsset); if (renderAsset.resource) { renderAsset.resource.destroy(); } } class RenderHandler extends ResourceHandler { open(url, data) { return new Render(); } patch(asset, registry) { if (!asset.data.containerAsset) { return; } const containerAsset = registry.get(asset.data.containerAsset); if (!containerAsset) { registry.once(`add:${asset.data.containerAsset}`, onContainerAssetAdded, asset); return; } onContainerAssetAdded.call(asset, containerAsset); } constructor(app){ super(app, 'render'); this._registry = app.assets; } } class AnimCurve { get paths() { return this._paths; } get input() { return this._input; } get output() { return this._output; } get interpolation() { return this._interpolation; } constructor(paths, input, output, interpolation){ this._paths = paths; this._input = input; this._output = output; this._interpolation = interpolation; } } class AnimData { get components() { return this._components; } get data() { return this._data; } constructor(components, data){ this._components = components; this._data = data; } } function DracoWorker(jsUrl, wasmUrl) { let draco; const POSITION_ATTRIBUTE = 0; const NORMAL_ATTRIBUTE = 1; const wrap = (typedArray, dataType)=>{ switch(dataType){ case draco.DT_INT8: return new Int8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); case draco.DT_INT16: return new Int16Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength / 2); case draco.DT_INT32: return new Int32Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength / 4); case draco.DT_UINT8: return new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); case draco.DT_UINT16: return new Uint16Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength / 2); case draco.DT_UINT32: return new Uint32Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength / 4); case draco.DT_FLOAT32: return new Float32Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength / 4); } return null; }; const componentSizeInBytes = (dataType)=>{ switch(dataType){ case draco.DT_INT8: return 1; case draco.DT_INT16: return 2; case draco.DT_INT32: return 4; case draco.DT_UINT8: return 1; case draco.DT_UINT16: return 2; case draco.DT_UINT32: return 4; case draco.DT_FLOAT32: return 4; } return 1; }; const attributeSizeInBytes = (attribute)=>{ return attribute.num_components() * componentSizeInBytes(attribute.data_type()); }; const attributeOrder = { 0: 0, 1: 1, 5: 2, 2: 3, 7: 4, 8: 5, 4: 6, 3: 7 }; const generateNormals = (vertices, indices)=>{ const subtract = (dst, a, b)=>{ dst[0] = a[0] - b[0]; dst[1] = a[1] - b[1]; dst[2] = a[2] - b[2]; }; const cross = (dst, a, b)=>{ dst[0] = a[1] * b[2] - b[1] * a[2]; dst[1] = a[2] * b[0] - b[2] * a[0]; dst[2] = a[0] * b[1] - b[0] * a[1]; }; const normalize = (dst, offset)=>{ const a = dst[offset + 0]; const b = dst[offset + 1]; const c = dst[offset + 2]; const l = 1.0 / Math.sqrt(a * a + b * b + c * c); dst[offset + 0] *= l; dst[offset + 1] *= l; dst[offset + 2] *= l; }; const copy = (dst, src, srcOffset)=>{ for(let i = 0; i < 3; ++i){ dst[i] = src[srcOffset + i]; } }; const numTriangles = indices.length / 3; const numVertices = vertices.length / 3; const result = new Float32Array(vertices.length); const a = [ 0, 0, 0 ], b = [ 0, 0, 0 ], c = [ 0, 0, 0 ], t1 = [ 0, 0, 0 ], t2 = [ 0, 0, 0 ], n = [ 0, 0, 0 ]; for(let i = 0; i < numTriangles; ++i){ const v0 = indices[i * 3 + 0] * 3; const v1 = indices[i * 3 + 1] * 3; const v2 = indices[i * 3 + 2] * 3; copy(a, vertices, v0); copy(b, vertices, v1); copy(c, vertices, v2); subtract(t1, b, a); subtract(t2, c, a); cross(n, t1, t2); normalize(n, 0); for(let j = 0; j < 3; ++j){ result[v0 + j] += n[j]; result[v1 + j] += n[j]; result[v2 + j] += n[j]; } } for(let i = 0; i < numVertices; ++i){ normalize(result, i * 3); } return new Uint8Array(result.buffer); }; const decodeMesh = (inputBuffer)=>{ const result = {}; const buffer = new draco.DecoderBuffer(); buffer.Init(inputBuffer, inputBuffer.length); const decoder = new draco.Decoder(); if (decoder.GetEncodedGeometryType(buffer) !== draco.TRIANGULAR_MESH) { result.error = 'Failed to decode draco mesh: not a mesh'; return result; } const mesh = new draco.Mesh(); const status = decoder.DecodeBufferToMesh(buffer, mesh); if (!status || !status.ok() || draco.getPointer(mesh) === 0) { result.error = 'Failed to decode draco asset'; return result; } const numIndices = mesh.num_faces() * 3; const shortIndices = mesh.num_points() <= 65535; const indicesSize = numIndices * (shortIndices ? 2 : 4); const indicesPtr = draco._malloc(indicesSize); if (shortIndices) { decoder.GetTrianglesUInt16Array(mesh, indicesSize, indicesPtr); result.indices = new Uint16Array(draco.HEAPU16.buffer, indicesPtr, numIndices).slice().buffer; } else { decoder.GetTrianglesUInt32Array(mesh, indicesSize, indicesPtr); result.indices = new Uint32Array(draco.HEAPU32.buffer, indicesPtr, numIndices).slice().buffer; } draco._free(indicesPtr); const attributes = []; for(let i = 0; i < mesh.num_attributes(); ++i){ attributes.push(decoder.GetAttribute(mesh, i)); } attributes.sort((a, b)=>{ return (attributeOrder[a.attribute_type()] ?? attributeOrder.length) - (attributeOrder[b.attribute_type()] ?? attributeOrder.length); }); result.attributes = attributes.map((a)=>a.unique_id()); let totalVertexSize = 0; const offsets = attributes.map((a)=>{ const offset = totalVertexSize; totalVertexSize += Math.ceil(attributeSizeInBytes(a) / 4) * 4; return offset; }); const hasNormals = attributes.some((a)=>a.attribute_type() === NORMAL_ATTRIBUTE); const normalOffset = offsets[1]; if (!hasNormals) { for(let i = 1; i < offsets.length; ++i){ offsets[i] += 12; } totalVertexSize += 12; } result.vertices = new ArrayBuffer(mesh.num_points() * totalVertexSize); const dst = new Uint8Array(result.vertices); for(let i = 0; i < mesh.num_attributes(); ++i){ const attribute = attributes[i]; const sizeInBytes = attributeSizeInBytes(attribute); const ptrSize = mesh.num_points() * sizeInBytes; const ptr = draco._malloc(ptrSize); decoder.GetAttributeDataArrayForAllPoints(mesh, attribute, attribute.data_type(), ptrSize, ptr); const src = new Uint8Array(draco.HEAPU8.buffer, ptr, ptrSize); for(let j = 0; j < mesh.num_points(); ++j){ for(let c = 0; c < sizeInBytes; ++c){ dst[j * totalVertexSize + offsets[i] + c] = src[j * sizeInBytes + c]; } } if (!hasNormals && attribute.attribute_type() === POSITION_ATTRIBUTE) { const normals = generateNormals(wrap(src, attribute.data_type()), shortIndices ? new Uint16Array(result.indices) : new Uint32Array(result.indices)); for(let j = 0; j < mesh.num_points(); ++j){ for(let c = 0; c < 12; ++c){ dst[j * totalVertexSize + normalOffset + c] = normals[j * 12 + c]; } } } draco._free(ptr); } draco.destroy(mesh); draco.destroy(decoder); draco.destroy(buffer); return result; }; const decode = (data)=>{ const result = decodeMesh(new Uint8Array(data.buffer)); self.postMessage({ jobId: data.jobId, error: result.error, indices: result.indices, vertices: result.vertices, attributes: result.attributes }, [ result.indices, result.vertices ].filter((t)=>t != null)); }; const workQueue = []; self.onmessage = (message)=>{ const data = message.data; switch(data.type){ case 'init': self.DracoDecoderModule({ instantiateWasm: (imports, successCallback)=>{ WebAssembly.instantiate(data.module, imports).then((result)=>successCallback(result)).catch((reason)=>console.error(`instantiate failed + ${reason}`)); return {}; } }).then((instance)=>{ draco = instance; workQueue.forEach((data)=>decode(data)); }); break; case 'decodeMesh': if (draco) { decode(data); } else { workQueue.push(data); } break; } }; } const downloadMaxRetries = 3; class JobQueue { init(workers) { workers.forEach((worker)=>{ worker.addEventListener('message', (message)=>{ const data = message.data; const callback = this.jobCallbacks.get(data.jobId); if (callback) { callback(data.error, { indices: data.indices, vertices: data.vertices, attributes: data.attributes }); } this.jobCallbacks.delete(data.jobId); if (this.jobQueue.length > 0) { const job = this.jobQueue.shift(); this.run(worker, job); } else { const index2 = this.workers[2].indexOf(worker); if (index2 !== -1) { this.workers[2].splice(index2, 1); this.workers[1].push(worker); } else { const index1 = this.workers[1].indexOf(worker); if (index1 !== -1) { this.workers[1].splice(index1, 1); this.workers[0].push(worker); } } } }); }); this.workers[0] = workers; while(this.jobQueue.length && (this.workers[0].length || this.workers[1].length)){ const job = this.jobQueue.shift(); if (this.workers[0].length > 0) { const worker = this.workers[0].shift(); this.workers[1].push(worker); this.run(worker, job); } else { const worker = this.workers[1].shift(); this.workers[2].push(worker); this.run(worker, job); } } } enqueueJob(buffer, callback) { const job = { jobId: this.jobId++, buffer: buffer }; this.jobCallbacks.set(job.jobId, callback); if (this.workers[0].length > 0) { const worker = this.workers[0].shift(); this.workers[1].push(worker); this.run(worker, job); } else if (this.workers[1].length > 0) { const worker = this.workers[1].shift(); this.workers[2].push(worker); this.run(worker, job); } else { this.jobQueue.push(job); } } constructor(){ this.workers = [ [], [], [] ]; this.jobId = 0; this.jobQueue = []; this.jobCallbacks = new Map(); this.run = (worker, job)=>{ worker.postMessage({ type: 'decodeMesh', jobId: job.jobId, buffer: job.buffer }, [ job.buffer ]); }; } } const downloadScript = (url)=>{ return new Promise((resolve, reject)=>{ const options = { cache: true, responseType: 'text', retry: downloadMaxRetries > 0, maxRetries: downloadMaxRetries }; http.get(url, options, (err, response)=>{ if (err) { reject(err); } else { resolve(response); } }); }); }; const compileModule = (url)=>{ const compileManual = ()=>{ return fetch(url).then((result)=>result.arrayBuffer()).then((buffer)=>WebAssembly.compile(buffer)); }; const compileStreaming = ()=>{ return WebAssembly.compileStreaming(fetch(url)).catch((err)=>{ return compileManual(); }); }; return WebAssembly.compileStreaming ? compileStreaming() : compileManual(); }; const defaultNumWorkers$1 = 1; let jobQueue; const initializeWorkers = (config)=>{ if (jobQueue) { return true; } if (!config) { { const moduleConfig = WasmModule.getConfig('DracoDecoderModule'); if (moduleConfig) { config = { jsUrl: moduleConfig.glueUrl, wasmUrl: moduleConfig.wasmUrl, numWorkers: moduleConfig.numWorkers }; } else { config = { jsUrl: 'draco.wasm.js', wasmUrl: 'draco.wasm.wasm', numWorkers: defaultNumWorkers$1 }; } } } if (!config.jsUrl || !config.wasmUrl) { return false; } jobQueue = new JobQueue(); Promise.all([ downloadScript(config.jsUrl), compileModule(config.wasmUrl) ]).then(([dracoSource, dracoModule])=>{ const code = [ '/* draco */', dracoSource, '/* worker */', `(\n${DracoWorker.toString()}\n)()\n\n` ].join('\n'); const blob = new Blob([ code ], { type: "application/javascript" }); const workerUrl = URL.createObjectURL(blob); const numWorkers = Math.max(1, Math.min(16, config.numWorkers || defaultNumWorkers$1)); const workers = []; for(let i = 0; i < numWorkers; ++i){ const worker = new Worker(workerUrl); worker.postMessage({ type: 'init', module: dracoModule }); workers.push(worker); } jobQueue.init(workers); }); return true; }; const dracoDecode = (buffer, callback)=>{ if (!initializeWorkers()) { return false; } jobQueue.enqueueJob(buffer, callback); return true; }; class GlbResources { destroy() { if (this.renders) { this.renders.forEach((render)=>{ render.meshes = null; }); } } } const isDataURI = (uri)=>{ return /^data:[^\n\r,\u2028\u2029]*,.*$/i.test(uri); }; const getDataURIMimeType = (uri)=>{ return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';')); }; const getNumComponents = (accessorType)=>{ switch(accessorType){ case 'SCALAR': return 1; case 'VEC2': return 2; case 'VEC3': return 3; case 'VEC4': return 4; case 'MAT2': return 4; case 'MAT3': return 9; case 'MAT4': return 16; default: return 3; } }; const getComponentType = (componentType)=>{ switch(componentType){ case 5120: return TYPE_INT8; case 5121: return TYPE_UINT8; case 5122: return TYPE_INT16; case 5123: return TYPE_UINT16; case 5124: return TYPE_INT32; case 5125: return TYPE_UINT32; case 5126: return TYPE_FLOAT32; default: return 0; } }; const getComponentSizeInBytes = (componentType)=>{ switch(componentType){ case 5120: return 1; case 5121: return 1; case 5122: return 2; case 5123: return 2; case 5124: return 4; case 5125: return 4; case 5126: return 4; default: return 0; } }; const getComponentDataType = (componentType)=>{ switch(componentType){ case 5120: return Int8Array; case 5121: return Uint8Array; case 5122: return Int16Array; case 5123: return Uint16Array; case 5124: return Int32Array; case 5125: return Uint32Array; case 5126: return Float32Array; default: return null; } }; const gltfToEngineSemanticMap = { 'POSITION': SEMANTIC_POSITION, 'NORMAL': SEMANTIC_NORMAL, 'TANGENT': SEMANTIC_TANGENT, 'COLOR_0': SEMANTIC_COLOR, 'JOINTS_0': SEMANTIC_BLENDINDICES, 'WEIGHTS_0': SEMANTIC_BLENDWEIGHT, 'TEXCOORD_0': SEMANTIC_TEXCOORD0, 'TEXCOORD_1': SEMANTIC_TEXCOORD1, 'TEXCOORD_2': SEMANTIC_TEXCOORD2, 'TEXCOORD_3': SEMANTIC_TEXCOORD3, 'TEXCOORD_4': SEMANTIC_TEXCOORD4, 'TEXCOORD_5': SEMANTIC_TEXCOORD5, 'TEXCOORD_6': SEMANTIC_TEXCOORD6, 'TEXCOORD_7': SEMANTIC_TEXCOORD7 }; const attributeOrder = { [SEMANTIC_POSITION]: 0, [SEMANTIC_NORMAL]: 1, [SEMANTIC_TANGENT]: 2, [SEMANTIC_COLOR]: 3, [SEMANTIC_BLENDINDICES]: 4, [SEMANTIC_BLENDWEIGHT]: 5, [SEMANTIC_TEXCOORD0]: 6, [SEMANTIC_TEXCOORD1]: 7, [SEMANTIC_TEXCOORD2]: 8, [SEMANTIC_TEXCOORD3]: 9, [SEMANTIC_TEXCOORD4]: 10, [SEMANTIC_TEXCOORD5]: 11, [SEMANTIC_TEXCOORD6]: 12, [SEMANTIC_TEXCOORD7]: 13 }; const getDequantizeFunc = (srcType)=>{ switch(srcType){ case TYPE_INT8: return (x)=>Math.max(x / 127.0, -1); case TYPE_UINT8: return (x)=>x / 255.0; case TYPE_INT16: return (x)=>Math.max(x / 32767.0, -1); case TYPE_UINT16: return (x)=>x / 65535.0; default: return (x)=>x; } }; const dequantizeArray = (dstArray, srcArray, srcType)=>{ const convFunc = getDequantizeFunc(srcType); const len = srcArray.length; for(let i = 0; i < len; ++i){ dstArray[i] = convFunc(srcArray[i]); } return dstArray; }; const getAccessorData = (gltfAccessor, bufferViews, flatten = false)=>{ const numComponents = getNumComponents(gltfAccessor.type); const dataType = getComponentDataType(gltfAccessor.componentType); if (!dataType) { return null; } let result; if (gltfAccessor.sparse) { const sparse = gltfAccessor.sparse; const indicesAccessor = { count: sparse.count, type: 'SCALAR' }; const indices = getAccessorData(Object.assign(indicesAccessor, sparse.indices), bufferViews, true); const valuesAccessor = { count: sparse.count, type: gltfAccessor.type, componentType: gltfAccessor.componentType }; const values = getAccessorData(Object.assign(valuesAccessor, sparse.values), bufferViews, true); if (gltfAccessor.hasOwnProperty('bufferView')) { const baseAccessor = { bufferView: gltfAccessor.bufferView, byteOffset: gltfAccessor.byteOffset, componentType: gltfAccessor.componentType, count: gltfAccessor.count, type: gltfAccessor.type }; result = getAccessorData(baseAccessor, bufferViews, true).slice(); } else { result = new dataType(gltfAccessor.count * numComponents); } for(let i = 0; i < sparse.count; ++i){ const targetIndex = indices[i]; for(let j = 0; j < numComponents; ++j){ result[targetIndex * numComponents + j] = values[i * numComponents + j]; } } } else { if (gltfAccessor.hasOwnProperty('bufferView')) { const bufferView = bufferViews[gltfAccessor.bufferView]; if (flatten && bufferView.hasOwnProperty('byteStride')) { const bytesPerElement = numComponents * dataType.BYTES_PER_ELEMENT; const storage = new ArrayBuffer(gltfAccessor.count * bytesPerElement); const tmpArray = new Uint8Array(storage); let dstOffset = 0; for(let i = 0; i < gltfAccessor.count; ++i){ let srcOffset = (gltfAccessor.byteOffset || 0) + i * bufferView.byteStride; for(let b = 0; b < bytesPerElement; ++b){ tmpArray[dstOffset++] = bufferView[srcOffset++]; } } result = new dataType(storage); } else { result = new dataType(bufferView.buffer, bufferView.byteOffset + (gltfAccessor.byteOffset || 0), gltfAccessor.count * numComponents); } } else { result = new dataType(gltfAccessor.count * numComponents); } } return result; }; const getAccessorDataFloat32 = (gltfAccessor, bufferViews)=>{ const data = getAccessorData(gltfAccessor, bufferViews, true); if (data instanceof Float32Array || !gltfAccessor.normalized) { return data; } const float32Data = new Float32Array(data.length); dequantizeArray(float32Data, data, getComponentType(gltfAccessor.componentType)); return float32Data; }; const getAccessorBoundingBox = (gltfAccessor)=>{ let min = gltfAccessor.min; let max = gltfAccessor.max; if (!min || !max) { return null; } if (gltfAccessor.normalized) { const ctype = getComponentType(gltfAccessor.componentType); min = dequantizeArray([], min, ctype); max = dequantizeArray([], max, ctype); } return new BoundingBox(new Vec3((max[0] + min[0]) * 0.5, (max[1] + min[1]) * 0.5, (max[2] + min[2]) * 0.5), new Vec3((max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5)); }; const getPrimitiveType = (primitive)=>{ if (!primitive.hasOwnProperty('mode')) { return PRIMITIVE_TRIANGLES; } switch(primitive.mode){ case 0: return PRIMITIVE_POINTS; case 1: return PRIMITIVE_LINES; case 2: return PRIMITIVE_LINELOOP; case 3: return PRIMITIVE_LINESTRIP; case 4: return PRIMITIVE_TRIANGLES; case 5: return PRIMITIVE_TRISTRIP; case 6: return PRIMITIVE_TRIFAN; default: return PRIMITIVE_TRIANGLES; } }; const generateIndices = (numVertices)=>{ const dummyIndices = new Uint16Array(numVertices); for(let i = 0; i < numVertices; i++){ dummyIndices[i] = i; } return dummyIndices; }; const generateNormals = (sourceDesc, indices)=>{ const p = sourceDesc[SEMANTIC_POSITION]; if (!p || p.components !== 3) { return; } let positions; if (p.size !== p.stride) { const srcStride = p.stride / typedArrayTypesByteSize[p.type]; const src = new typedArrayTypes[p.type](p.buffer, p.offset, p.count * srcStride); positions = new typedArrayTypes[p.type](p.count * 3); for(let i = 0; i < p.count; ++i){ positions[i * 3 + 0] = src[i * srcStride + 0]; positions[i * 3 + 1] = src[i * srcStride + 1]; positions[i * 3 + 2] = src[i * srcStride + 2]; } } else { positions = new typedArrayTypes[p.type](p.buffer, p.offset, p.count * 3); } const numVertices = p.count; if (!indices) { indices = generateIndices(numVertices); } const normalsTemp = calculateNormals(positions, indices); const normals = new Float32Array(normalsTemp.length); normals.set(normalsTemp); sourceDesc[SEMANTIC_NORMAL] = { buffer: normals.buffer, size: 12, offset: 0, stride: 12, count: numVertices, components: 3, type: TYPE_FLOAT32 }; }; const cloneTexture = (texture)=>{ const shallowCopyLevels = (texture)=>{ const result = []; for(let mip = 0; mip < texture._levels.length; ++mip){ let level = []; if (texture.cubemap) { for(let face = 0; face < 6; ++face){ level.push(texture._levels[mip][face]); } } else { level = texture._levels[mip]; } result.push(level); } return result; }; const result = new Texture(texture.device, texture); result._levels = shallowCopyLevels(texture); return result; }; const cloneTextureAsset = (src)=>{ const result = new Asset(`${src.name}_clone`, src.type, src.file, src.data, src.options); result.loaded = true; result.resource = cloneTexture(src.resource); src.registry.add(result); return result; }; const createVertexBufferInternal = (device, sourceDesc)=>{ const positionDesc = sourceDesc[SEMANTIC_POSITION]; if (!positionDesc) { return null; } const numVertices = positionDesc.count; const vertexDesc = []; for(const semantic in sourceDesc){ if (sourceDesc.hasOwnProperty(semantic)) { const element = { semantic: semantic, components: sourceDesc[semantic].components, type: sourceDesc[semantic].type, normalize: !!sourceDesc[semantic].normalize }; if (!VertexFormat.isElementValid(device, element)) { element.components++; } vertexDesc.push(element); } } vertexDesc.sort((lhs, rhs)=>{ return attributeOrder[lhs.semantic] - attributeOrder[rhs.semantic]; }); let i, j, k; let source, target, sourceOffset; const vertexFormat = new VertexFormat(device, vertexDesc); let isCorrectlyInterleaved = true; for(i = 0; i < vertexFormat.elements.length; ++i){ target = vertexFormat.elements[i]; source = sourceDesc[target.name]; sourceOffset = source.offset - positionDesc.offset; if (source.buffer !== positionDesc.buffer || source.stride !== target.stride || source.size !== target.size || sourceOffset !== target.offset) { isCorrectlyInterleaved = false; break; } } const vertexBuffer = new VertexBuffer(device, vertexFormat, numVertices); const vertexData = vertexBuffer.lock(); const targetArray = new Uint32Array(vertexData); let sourceArray; if (isCorrectlyInterleaved) { sourceArray = new Uint32Array(positionDesc.buffer, positionDesc.offset, numVertices * vertexBuffer.format.size / 4); targetArray.set(sourceArray); } else { let targetStride, sourceStride; for(i = 0; i < vertexBuffer.format.elements.length; ++i){ target = vertexBuffer.format.elements[i]; targetStride = target.stride / 4; source = sourceDesc[target.name]; sourceStride = source.stride / 4; sourceArray = new Uint32Array(source.buffer, source.offset, (source.count - 1) * sourceStride + (source.size + 3) / 4); let src = 0; let dst = target.offset / 4; const kend = Math.floor((source.size + 3) / 4); for(j = 0; j < numVertices; ++j){ for(k = 0; k < kend; ++k){ targetArray[dst + k] = sourceArray[src + k]; } src += sourceStride; dst += targetStride; } } } vertexBuffer.unlock(); return vertexBuffer; }; const createVertexBuffer = (device, attributes, indices, accessors, bufferViews, vertexBufferDict)=>{ const useAttributes = {}; const attribIds = []; for(const attrib in attributes){ if (attributes.hasOwnProperty(attrib) && gltfToEngineSemanticMap.hasOwnProperty(attrib)) { useAttributes[attrib] = attributes[attrib]; attribIds.push(`${attrib}:${attributes[attrib]}`); } } attribIds.sort(); const vbKey = attribIds.join(); let vb = vertexBufferDict[vbKey]; if (!vb) { const sourceDesc = {}; for(const attrib in useAttributes){ const accessor = accessors[attributes[attrib]]; const accessorData = getAccessorData(accessor, bufferViews); const bufferView = bufferViews[accessor.bufferView]; const semantic = gltfToEngineSemanticMap[attrib]; const size = getNumComponents(accessor.type) * getComponentSizeInBytes(accessor.componentType); const stride = bufferView && bufferView.hasOwnProperty('byteStride') ? bufferView.byteStride : size; sourceDesc[semantic] = { buffer: accessorData.buffer, size: size, offset: accessorData.byteOffset, stride: stride, count: accessor.count, components: getNumComponents(accessor.type), type: getComponentType(accessor.componentType), normalize: accessor.normalized }; } if (!sourceDesc.hasOwnProperty(SEMANTIC_NORMAL)) { generateNormals(sourceDesc, indices); } vb = createVertexBufferInternal(device, sourceDesc); vertexBufferDict[vbKey] = vb; } return vb; }; const createSkin = (device, gltfSkin, accessors, bufferViews, nodes, glbSkins)=>{ let i, j, bindMatrix; const joints = gltfSkin.joints; const numJoints = joints.length; const ibp = []; if (gltfSkin.hasOwnProperty('inverseBindMatrices')) { const inverseBindMatrices = gltfSkin.inverseBindMatrices; const ibmData = getAccessorData(accessors[inverseBindMatrices], bufferViews, true); const ibmValues = []; for(i = 0; i < numJoints; i++){ for(j = 0; j < 16; j++){ ibmValues[j] = ibmData[i * 16 + j]; } bindMatrix = new Mat4(); bindMatrix.set(ibmValues); ibp.push(bindMatrix); } } else { for(i = 0; i < numJoints; i++){ bindMatrix = new Mat4(); ibp.push(bindMatrix); } } const boneNames = []; for(i = 0; i < numJoints; i++){ boneNames[i] = nodes[joints[i]].name; } const key = boneNames.join('#'); let skin = glbSkins.get(key); if (!skin) { skin = new Skin(device, ibp, boneNames); glbSkins.set(key, skin); } return skin; }; const createDracoMesh = (device, primitive, accessors, bufferViews, meshVariants, meshDefaultMaterials, promises)=>{ const result = new Mesh(device); result.aabb = getAccessorBoundingBox(accessors[primitive.attributes.POSITION]); const vertexDesc = []; for (const [name, index] of Object.entries(primitive.attributes)){ const accessor = accessors[index]; const semantic = gltfToEngineSemanticMap[name]; const componentType = getComponentType(accessor.componentType); vertexDesc.push({ semantic: semantic, components: getNumComponents(accessor.type), type: componentType, normalize: accessor.normalized ?? (semantic === SEMANTIC_COLOR && (componentType === TYPE_UINT8 || componentType === TYPE_UINT16)) }); } promises.push(new Promise((resolve, reject)=>{ const dracoExt = primitive.extensions.KHR_draco_mesh_compression; dracoDecode(bufferViews[dracoExt.bufferView].slice().buffer, (err, decompressedData)=>{ if (err) { console.log(err); reject(err); } else { const order = {}; for (const [name, index] of Object.entries(dracoExt.attributes)){ order[gltfToEngineSemanticMap[name]] = decompressedData.attributes.indexOf(index); } vertexDesc.sort((a, b)=>{ return order[a.semantic] - order[b.semantic]; }); if (!primitive.attributes?.NORMAL) { vertexDesc.splice(1, 0, { semantic: 'NORMAL', components: 3, type: TYPE_FLOAT32 }); } const vertexFormat = new VertexFormat(device, vertexDesc); const numVertices = decompressedData.vertices.byteLength / vertexFormat.size; const indexFormat = numVertices <= 65535 ? INDEXFORMAT_UINT16 : INDEXFORMAT_UINT32; const numIndices = decompressedData.indices.byteLength / (numVertices <= 65535 ? 2 : 4); const vertexBuffer = new VertexBuffer(device, vertexFormat, numVertices, { data: decompressedData.vertices }); const indexBuffer = new IndexBuffer(device, indexFormat, numIndices, BUFFER_STATIC, decompressedData.indices); result.vertexBuffer = vertexBuffer; result.indexBuffer[0] = indexBuffer; result.primitive[0].type = getPrimitiveType(primitive); result.primitive[0].base = 0; result.primitive[0].count = indexBuffer ? numIndices : numVertices; result.primitive[0].indexed = !!indexBuffer; resolve(); } }); })); if (primitive?.extensions?.KHR_materials_variants) { const variants = primitive.extensions.KHR_materials_variants; const tempMapping = {}; variants.mappings.forEach((mapping)=>{ mapping.variants.forEach((variant)=>{ tempMapping[variant] = mapping.material; }); }); meshVariants[result.id] = tempMapping; } meshDefaultMaterials[result.id] = primitive.material; return result; }; const createMesh = (device, gltfMesh, accessors, bufferViews, vertexBufferDict, meshVariants, meshDefaultMaterials, assetOptions, promises)=>{ const meshes = []; gltfMesh.primitives.forEach((primitive)=>{ if (primitive.extensions?.KHR_draco_mesh_compression) { meshes.push(createDracoMesh(device, primitive, accessors, bufferViews, meshVariants, meshDefaultMaterials, promises)); } else { let indices = primitive.hasOwnProperty('indices') ? getAccessorData(accessors[primitive.indices], bufferViews, true) : null; const vertexBuffer = createVertexBuffer(device, primitive.attributes, indices, accessors, bufferViews, vertexBufferDict); const primitiveType = getPrimitiveType(primitive); const mesh = new Mesh(device); mesh.vertexBuffer = vertexBuffer; mesh.primitive[0].type = primitiveType; mesh.primitive[0].base = 0; mesh.primitive[0].indexed = indices !== null; if (indices !== null) { let indexFormat; if (indices instanceof Uint8Array) { indexFormat = INDEXFORMAT_UINT8; } else if (indices instanceof Uint16Array) { indexFormat = INDEXFORMAT_UINT16; } else { indexFormat = INDEXFORMAT_UINT32; } if (indexFormat === INDEXFORMAT_UINT8 && device.isWebGPU) { indexFormat = INDEXFORMAT_UINT16; indices = new Uint16Array(indices); } const indexBuffer = new IndexBuffer(device, indexFormat, indices.length, BUFFER_STATIC, indices); mesh.indexBuffer[0] = indexBuffer; mesh.primitive[0].count = indices.length; } else { mesh.primitive[0].count = vertexBuffer.numVertices; } if (primitive.hasOwnProperty('extensions') && primitive.extensions.hasOwnProperty('KHR_materials_variants')) { const variants = primitive.extensions.KHR_materials_variants; const tempMapping = {}; variants.mappings.forEach((mapping)=>{ mapping.variants.forEach((variant)=>{ tempMapping[variant] = mapping.material; }); }); meshVariants[mesh.id] = tempMapping; } meshDefaultMaterials[mesh.id] = primitive.material; let accessor = accessors[primitive.attributes.POSITION]; mesh.aabb = getAccessorBoundingBox(accessor); if (primitive.hasOwnProperty('targets')) { const targets = []; primitive.targets.forEach((target, index)=>{ const options = {}; if (target.hasOwnProperty('POSITION')) { accessor = accessors[target.POSITION]; options.deltaPositions = getAccessorDataFloat32(accessor, bufferViews); options.aabb = getAccessorBoundingBox(accessor); } if (target.hasOwnProperty('NORMAL')) { accessor = accessors[target.NORMAL]; options.deltaNormals = getAccessorDataFloat32(accessor, bufferViews); } if (gltfMesh.hasOwnProperty('extras') && gltfMesh.extras.hasOwnProperty('targetNames')) { options.name = gltfMesh.extras.targetNames[index]; } else { options.name = index.toString(10); } if (gltfMesh.hasOwnProperty('weights')) { options.defaultWeight = gltfMesh.weights[index]; } options.preserveData = assetOptions.morphPreserveData; targets.push(new MorphTarget(options)); }); mesh.morph = new Morph(targets, device, { preferHighPrecision: assetOptions.morphPreferHighPrecision }); } meshes.push(mesh); } }); return meshes; }; const extractTextureTransform = (source, material, maps)=>{ let map; const texCoord = source.texCoord; if (texCoord) { for(map = 0; map < maps.length; ++map){ material[`${maps[map]}MapUv`] = texCoord; } } const zeros = [ 0, 0 ]; const ones = [ 1, 1 ]; const textureTransform = source.extensions?.KHR_texture_transform; if (textureTransform) { const offset = textureTransform.offset || zeros; const scale = textureTransform.scale || ones; const rotation = textureTransform.rotation ? -textureTransform.rotation * math.RAD_TO_DEG : 0; const tilingVec = new Vec2(scale[0], scale[1]); const offsetVec = new Vec2(offset[0], 1.0 - scale[1] - offset[1]); for(map = 0; map < maps.length; ++map){ material[`${maps[map]}MapTiling`] = tilingVec; material[`${maps[map]}MapOffset`] = offsetVec; material[`${maps[map]}MapRotation`] = rotation; } } }; const extensionPbrSpecGlossiness = (data, material, textures)=>{ let texture; if (data.hasOwnProperty('diffuseFactor')) { const [r, g, b, a] = data.diffuseFactor; material.diffuse.set(r, g, b).gamma(); material.opacity = a; } else { material.diffuse.set(1, 1, 1); material.opacity = 1; } if (data.hasOwnProperty('diffuseTexture')) { const diffuseTexture = data.diffuseTexture; texture = textures[diffuseTexture.index]; material.diffuseMap = texture; material.diffuseMapChannel = 'rgb'; material.opacityMap = texture; material.opacityMapChannel = 'a'; extractTextureTransform(diffuseTexture, material, [ 'diffuse', 'opacity' ]); } material.useMetalness = false; if (data.hasOwnProperty('specularFactor')) { const [r, g, b] = data.specularFactor; material.specular.set(r, g, b).gamma(); } else { material.specular.set(1, 1, 1); } if (data.hasOwnProperty('glossinessFactor')) { material.gloss = data.glossinessFactor; } else { material.gloss = 1.0; } if (data.hasOwnProperty('specularGlossinessTexture')) { const specularGlossinessTexture = data.specularGlossinessTexture; material.specularMap = material.glossMap = textures[specularGlossinessTexture.index]; material.specularMapChannel = 'rgb'; material.glossMapChannel = 'a'; extractTextureTransform(specularGlossinessTexture, material, [ 'gloss', 'metalness' ]); } }; const extensionClearCoat = (data, material, textures)=>{ if (data.hasOwnProperty('clearcoatFactor')) { material.clearCoat = data.clearcoatFactor * 0.25; } else { material.clearCoat = 0; } if (data.hasOwnProperty('clearcoatTexture')) { const clearcoatTexture = data.clearcoatTexture; material.clearCoatMap = textures[clearcoatTexture.index]; material.clearCoatMapChannel = 'r'; extractTextureTransform(clearcoatTexture, material, [ 'clearCoat' ]); } if (data.hasOwnProperty('clearcoatRoughnessFactor')) { material.clearCoatGloss = data.clearcoatRoughnessFactor; } else { material.clearCoatGloss = 0; } if (data.hasOwnProperty('clearcoatRoughnessTexture')) { const clearcoatRoughnessTexture = data.clearcoatRoughnessTexture; material.clearCoatGlossMap = textures[clearcoatRoughnessTexture.index]; material.clearCoatGlossMapChannel = 'g'; extractTextureTransform(clearcoatRoughnessTexture, material, [ 'clearCoatGloss' ]); } if (data.hasOwnProperty('clearcoatNormalTexture')) { const clearcoatNormalTexture = data.clearcoatNormalTexture; material.clearCoatNormalMap = textures[clearcoatNormalTexture.index]; extractTextureTransform(clearcoatNormalTexture, material, [ 'clearCoatNormal' ]); if (clearcoatNormalTexture.hasOwnProperty('scale')) { material.clearCoatBumpiness = clearcoatNormalTexture.scale; } else { material.clearCoatBumpiness = 1; } } material.clearCoatGlossInvert = true; }; const extensionUnlit = (data, material, textures)=>{ material.useLighting = false; material.emissive.copy(material.diffuse); material.emissiveMap = material.diffuseMap; material.emissiveMapUv = material.diffuseMapUv; material.emissiveMapTiling.copy(material.diffuseMapTiling); material.emissiveMapOffset.copy(material.diffuseMapOffset); material.emissiveMapRotation = material.diffuseMapRotation; material.emissiveMapChannel = material.diffuseMapChannel; material.emissiveVertexColor = material.diffuseVertexColor; material.emissiveVertexColorChannel = material.diffuseVertexColorChannel; material.useLighting = false; material.useSkybox = false; material.diffuse.set(1, 1, 1); material.diffuseMap = null; material.diffuseVertexColor = false; }; const extensionSpecular = (data, material, textures)=>{ material.useMetalnessSpecularColor = true; if (data.hasOwnProperty('specularColorTexture')) { material.specularMap = textures[data.specularColorTexture.index]; material.specularMapChannel = 'rgb'; extractTextureTransform(data.specularColorTexture, material, [ 'specular' ]); } if (data.hasOwnProperty('specularColorFactor')) { const [r, g, b] = data.specularColorFactor; material.specular.set(r, g, b).gamma(); } else { material.specular.set(1, 1, 1); } if (data.hasOwnProperty('specularFactor')) { material.specularityFactor = data.specularFactor; } else { material.specularityFactor = 1; } if (data.hasOwnProperty('specularTexture')) { material.specularityFactorMapChannel = 'a'; material.specularityFactorMap = textures[data.specularTexture.index]; extractTextureTransform(data.specularTexture, material, [ 'specularityFactor' ]); } }; const extensionIor = (data, material, textures)=>{ if (data.hasOwnProperty('ior')) { material.refractionIndex = 1.0 / data.ior; } }; const extensionDispersion = (data, material, textures)=>{ if (data.hasOwnProperty('dispersion')) { material.dispersion = data.dispersion; } }; const extensionTransmission = (data, material, textures)=>{ material.blendType = BLEND_NORMAL; material.useDynamicRefraction = true; if (data.hasOwnProperty('transmissionFactor')) { material.refraction = data.transmissionFactor; } if (data.hasOwnProperty('transmissionTexture')) { material.refractionMapChannel = 'r'; material.refractionMap = textures[data.transmissionTexture.index]; extractTextureTransform(data.transmissionTexture, material, [ 'refraction' ]); } }; const extensionSheen = (data, material, textures)=>{ material.useSheen = true; if (data.hasOwnProperty('sheenColorFactor')) { const [r, g, b] = data.sheenColorFactor; material.sheen.set(r, g, b).gamma(); } else { material.sheen.set(1, 1, 1); } if (data.hasOwnProperty('sheenColorTexture')) { material.sheenMap = textures[data.sheenColorTexture.index]; extractTextureTransform(data.sheenColorTexture, material, [ 'sheen' ]); } material.sheenGloss = data.hasOwnProperty('sheenRoughnessFactor') ? data.sheenRoughnessFactor : 0.0; if (data.hasOwnProperty('sheenRoughnessTexture')) { material.sheenGlossMap = textures[data.sheenRoughnessTexture.index]; material.sheenGlossMapChannel = 'a'; extractTextureTransform(data.sheenRoughnessTexture, material, [ 'sheenGloss' ]); } material.sheenGlossInvert = true; }; const extensionVolume = (data, material, textures)=>{ material.blendType = BLEND_NORMAL; material.useDynamicRefraction = true; if (data.hasOwnProperty('thicknessFactor')) { material.thickness = data.thicknessFactor; } if (data.hasOwnProperty('thicknessTexture')) { material.thicknessMap = textures[data.thicknessTexture.index]; material.thicknessMapChannel = 'g'; extractTextureTransform(data.thicknessTexture, material, [ 'thickness' ]); } if (data.hasOwnProperty('attenuationDistance')) { material.attenuationDistance = data.attenuationDistance; } if (data.hasOwnProperty('attenuationColor')) { const [r, g, b] = data.attenuationColor; material.attenuation.set(r, g, b).gamma(); } }; const extensionEmissiveStrength = (data, material, textures)=>{ if (data.hasOwnProperty('emissiveStrength')) { material.emissiveIntensity = data.emissiveStrength; } }; const extensionIridescence = (data, material, textures)=>{ material.useIridescence = true; if (data.hasOwnProperty('iridescenceFactor')) { material.iridescence = data.iridescenceFactor; } if (data.hasOwnProperty('iridescenceTexture')) { material.iridescenceMapChannel = 'r'; material.iridescenceMap = textures[data.iridescenceTexture.index]; extractTextureTransform(data.iridescenceTexture, material, [ 'iridescence' ]); } if (data.hasOwnProperty('iridescenceIor')) { material.iridescenceRefractionIndex = data.iridescenceIor; } if (data.hasOwnProperty('iridescenceThicknessMinimum')) { material.iridescenceThicknessMin = data.iridescenceThicknessMinimum; } if (data.hasOwnProperty('iridescenceThicknessMaximum')) { material.iridescenceThicknessMax = data.iridescenceThicknessMaximum; } if (data.hasOwnProperty('iridescenceThicknessTexture')) { material.iridescenceThicknessMapChannel = 'g'; material.iridescenceThicknessMap = textures[data.iridescenceThicknessTexture.index]; extractTextureTransform(data.iridescenceThicknessTexture, material, [ 'iridescenceThickness' ]); } }; const extensionAnisotropy = (data, material, textures)=>{ material.enableGGXSpecular = true; if (data.hasOwnProperty('anisotropyStrength')) { material.anisotropyIntensity = data.anisotropyStrength; } else { material.anisotropyIntensity = 0; } if (data.hasOwnProperty('anisotropyTexture')) { const anisotropyTexture = data.anisotropyTexture; material.anisotropyMap = textures[anisotropyTexture.index]; extractTextureTransform(anisotropyTexture, material, [ 'anisotropy' ]); } if (data.hasOwnProperty('anisotropyRotation')) { material.anisotropyRotation = data.anisotropyRotation * math.RAD_TO_DEG; } else { material.anisotropyRotation = 0; } }; const createMaterial = (gltfMaterial, textures)=>{ const material = new StandardMaterial(); if (gltfMaterial.hasOwnProperty('name')) { material.name = gltfMaterial.name; } material.occludeSpecular = SPECOCC_AO; material.diffuseVertexColor = true; material.specularTint = true; material.specularVertexColor = true; material.specular.set(1, 1, 1); material.gloss = 1; material.glossInvert = true; material.useMetalness = true; let texture; if (gltfMaterial.hasOwnProperty('pbrMetallicRoughness')) { const pbrData = gltfMaterial.pbrMetallicRoughness; if (pbrData.hasOwnProperty('baseColorFactor')) { const [r, g, b, a] = pbrData.baseColorFactor; material.diffuse.set(r, g, b).gamma(); material.opacity = a; } if (pbrData.hasOwnProperty('baseColorTexture')) { const baseColorTexture = pbrData.baseColorTexture; texture = textures[baseColorTexture.index]; material.diffuseMap = texture; material.diffuseMapChannel = 'rgb'; material.opacityMap = texture; material.opacityMapChannel = 'a'; extractTextureTransform(baseColorTexture, material, [ 'diffuse', 'opacity' ]); } if (pbrData.hasOwnProperty('metallicFactor')) { material.metalness = pbrData.metallicFactor; } if (pbrData.hasOwnProperty('roughnessFactor')) { material.gloss = pbrData.roughnessFactor; } if (pbrData.hasOwnProperty('metallicRoughnessTexture')) { const metallicRoughnessTexture = pbrData.metallicRoughnessTexture; material.metalnessMap = material.glossMap = textures[metallicRoughnessTexture.index]; material.metalnessMapChannel = 'b'; material.glossMapChannel = 'g'; extractTextureTransform(metallicRoughnessTexture, material, [ 'gloss', 'metalness' ]); } } if (gltfMaterial.hasOwnProperty('normalTexture')) { const normalTexture = gltfMaterial.normalTexture; material.normalMap = textures[normalTexture.index]; extractTextureTransform(normalTexture, material, [ 'normal' ]); if (normalTexture.hasOwnProperty('scale')) { material.bumpiness = normalTexture.scale; } } if (gltfMaterial.hasOwnProperty('occlusionTexture')) { const occlusionTexture = gltfMaterial.occlusionTexture; material.aoMap = textures[occlusionTexture.index]; material.aoMapChannel = 'r'; extractTextureTransform(occlusionTexture, material, [ 'ao' ]); } if (gltfMaterial.hasOwnProperty('emissiveFactor')) { const [r, g, b] = gltfMaterial.emissiveFactor; material.emissive.set(r, g, b).gamma(); } if (gltfMaterial.hasOwnProperty('emissiveTexture')) { const emissiveTexture = gltfMaterial.emissiveTexture; material.emissiveMap = textures[emissiveTexture.index]; extractTextureTransform(emissiveTexture, material, [ 'emissive' ]); } if (gltfMaterial.hasOwnProperty('alphaMode')) { switch(gltfMaterial.alphaMode){ case 'MASK': material.blendType = BLEND_NONE; if (gltfMaterial.hasOwnProperty('alphaCutoff')) { material.alphaTest = gltfMaterial.alphaCutoff; } else { material.alphaTest = 0.5; } break; case 'BLEND': material.blendType = BLEND_NORMAL; material.depthWrite = false; break; default: case 'OPAQUE': material.blendType = BLEND_NONE; break; } } else { material.blendType = BLEND_NONE; } if (gltfMaterial.hasOwnProperty('doubleSided')) { material.twoSidedLighting = gltfMaterial.doubleSided; material.cull = gltfMaterial.doubleSided ? CULLFACE_NONE : CULLFACE_BACK; } else { material.twoSidedLighting = false; material.cull = CULLFACE_BACK; } const extensions = { 'KHR_materials_clearcoat': extensionClearCoat, 'KHR_materials_emissive_strength': extensionEmissiveStrength, 'KHR_materials_ior': extensionIor, 'KHR_materials_dispersion': extensionDispersion, 'KHR_materials_iridescence': extensionIridescence, 'KHR_materials_pbrSpecularGlossiness': extensionPbrSpecGlossiness, 'KHR_materials_sheen': extensionSheen, 'KHR_materials_specular': extensionSpecular, 'KHR_materials_transmission': extensionTransmission, 'KHR_materials_unlit': extensionUnlit, 'KHR_materials_volume': extensionVolume, 'KHR_materials_anisotropy': extensionAnisotropy }; if (gltfMaterial.hasOwnProperty('extensions')) { for(const key in gltfMaterial.extensions){ const extensionFunc = extensions[key]; if (extensionFunc !== undefined) { extensionFunc(gltfMaterial.extensions[key], material, textures); } } } material.update(); return material; }; const createAnimation = (gltfAnimation, animationIndex, gltfAccessors, bufferViews, nodes, meshes, gltfNodes)=>{ const createAnimData = (gltfAccessor)=>{ return new AnimData(getNumComponents(gltfAccessor.type), getAccessorDataFloat32(gltfAccessor, bufferViews)); }; const interpMap = { 'STEP': INTERPOLATION_STEP, 'LINEAR': INTERPOLATION_LINEAR, 'CUBICSPLINE': INTERPOLATION_CUBIC }; const inputMap = {}; const outputMap = {}; const curveMap = {}; let outputCounter = 1; let i; for(i = 0; i < gltfAnimation.samplers.length; ++i){ const sampler = gltfAnimation.samplers[i]; if (!inputMap.hasOwnProperty(sampler.input)) { inputMap[sampler.input] = createAnimData(gltfAccessors[sampler.input]); } if (!outputMap.hasOwnProperty(sampler.output)) { outputMap[sampler.output] = createAnimData(gltfAccessors[sampler.output]); } const interpolation = sampler.hasOwnProperty('interpolation') && interpMap.hasOwnProperty(sampler.interpolation) ? interpMap[sampler.interpolation] : INTERPOLATION_LINEAR; const curve = { paths: [], input: sampler.input, output: sampler.output, interpolation: interpolation }; curveMap[i] = curve; } const quatArrays = []; const transformSchema = { 'translation': 'localPosition', 'rotation': 'localRotation', 'scale': 'localScale' }; const constructNodePath = (node)=>{ const path = []; while(node){ path.unshift(node.name); node = node.parent; } return path; }; const createMorphTargetCurves = (curve, gltfNode, entityPath)=>{ const out = outputMap[curve.output]; if (!out) { return; } let targetNames; if (meshes && meshes[gltfNode.mesh]) { const mesh = meshes[gltfNode.mesh]; if (mesh.hasOwnProperty('extras') && mesh.extras.hasOwnProperty('targetNames')) { targetNames = mesh.extras.targetNames; } } const outData = out.data; const morphTargetCount = outData.length / inputMap[curve.input].data.length; const keyframeCount = outData.length / morphTargetCount; const singleBufferSize = keyframeCount * 4; const buffer = new ArrayBuffer(singleBufferSize * morphTargetCount); for(let j = 0; j < morphTargetCount; j++){ const morphTargetOutput = new Float32Array(buffer, singleBufferSize * j, keyframeCount); for(let k = 0; k < keyframeCount; k++){ morphTargetOutput[k] = outData[k * morphTargetCount + j]; } const output = new AnimData(1, morphTargetOutput); const weightName = targetNames?.[j] ? `name.${targetNames[j]}` : j; outputMap[-outputCounter] = output; const morphCurve = { paths: [ { entityPath: entityPath, component: 'graph', propertyPath: [ `weight.${weightName}` ] } ], input: curve.input, output: -outputCounter, interpolation: curve.interpolation }; outputCounter++; curveMap[`morphCurve-${i}-${j}`] = morphCurve; } }; for(i = 0; i < gltfAnimation.channels.length; ++i){ const channel = gltfAnimation.channels[i]; const target = channel.target; const curve = curveMap[channel.sampler]; const node = nodes[target.node]; const gltfNode = gltfNodes[target.node]; const entityPath = constructNodePath(node); if (target.path.startsWith('weights')) { createMorphTargetCurves(curve, gltfNode, entityPath); curveMap[channel.sampler].morphCurve = true; } else { curve.paths.push({ entityPath: entityPath, component: 'graph', propertyPath: [ transformSchema[target.path] ] }); } } const inputs = []; const outputs = []; const curves = []; for(const inputKey in inputMap){ inputs.push(inputMap[inputKey]); inputMap[inputKey] = inputs.length - 1; } for(const outputKey in outputMap){ outputs.push(outputMap[outputKey]); outputMap[outputKey] = outputs.length - 1; } for(const curveKey in curveMap){ const curveData = curveMap[curveKey]; if (curveData.morphCurve) { continue; } curves.push(new AnimCurve(curveData.paths, inputMap[curveData.input], outputMap[curveData.output], curveData.interpolation)); if (curveData.paths.length > 0 && curveData.paths[0].propertyPath[0] === 'localRotation' && curveData.interpolation !== INTERPOLATION_CUBIC) { quatArrays.push(curves[curves.length - 1].output); } } quatArrays.sort(); let prevIndex = null; let data; for(i = 0; i < quatArrays.length; ++i){ const index = quatArrays[i]; if (i === 0 || index !== prevIndex) { data = outputs[index]; if (data.components === 4) { const d = data.data; const len = d.length - 4; for(let j = 0; j < len; j += 4){ const dp = d[j + 0] * d[j + 4] + d[j + 1] * d[j + 5] + d[j + 2] * d[j + 6] + d[j + 3] * d[j + 7]; if (dp < 0) { d[j + 4] *= -1; d[j + 5] *= -1; d[j + 6] *= -1; d[j + 7] *= -1; } } } prevIndex = index; } } let duration = 0; for(i = 0; i < inputs.length; i++){ data = inputs[i]._data; duration = Math.max(duration, data.length === 0 ? 0 : data[data.length - 1]); } return new AnimTrack(gltfAnimation.hasOwnProperty('name') ? gltfAnimation.name : `animation_${animationIndex}`, duration, inputs, outputs, curves); }; const tempMat = new Mat4(); const tempVec = new Vec3(); const tempQuat = new Quat(); const createNode = (gltfNode, nodeIndex, nodeInstancingMap)=>{ const entity = new GraphNode(); if (gltfNode.hasOwnProperty('name') && gltfNode.name.length > 0) { entity.name = gltfNode.name; } else { entity.name = `node_${nodeIndex}`; } if (gltfNode.hasOwnProperty('matrix')) { tempMat.data.set(gltfNode.matrix); tempMat.getTranslation(tempVec); entity.setLocalPosition(tempVec); tempQuat.setFromMat4(tempMat); entity.setLocalRotation(tempQuat); tempMat.getScale(tempVec); tempVec.x *= tempMat.scaleSign; entity.setLocalScale(tempVec); } if (gltfNode.hasOwnProperty('rotation')) { const r = gltfNode.rotation; entity.setLocalRotation(r[0], r[1], r[2], r[3]); } if (gltfNode.hasOwnProperty('translation')) { const t = gltfNode.translation; entity.setLocalPosition(t[0], t[1], t[2]); } if (gltfNode.hasOwnProperty('scale')) { const s = gltfNode.scale; entity.setLocalScale(s[0], s[1], s[2]); } if (gltfNode.hasOwnProperty('extensions') && gltfNode.extensions.EXT_mesh_gpu_instancing) { nodeInstancingMap.set(gltfNode, { ext: gltfNode.extensions.EXT_mesh_gpu_instancing }); } return entity; }; const createCamera$1 = (gltfCamera, node)=>{ const isOrthographic = gltfCamera.type === 'orthographic'; const gltfProperties = isOrthographic ? gltfCamera.orthographic : gltfCamera.perspective; const componentData = { enabled: false, projection: isOrthographic ? PROJECTION_ORTHOGRAPHIC : PROJECTION_PERSPECTIVE, nearClip: gltfProperties.znear, aspectRatioMode: ASPECT_AUTO }; if (gltfProperties.zfar) { componentData.farClip = gltfProperties.zfar; } if (isOrthographic) { componentData.orthoHeight = gltfProperties.ymag; if (gltfProperties.xmag && gltfProperties.ymag) { componentData.aspectRatioMode = ASPECT_MANUAL; componentData.aspectRatio = gltfProperties.xmag / gltfProperties.ymag; } } else { componentData.fov = gltfProperties.yfov * math.RAD_TO_DEG; if (gltfProperties.aspectRatio) { componentData.aspectRatioMode = ASPECT_MANUAL; componentData.aspectRatio = gltfProperties.aspectRatio; } } const cameraEntity = new Entity(gltfCamera.name); cameraEntity.addComponent('camera', componentData); return cameraEntity; }; const createLight = (gltfLight, node)=>{ const lightProps = { enabled: false, type: gltfLight.type === 'point' ? 'omni' : gltfLight.type, color: gltfLight.hasOwnProperty('color') ? new Color(gltfLight.color) : Color.WHITE, range: gltfLight.hasOwnProperty('range') ? gltfLight.range : 9999, falloffMode: LIGHTFALLOFF_INVERSESQUARED, intensity: gltfLight.hasOwnProperty('intensity') ? math.clamp(gltfLight.intensity, 0, 2) : 1 }; if (gltfLight.hasOwnProperty('spot')) { lightProps.innerConeAngle = gltfLight.spot.hasOwnProperty('innerConeAngle') ? gltfLight.spot.innerConeAngle * math.RAD_TO_DEG : 0; lightProps.outerConeAngle = gltfLight.spot.hasOwnProperty('outerConeAngle') ? gltfLight.spot.outerConeAngle * math.RAD_TO_DEG : 45; } if (gltfLight.hasOwnProperty('intensity')) { const outerAngleRad = gltfLight.spot?.outerConeAngle ?? Math.PI / 4; const innerAngleRad = gltfLight.spot?.innerConeAngle ?? 0; lightProps.luminance = gltfLight.intensity * Light.getLightUnitConversion(lightTypes[lightProps.type], outerAngleRad, innerAngleRad); } const lightEntity = new Entity(node.name); lightEntity.rotateLocal(90, 0, 0); lightEntity.addComponent('light', lightProps); return lightEntity; }; const createSkins = (device, gltf, nodes, bufferViews)=>{ if (!gltf.hasOwnProperty('skins') || gltf.skins.length === 0) { return []; } const glbSkins = new Map(); return gltf.skins.map((gltfSkin)=>{ return createSkin(device, gltfSkin, gltf.accessors, bufferViews, nodes, glbSkins); }); }; const createMeshes = (device, gltf, bufferViews, options)=>{ const vertexBufferDict = {}; const meshVariants = {}; const meshDefaultMaterials = {}; const promises = []; const valid = !options.skipMeshes && gltf?.meshes?.length && gltf?.accessors?.length && gltf?.bufferViews?.length; const meshes = valid ? gltf.meshes.map((gltfMesh)=>{ return createMesh(device, gltfMesh, gltf.accessors, bufferViews, vertexBufferDict, meshVariants, meshDefaultMaterials, options, promises); }) : []; return { meshes, meshVariants, meshDefaultMaterials, promises }; }; const createMaterials = (gltf, textures, options)=>{ if (!gltf.hasOwnProperty('materials') || gltf.materials.length === 0) { return []; } const preprocess = options?.material?.preprocess; const process = options?.material?.process ?? createMaterial; const postprocess = options?.material?.postprocess; return gltf.materials.map((gltfMaterial)=>{ if (preprocess) { preprocess(gltfMaterial); } const material = process(gltfMaterial, textures); if (postprocess) { postprocess(gltfMaterial, material); } return material; }); }; const createVariants = (gltf)=>{ if (!gltf.hasOwnProperty('extensions') || !gltf.extensions.hasOwnProperty('KHR_materials_variants')) { return null; } const data = gltf.extensions.KHR_materials_variants.variants; const variants = {}; for(let i = 0; i < data.length; i++){ variants[data[i].name] = i; } return variants; }; const createAnimations = (gltf, nodes, bufferViews, options)=>{ if (!gltf.hasOwnProperty('animations') || gltf.animations.length === 0) { return []; } const preprocess = options?.animation?.preprocess; const postprocess = options?.animation?.postprocess; return gltf.animations.map((gltfAnimation, index)=>{ if (preprocess) { preprocess(gltfAnimation); } const animation = createAnimation(gltfAnimation, index, gltf.accessors, bufferViews, nodes, gltf.meshes, gltf.nodes); if (postprocess) { postprocess(gltfAnimation, animation); } return animation; }); }; const createInstancing = (device, gltf, nodeInstancingMap, bufferViews)=>{ const accessors = gltf.accessors; nodeInstancingMap.forEach((data, entity)=>{ const attributes = data.ext.attributes; let translations; if (attributes.hasOwnProperty('TRANSLATION')) { const accessor = accessors[attributes.TRANSLATION]; translations = getAccessorDataFloat32(accessor, bufferViews); } let rotations; if (attributes.hasOwnProperty('ROTATION')) { const accessor = accessors[attributes.ROTATION]; rotations = getAccessorDataFloat32(accessor, bufferViews); } let scales; if (attributes.hasOwnProperty('SCALE')) { const accessor = accessors[attributes.SCALE]; scales = getAccessorDataFloat32(accessor, bufferViews); } const instanceCount = (translations ? translations.length / 3 : 0) || (rotations ? rotations.length / 4 : 0) || (scales ? scales.length / 3 : 0); if (instanceCount) { const matrices = new Float32Array(instanceCount * 16); const pos = new Vec3(); const rot = new Quat(); const scl = new Vec3(1, 1, 1); const matrix = new Mat4(); let matrixIndex = 0; for(let i = 0; i < instanceCount; i++){ const i3 = i * 3; if (translations) { pos.set(translations[i3], translations[i3 + 1], translations[i3 + 2]); } if (rotations) { const i4 = i * 4; rot.set(rotations[i4], rotations[i4 + 1], rotations[i4 + 2], rotations[i4 + 3]); } if (scales) { scl.set(scales[i3], scales[i3 + 1], scales[i3 + 2]); } matrix.setTRS(pos, rot, scl); for(let m = 0; m < 16; m++){ matrices[matrixIndex++] = matrix.data[m]; } } data.matrices = matrices; } }); }; const createNodes = (gltf, options, nodeInstancingMap)=>{ if (!gltf.hasOwnProperty('nodes') || gltf.nodes.length === 0) { return []; } const preprocess = options?.node?.preprocess; const process = options?.node?.process ?? createNode; const postprocess = options?.node?.postprocess; const nodes = gltf.nodes.map((gltfNode, index)=>{ if (preprocess) { preprocess(gltfNode); } const node = process(gltfNode, index, nodeInstancingMap); if (postprocess) { postprocess(gltfNode, node); } return node; }); for(let i = 0; i < gltf.nodes.length; ++i){ const gltfNode = gltf.nodes[i]; if (gltfNode.hasOwnProperty('children')) { const parent = nodes[i]; const uniqueNames = {}; for(let j = 0; j < gltfNode.children.length; ++j){ const child = nodes[gltfNode.children[j]]; if (!child.parent) { if (uniqueNames.hasOwnProperty(child.name)) { child.name += uniqueNames[child.name]++; } else { uniqueNames[child.name] = 1; } parent.addChild(child); } } } } return nodes; }; const createScenes = (gltf, nodes)=>{ const scenes = []; const count = gltf.scenes.length; if (count === 1 && gltf.scenes[0].nodes?.length === 1) { const nodeIndex = gltf.scenes[0].nodes[0]; scenes.push(nodes[nodeIndex]); } else { for(let i = 0; i < count; i++){ const scene = gltf.scenes[i]; if (scene.nodes) { const sceneRoot = new GraphNode(scene.name); for(let n = 0; n < scene.nodes.length; n++){ const childNode = nodes[scene.nodes[n]]; sceneRoot.addChild(childNode); } scenes.push(sceneRoot); } } } return scenes; }; const createCameras = (gltf, nodes, options)=>{ let cameras = null; if (gltf.hasOwnProperty('nodes') && gltf.hasOwnProperty('cameras') && gltf.cameras.length > 0) { const preprocess = options?.camera?.preprocess; const process = options?.camera?.process ?? createCamera$1; const postprocess = options?.camera?.postprocess; gltf.nodes.forEach((gltfNode, nodeIndex)=>{ if (gltfNode.hasOwnProperty('camera')) { const gltfCamera = gltf.cameras[gltfNode.camera]; if (gltfCamera) { if (preprocess) { preprocess(gltfCamera); } const camera = process(gltfCamera, nodes[nodeIndex]); if (postprocess) { postprocess(gltfCamera, camera); } if (camera) { if (!cameras) cameras = new Map(); cameras.set(gltfNode, camera); } } } }); } return cameras; }; const createLights = (gltf, nodes, options)=>{ let lights = null; if (gltf.hasOwnProperty('nodes') && gltf.hasOwnProperty('extensions') && gltf.extensions.hasOwnProperty('KHR_lights_punctual') && gltf.extensions.KHR_lights_punctual.hasOwnProperty('lights')) { const gltfLights = gltf.extensions.KHR_lights_punctual.lights; if (gltfLights.length) { const preprocess = options?.light?.preprocess; const process = options?.light?.process ?? createLight; const postprocess = options?.light?.postprocess; gltf.nodes.forEach((gltfNode, nodeIndex)=>{ if (gltfNode.hasOwnProperty('extensions') && gltfNode.extensions.hasOwnProperty('KHR_lights_punctual') && gltfNode.extensions.KHR_lights_punctual.hasOwnProperty('light')) { const lightIndex = gltfNode.extensions.KHR_lights_punctual.light; const gltfLight = gltfLights[lightIndex]; if (gltfLight) { if (preprocess) { preprocess(gltfLight); } const light = process(gltfLight, nodes[nodeIndex]); if (postprocess) { postprocess(gltfLight, light); } if (light) { if (!lights) lights = new Map(); lights.set(gltfNode, light); } } } }); } } return lights; }; const linkSkins = (gltf, renders, skins)=>{ gltf.nodes.forEach((gltfNode)=>{ if (gltfNode.hasOwnProperty('mesh') && gltfNode.hasOwnProperty('skin')) { const meshGroup = renders[gltfNode.mesh].meshes; meshGroup.forEach((mesh)=>{ mesh.skin = skins[gltfNode.skin]; }); } }); }; const createResources = async (device, gltf, bufferViews, textures, options)=>{ const preprocess = options?.global?.preprocess; const postprocess = options?.global?.postprocess; if (preprocess) { preprocess(gltf); } if (gltf.asset && gltf.asset.generator === 'PlayCanvas') ; const nodeInstancingMap = new Map(); const nodes = createNodes(gltf, options, nodeInstancingMap); const scenes = createScenes(gltf, nodes); const lights = createLights(gltf, nodes, options); const cameras = createCameras(gltf, nodes, options); const variants = createVariants(gltf); const bufferViewData = await Promise.all(bufferViews); const { meshes, meshVariants, meshDefaultMaterials, promises } = createMeshes(device, gltf, bufferViewData, options); const animations = createAnimations(gltf, nodes, bufferViewData, options); createInstancing(device, gltf, nodeInstancingMap, bufferViewData); const textureAssets = await Promise.all(textures); const textureInstances = textureAssets.map((t)=>t.resource); const materials = createMaterials(gltf, textureInstances, options); const skins = createSkins(device, gltf, nodes, bufferViewData); const renders = []; for(let i = 0; i < meshes.length; i++){ renders[i] = new Render(); renders[i].meshes = meshes[i]; } linkSkins(gltf, renders, skins); const result = new GlbResources(); result.gltf = gltf; result.nodes = nodes; result.scenes = scenes; result.animations = animations; result.textures = textureAssets; result.materials = materials; result.variants = variants; result.meshVariants = meshVariants; result.meshDefaultMaterials = meshDefaultMaterials; result.renders = renders; result.skins = skins; result.lights = lights; result.cameras = cameras; result.nodeInstancingMap = nodeInstancingMap; if (postprocess) { postprocess(gltf, result); } await Promise.all(promises); return result; }; const applySampler = (texture, gltfSampler)=>{ const getFilter = (filter, defaultValue)=>{ switch(filter){ case 9728: return FILTER_NEAREST; case 9729: return FILTER_LINEAR; case 9984: return FILTER_NEAREST_MIPMAP_NEAREST; case 9985: return FILTER_LINEAR_MIPMAP_NEAREST; case 9986: return FILTER_NEAREST_MIPMAP_LINEAR; case 9987: return FILTER_LINEAR_MIPMAP_LINEAR; default: return defaultValue; } }; const getWrap = (wrap, defaultValue)=>{ switch(wrap){ case 33071: return ADDRESS_CLAMP_TO_EDGE; case 33648: return ADDRESS_MIRRORED_REPEAT; case 10497: return ADDRESS_REPEAT; default: return defaultValue; } }; if (texture) { gltfSampler = gltfSampler ?? {}; texture.minFilter = getFilter(gltfSampler.minFilter, FILTER_LINEAR_MIPMAP_LINEAR); texture.magFilter = getFilter(gltfSampler.magFilter, FILTER_LINEAR); texture.addressU = getWrap(gltfSampler.wrapS, ADDRESS_REPEAT); texture.addressV = getWrap(gltfSampler.wrapT, ADDRESS_REPEAT); } }; let gltfTextureUniqueId = 0; const getTextureSource = (gltfTexture)=>gltfTexture.extensions?.KHR_texture_basisu?.source ?? gltfTexture.extensions?.EXT_texture_webp?.source ?? gltfTexture.source; const createImages = (gltf, bufferViews, urlBase, registry, options)=>{ if (!gltf.images || gltf.images.length === 0) { return []; } const preprocess = options?.image?.preprocess; const processAsync = options?.image?.processAsync; const postprocess = options?.image?.postprocess; const mimeTypeFileExtensions = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/basis': 'basis', 'image/ktx': 'ktx', 'image/ktx2': 'ktx2', 'image/vnd-ms.dds': 'dds' }; const getGammaTextures = (gltf)=>{ const set = new Set(); if (gltf.hasOwnProperty('materials')) { gltf.materials.forEach((gltfMaterial)=>{ if (gltfMaterial.hasOwnProperty('pbrMetallicRoughness')) { const pbrData = gltfMaterial.pbrMetallicRoughness; if (pbrData.hasOwnProperty('baseColorTexture')) { const gltfTexture = gltf.textures[pbrData.baseColorTexture.index]; set.add(getTextureSource(gltfTexture)); } } if (gltfMaterial.hasOwnProperty('emissiveTexture')) { const gltfTexture = gltf.textures[gltfMaterial.emissiveTexture.index]; set.add(getTextureSource(gltfTexture)); } if (gltfMaterial.hasOwnProperty('extensions')) { const sheen = gltfMaterial.extensions.KHR_materials_sheen; if (sheen) { if (sheen.hasOwnProperty('sheenColorTexture')) { const gltfTexture = gltf.textures[sheen.sheenColorTexture.index]; set.add(getTextureSource(gltfTexture)); } } const specularGlossiness = gltfMaterial.extensions.KHR_materials_pbrSpecularGlossiness; if (specularGlossiness) { if (specularGlossiness.hasOwnProperty('specularGlossinessTexture')) { const gltfTexture = gltf.textures[specularGlossiness.specularGlossinessTexture.index]; set.add(getTextureSource(gltfTexture)); } } const specular = gltfMaterial.extensions.KHR_materials_specular; if (specular) { if (specular.hasOwnProperty('specularColorTexture')) { const gltfTexture = gltf.textures[specular.specularColorTexture.index]; set.add(getTextureSource(gltfTexture)); } } } }); } return set; }; const loadTexture = (gltfImage, url, bufferView, mimeType, options, srgb)=>{ return new Promise((resolve, reject)=>{ const continuation = (bufferViewData)=>{ const name = `${gltfImage.name || 'gltf-texture'}-${gltfTextureUniqueId++}`; const file = { url: url || name }; if (bufferViewData) { file.contents = bufferViewData.slice(0).buffer; } if (mimeType) { const extension = mimeTypeFileExtensions[mimeType]; if (extension) { file.filename = `${file.url}.${extension}`; } } const data = { srgb }; const asset = new Asset(name, 'texture', file, data, options); asset.on('load', (asset)=>resolve(asset)); asset.on('error', (err)=>reject(err)); registry.add(asset); registry.load(asset); }; if (bufferView) { bufferView.then((bufferViewData)=>continuation(bufferViewData)); } else { continuation(null); } }); }; const gammaTextures = getGammaTextures(gltf); return gltf.images.map((gltfImage, i)=>{ if (preprocess) { preprocess(gltfImage); } let promise; if (processAsync) { promise = new Promise((resolve, reject)=>{ processAsync(gltfImage, (err, textureAsset)=>{ if (err) { reject(err); } else { resolve(textureAsset); } }); }); } else { promise = new Promise((resolve)=>{ resolve(null); }); } promise = promise.then((textureAsset)=>{ const srgb = gammaTextures.has(i); if (textureAsset) { return textureAsset; } else if (gltfImage.hasOwnProperty('uri')) { if (isDataURI(gltfImage.uri)) { return loadTexture(gltfImage, gltfImage.uri, null, getDataURIMimeType(gltfImage.uri), null, srgb); } return loadTexture(gltfImage, ABSOLUTE_URL.test(gltfImage.uri) ? gltfImage.uri : path.join(urlBase, gltfImage.uri), null, null, { crossOrigin: 'anonymous' }, srgb); } else if (gltfImage.hasOwnProperty('bufferView') && gltfImage.hasOwnProperty('mimeType')) { return loadTexture(gltfImage, null, bufferViews[gltfImage.bufferView], gltfImage.mimeType, null, srgb); } return Promise.reject(new Error(`Invalid image found in gltf (neither uri or bufferView found). index=${i}`)); }); if (postprocess) { promise = promise.then((textureAsset)=>{ postprocess(gltfImage, textureAsset); return textureAsset; }); } return promise; }); }; const createTextures = (gltf, images, options)=>{ if (!gltf?.images?.length || !gltf?.textures?.length) { return []; } const preprocess = options?.texture?.preprocess; const processAsync = options?.texture?.processAsync; const postprocess = options?.texture?.postprocess; const seenImages = new Set(); return gltf.textures.map((gltfTexture)=>{ if (preprocess) { preprocess(gltfTexture); } let promise; if (processAsync) { promise = new Promise((resolve, reject)=>{ processAsync(gltfTexture, gltf.images, (err, gltfImageIndex)=>{ if (err) { reject(err); } else { resolve(gltfImageIndex); } }); }); } else { promise = new Promise((resolve)=>{ resolve(null); }); } promise = promise.then((gltfImageIndex)=>{ gltfImageIndex = gltfImageIndex ?? getTextureSource(gltfTexture); const cloneAsset = seenImages.has(gltfImageIndex); seenImages.add(gltfImageIndex); return images[gltfImageIndex].then((imageAsset)=>{ const asset = cloneAsset ? cloneTextureAsset(imageAsset) : imageAsset; applySampler(asset.resource, (gltf.samplers ?? [])[gltfTexture.sampler]); return asset; }); }); if (postprocess) { promise = promise.then((textureAsset)=>{ postprocess(gltfTexture, textureAsset); return textureAsset; }); } return promise; }); }; const loadBuffers = (gltf, binaryChunk, urlBase, options)=>{ if (!gltf.buffers || gltf.buffers.length === 0) { return []; } const preprocess = options?.buffer?.preprocess; const processAsync = options?.buffer?.processAsync; const postprocess = options?.buffer?.postprocess; return gltf.buffers.map((gltfBuffer, i)=>{ if (preprocess) { preprocess(gltfBuffer); } let promise; if (processAsync) { promise = new Promise((resolve, reject)=>{ processAsync(gltfBuffer, (err, arrayBuffer)=>{ if (err) { reject(err); } else { resolve(arrayBuffer); } }); }); } else { promise = new Promise((resolve)=>{ resolve(null); }); } promise = promise.then((arrayBuffer)=>{ if (arrayBuffer) { return arrayBuffer; } else if (gltfBuffer.hasOwnProperty('uri')) { if (isDataURI(gltfBuffer.uri)) { const byteString = atob(gltfBuffer.uri.split(',')[1]); const binaryArray = new Uint8Array(byteString.length); for(let j = 0; j < byteString.length; j++){ binaryArray[j] = byteString.charCodeAt(j); } return binaryArray; } return new Promise((resolve, reject)=>{ http.get(ABSOLUTE_URL.test(gltfBuffer.uri) ? gltfBuffer.uri : path.join(urlBase, gltfBuffer.uri), { cache: true, responseType: 'arraybuffer', retry: false }, (err, result)=>{ if (err) { reject(err); } else { resolve(new Uint8Array(result)); } }); }); } return binaryChunk; }); if (postprocess) { promise = promise.then((buffer)=>{ postprocess(gltf.buffers[i], buffer); return buffer; }); } return promise; }); }; const parseGltf = (gltfChunk, callback)=>{ const decodeBinaryUtf8 = (array)=>{ if (typeof TextDecoder !== 'undefined') { return new TextDecoder().decode(array); } let str = ''; for(let i = 0; i < array.length; i++){ str += String.fromCharCode(array[i]); } return decodeURIComponent(escape(str)); }; const gltf = JSON.parse(decodeBinaryUtf8(gltfChunk)); if (gltf.asset && gltf.asset.version && parseFloat(gltf.asset.version) < 2) { callback(`Invalid gltf version. Expected version 2.0 or above but found version '${gltf.asset.version}'.`); return; } callback(null, gltf); }; const parseGlb = (glbData, callback)=>{ const data = glbData instanceof ArrayBuffer ? new DataView(glbData) : new DataView(glbData.buffer, glbData.byteOffset, glbData.byteLength); const magic = data.getUint32(0, true); const version = data.getUint32(4, true); const length = data.getUint32(8, true); if (magic !== 0x46546C67) { callback(`Invalid magic number found in glb header. Expected 0x46546C67, found 0x${magic.toString(16)}`); return; } if (version !== 2) { callback(`Invalid version number found in glb header. Expected 2, found ${version}`); return; } if (length <= 0 || length > data.byteLength) { callback(`Invalid length found in glb header. Found ${length}`); return; } const chunks = []; let offset = 12; while(offset < length){ const chunkLength = data.getUint32(offset, true); if (offset + chunkLength + 8 > data.byteLength) { callback(`Invalid chunk length found in glb. Found ${chunkLength}`); } const chunkType = data.getUint32(offset + 4, true); const chunkData = new Uint8Array(data.buffer, data.byteOffset + offset + 8, chunkLength); chunks.push({ length: chunkLength, type: chunkType, data: chunkData }); offset += chunkLength + 8; } if (chunks.length !== 1 && chunks.length !== 2) { callback('Invalid number of chunks found in glb file.'); return; } if (chunks[0].type !== 0x4E4F534A) { callback(`Invalid chunk type found in glb file. Expected 0x4E4F534A, found 0x${chunks[0].type.toString(16)}`); return; } if (chunks.length > 1 && chunks[1].type !== 0x004E4942) { callback(`Invalid chunk type found in glb file. Expected 0x004E4942, found 0x${chunks[1].type.toString(16)}`); return; } callback(null, { gltfChunk: chunks[0].data, binaryChunk: chunks.length === 2 ? chunks[1].data : null }); }; const parseChunk = (filename, data, callback)=>{ const hasGlbHeader = ()=>{ const u8 = new Uint8Array(data); return u8[0] === 103 && u8[1] === 108 && u8[2] === 84 && u8[3] === 70; }; if (filename && filename.toLowerCase().endsWith('.glb') || hasGlbHeader()) { parseGlb(data, callback); } else { callback(null, { gltfChunk: data, binaryChunk: null }); } }; const createBufferViews = (gltf, buffers, options)=>{ const result = []; const preprocess = options?.bufferView?.preprocess; const processAsync = options?.bufferView?.processAsync; const postprocess = options?.bufferView?.postprocess; if (!gltf.bufferViews?.length) { return result; } for(let i = 0; i < gltf.bufferViews.length; ++i){ const gltfBufferView = gltf.bufferViews[i]; if (preprocess) { preprocess(gltfBufferView); } let promise; if (processAsync) { promise = new Promise((resolve, reject)=>{ processAsync(gltfBufferView, buffers, (err, result)=>{ if (err) { reject(err); } else { resolve(result); } }); }); } else { promise = new Promise((resolve)=>{ resolve(null); }); } promise = promise.then((buffer)=>{ if (buffer) { return buffer; } return buffers[gltfBufferView.buffer].then((buffer)=>{ return new Uint8Array(buffer.buffer, buffer.byteOffset + (gltfBufferView.byteOffset || 0), gltfBufferView.byteLength); }); }); if (gltfBufferView.hasOwnProperty('byteStride')) { promise = promise.then((typedArray)=>{ typedArray.byteStride = gltfBufferView.byteStride; return typedArray; }); } if (postprocess) { promise = promise.then((typedArray)=>{ postprocess(gltfBufferView, typedArray); return typedArray; }); } result.push(promise); } return result; }; class GlbParser { static parse(filename, urlBase, data, device, registry, options, callback) { parseChunk(filename, data, (err, chunks)=>{ if (err) { callback(err); return; } parseGltf(chunks.gltfChunk, (err, gltf)=>{ if (err) { callback(err); return; } const buffers = loadBuffers(gltf, chunks.binaryChunk, urlBase, options); const bufferViews = createBufferViews(gltf, buffers, options); const images = createImages(gltf, bufferViews, urlBase, registry, options); const textures = createTextures(gltf, images, options); createResources(device, gltf, bufferViews, textures, options).then((result)=>callback(null, result)).catch((err)=>callback(err)); }); }); } static createDefaultMaterial() { return createMaterial({ name: 'defaultGlbMaterial' }, []); } } class AnimationHandler extends ResourceHandler { load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; if (url.load.startsWith('blob:') || url.load.startsWith('data:')) { if (path.getExtension(url.original).toLowerCase() === '.glb') { options.responseType = Http.ResponseType.ARRAY_BUFFER; } else { options.responseType = Http.ResponseType.JSON; } } http.get(url.load, options, (err, response)=>{ if (err) { callback(`Error loading animation resource: ${url.original} [${err}]`); } else { if (path.getExtension(url.original).toLowerCase() === '.glb') { GlbParser.parse('filename.glb', '', response, this.device, this.assets, asset?.options ?? {}, (err, parseResult)=>{ if (err) { callback(err); } else { const animations = parseResult.animations; if (asset?.data?.events) { for(let i = 0; i < animations.length; i++){ animations[i].events = new AnimEvents(Object.values(asset.data.events)); } } parseResult.destroy(); callback(null, animations); } }); } else { callback(null, this[`_parseAnimationV${response.animation.version}`](response)); } } }); } open(url, data, asset) { return data; } _parseAnimationV3(data) { const animData = data.animation; const anim = new Animation(); anim.name = animData.name; anim.duration = animData.duration; for(let i = 0; i < animData.nodes.length; i++){ const node = new AnimationNode(); const n = animData.nodes[i]; node._name = n.name; for(let j = 0; j < n.keys.length; j++){ const k = n.keys[j]; const t = k.time; const p = k.pos; const r = k.rot; const s = k.scale; const pos = new Vec3(p[0], p[1], p[2]); const rot = new Quat().setFromEulerAngles(r[0], r[1], r[2]); const scl = new Vec3(s[0], s[1], s[2]); const key = new AnimationKey(t, pos, rot, scl); node._keys.push(key); } anim.addNode(node); } return anim; } _parseAnimationV4(data) { const animData = data.animation; const anim = new Animation(); anim.name = animData.name; anim.duration = animData.duration; for(let i = 0; i < animData.nodes.length; i++){ const node = new AnimationNode(); const n = animData.nodes[i]; node._name = n.name; const defPos = n.defaults.p; const defRot = n.defaults.r; const defScl = n.defaults.s; for(let j = 0; j < n.keys.length; j++){ const k = n.keys[j]; const t = k.t; const p = defPos ? defPos : k.p; const r = defRot ? defRot : k.r; const s = defScl ? defScl : k.s; const pos = new Vec3(p[0], p[1], p[2]); const rot = new Quat().setFromEulerAngles(r[0], r[1], r[2]); const scl = new Vec3(s[0], s[1], s[2]); const key = new AnimationKey(t, pos, rot, scl); node._keys.push(key); } anim.addNode(node); } return anim; } constructor(app){ super(app, 'animation'); this.device = app.graphicsDevice; this.assets = app.assets; } } class AnimClipHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; if (url.load.startsWith('blob:')) { options.responseType = Http.ResponseType.JSON; } http.get(url.load, options, (err, response)=>{ if (err) { callback(`Error loading animation clip resource: ${url.original} [${err}]`); } else { callback(null, response); } }); } open(url, data) { const name = data.name; const duration = data.duration; const inputs = data.inputs.map((input)=>{ return new AnimData(1, input); }); const outputs = data.outputs.map((output)=>{ return new AnimData(output.components, output.data); }); const curves = data.curves.map((curve)=>{ return new AnimCurve([ curve.path ], curve.inputIndex, curve.outputIndex, curve.interpolation); }); return new AnimTrack(name, duration, inputs, outputs, curves); } constructor(app){ super(app, 'animclip'); } } class AnimStateGraphHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; if (url.load.startsWith('blob:')) { options.responseType = Http.ResponseType.JSON; } http.get(url.load, options, (err, response)=>{ if (err) { callback(`Error loading animation state graph resource: ${url.original} [${err}]`); } else { callback(null, response); } }); } open(url, data) { return new AnimStateGraph(data); } constructor(app){ super(app, 'animstategraph'); } } const ie = function() { if (typeof window === 'undefined') { return false; } const ua = window.navigator.userAgent; const msie = ua.indexOf('MSIE '); if (msie > 0) { return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); } const trident = ua.indexOf('Trident/'); if (trident > 0) { const rv = ua.indexOf('rv:'); return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); } return false; }(); const supportedExtensions = [ '.ogg', '.mp3', '.wav', '.mp4a', '.m4a', '.mp4', '.aac', '.opus' ]; class AudioHandler extends ResourceHandler { _isSupported(url) { const ext = path.getExtension(url); return supportedExtensions.indexOf(ext) > -1; } load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const success = function(resource) { callback(null, new Sound(resource)); }; const error = function(err) { let msg = `Error loading audio url: ${url.original}`; if (err) { msg += `: ${err.message || err}`; } console.warn(msg); callback(msg); }; if (this._createSound) { if (!this._isSupported(url.original)) { error(`Audio format for ${url.original} not supported`); return; } this._createSound(url.load, success, error); } else { error(null); } } _createSound(url, success, error) { if (hasAudioContext()) { const manager = this.manager; if (!manager.context) { error('Audio manager has no audio context'); return; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; if (url.startsWith('blob:') || url.startsWith('data:')) { options.responseType = Http.ResponseType.ARRAY_BUFFER; } http.get(url, options, (err, response)=>{ if (err) { error(err); return; } manager.context.decodeAudioData(response, success, error); }); } else { let audio = null; try { audio = new Audio(); } catch (e) { error('No support for Audio element'); return; } if (ie) { document.body.appendChild(audio); } const onReady = function() { audio.removeEventListener('canplaythrough', onReady); if (ie) { document.body.removeChild(audio); } success(audio); }; audio.onerror = function() { audio.onerror = null; if (ie) { document.body.removeChild(audio); } error(); }; audio.addEventListener('canplaythrough', onReady); audio.src = url; } } constructor(app){ super(app, 'audio'); this.manager = app.soundManager; } } class BinaryHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { responseType: Http.ResponseType.ARRAY_BUFFER, retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { callback(null, response); } else { callback(`Error loading binary resource: ${url.original} [${err}]`); } }); } openBinary(data) { return data.buffer; } constructor(app){ super(app, 'binary'); } } class GlbContainerResource { get model() { if (!this._model) { const model = GlbContainerResource.createModel(this.data, this._defaultMaterial); const modelAsset = GlbContainerResource.createAsset(this._assetName, 'model', model, 0); this._assets.add(modelAsset); this._model = modelAsset; } return this._model; } static createAsset(assetName, type, resource, index) { const subAsset = new Asset(`${assetName}/${type}/${index}`, type, { url: '' }); subAsset.resource = resource; subAsset.loaded = true; return subAsset; } instantiateModelEntity(options) { const entity = new Entity(undefined, this._assets._loader._app); entity.addComponent('model', Object.assign({ type: 'asset', asset: this.model }, options)); return entity; } instantiateRenderEntity(options) { const defaultMaterial = this._defaultMaterial; const skinnedMeshInstances = []; const createMeshInstance = function(root, entity, mesh, materials, meshDefaultMaterials, skins, gltfNode, nodeInstancingMap) { const materialIndex = meshDefaultMaterials[mesh.id]; const material = materialIndex === undefined ? defaultMaterial : materials[materialIndex]; const meshInstance = new MeshInstance(mesh, material); if (mesh.morph) { meshInstance.morphInstance = new MorphInstance(mesh.morph); } if (gltfNode.hasOwnProperty('skin')) { skinnedMeshInstances.push({ meshInstance: meshInstance, rootBone: root, entity: entity }); } const instData = nodeInstancingMap.get(gltfNode); if (instData) { const matrices = instData.matrices; const vbFormat = VertexFormat.getDefaultInstancingFormat(mesh.device); const vb = new VertexBuffer(mesh.device, vbFormat, matrices.length / 16, { data: matrices }); meshInstance.setInstancing(vb); meshInstance.instancingData._destroyVertexBuffer = true; } return meshInstance; }; const cloneHierarchy = (root, node, glb)=>{ const entity = new Entity(undefined, this._assets._loader._app); node._cloneInternal(entity); if (!root) root = entity; let attachedMi = null; let renderAsset = null; for(let i = 0; i < glb.nodes.length; i++){ const glbNode = glb.nodes[i]; if (glbNode === node) { const gltfNode = glb.gltf.nodes[i]; if (gltfNode.hasOwnProperty('mesh')) { const meshGroup = glb.renders[gltfNode.mesh].meshes; renderAsset = this.renders[gltfNode.mesh]; for(let mi = 0; mi < meshGroup.length; mi++){ const mesh = meshGroup[mi]; if (mesh) { const cloneMi = createMeshInstance(root, entity, mesh, glb.materials, glb.meshDefaultMaterials, glb.skins, gltfNode, glb.nodeInstancingMap); if (!attachedMi) { attachedMi = []; } attachedMi.push(cloneMi); } } } if (glb.lights) { const lightEntity = glb.lights.get(gltfNode); if (lightEntity) { entity.addChild(lightEntity.clone()); } } if (glb.cameras) { const cameraEntity = glb.cameras.get(gltfNode); if (cameraEntity) { cameraEntity.camera.system.cloneComponent(cameraEntity, entity); } } } } if (attachedMi) { entity.addComponent('render', Object.assign({ type: 'asset', meshInstances: attachedMi }, options)); entity.render.assignAsset(renderAsset); } const children = node.children; for(let i = 0; i < children.length; i++){ const childClone = cloneHierarchy(root, children[i], glb); entity.addChild(childClone); } return entity; }; const sceneClones = []; for (const scene of this.data.scenes){ sceneClones.push(cloneHierarchy(null, scene, this.data)); } skinnedMeshInstances.forEach((data)=>{ data.meshInstance.skinInstance = SkinInstanceCache.createCachedSkinInstance(data.meshInstance.mesh.skin, data.rootBone, data.entity); data.meshInstance.node.render.rootBone = data.rootBone; }); return GlbContainerResource.createSceneHierarchy(sceneClones, Entity); } getMaterialVariants() { return this.data.variants ? Object.keys(this.data.variants) : []; } applyMaterialVariant(entity, name) { const variant = name ? this.data.variants[name] : null; if (variant === undefined) { return; } const renders = entity.findComponents('render'); for(let i = 0; i < renders.length; i++){ const renderComponent = renders[i]; this._applyMaterialVariant(variant, renderComponent.meshInstances); } } applyMaterialVariantInstances(instances, name) { const variant = name ? this.data.variants[name] : null; if (variant === undefined) { return; } this._applyMaterialVariant(variant, instances); } _applyMaterialVariant(variant, instances) { instances.forEach((instance)=>{ if (variant === null) { instance.material = this._defaultMaterial; } else { const meshVariants = this.data.meshVariants[instance.mesh.id]; if (meshVariants) { instance.material = this.data.materials[meshVariants[variant]]; } } }); } static createSceneHierarchy(sceneNodes, nodeType) { let root = null; if (sceneNodes.length === 1) { root = sceneNodes[0]; } else { root = new nodeType('SceneGroup'); for (const scene of sceneNodes){ root.addChild(scene); } } return root; } static createModel(glb, defaultMaterial) { const createMeshInstance = function(model, mesh, skins, skinInstances, materials, node, gltfNode) { const materialIndex = glb.meshDefaultMaterials[mesh.id]; const material = materialIndex === undefined ? defaultMaterial : materials[materialIndex]; const meshInstance = new MeshInstance(mesh, material, node); if (mesh.morph) { const morphInstance = new MorphInstance(mesh.morph); meshInstance.morphInstance = morphInstance; model.morphInstances.push(morphInstance); } if (gltfNode.hasOwnProperty('skin')) { const skinIndex = gltfNode.skin; const skin = skins[skinIndex]; mesh.skin = skin; const skinInstance = skinInstances[skinIndex]; meshInstance.skinInstance = skinInstance; model.skinInstances.push(skinInstance); } model.meshInstances.push(meshInstance); }; const model = new Model(); const skinInstances = []; for (const skin of glb.skins){ const skinInstance = new SkinInstance(skin); skinInstance.bones = skin.bones; skinInstances.push(skinInstance); } model.graph = GlbContainerResource.createSceneHierarchy(glb.scenes, GraphNode); for(let i = 0; i < glb.nodes.length; i++){ const node = glb.nodes[i]; if (node.root === model.graph) { const gltfNode = glb.gltf.nodes[i]; if (gltfNode.hasOwnProperty('mesh')) { const meshGroup = glb.renders[gltfNode.mesh].meshes; for(let mi = 0; mi < meshGroup.length; mi++){ const mesh = meshGroup[mi]; if (mesh) { createMeshInstance(model, mesh, glb.skins, skinInstances, glb.materials, node, gltfNode); } } } } } return model; } destroy() { const registry = this._assets; const destroyAsset = function(asset) { registry.remove(asset); asset.unload(); }; const destroyAssets = function(assets) { assets.forEach((asset)=>{ destroyAsset(asset); }); }; if (this.animations) { destroyAssets(this.animations); this.animations = null; } if (this.textures) { destroyAssets(this.textures); this.textures = null; } if (this.materials) { destroyAssets(this.materials); this.materials = null; } if (this.renders) { destroyAssets(this.renders); this.renders = null; } if (this._model) { destroyAsset(this._model); this._model = null; } this.data = null; this.assets = null; } constructor(data, asset, assets, defaultMaterial){ const createAsset = function(type, resource, index) { const subAsset = GlbContainerResource.createAsset(asset.name, type, resource, index); assets.add(subAsset); return subAsset; }; const renders = []; for(let i = 0; i < data.renders.length; ++i){ renders.push(createAsset('render', data.renders[i], i)); } const materials = []; for(let i = 0; i < data.materials.length; ++i){ materials.push(createAsset('material', data.materials[i], i)); } const animations = []; for(let i = 0; i < data.animations.length; ++i){ animations.push(createAsset('animation', data.animations[i], i)); } this.data = data; this._model = null; this._assetName = asset.name; this._assets = assets; this._defaultMaterial = defaultMaterial; this.renders = renders; this.materials = materials; this.textures = data.textures; this.animations = animations; } } class GlbContainerParser { _getUrlWithoutParams(url) { return url.indexOf('?') >= 0 ? url.split('?')[0] : url; } load(url, callback, asset) { Asset.fetchArrayBuffer(url.load, (err, result)=>{ if (err) { callback(err); } else { GlbParser.parse(this._getUrlWithoutParams(url.original), path.extractPath(url.load), result, this._device, asset.registry, asset.options, (err, result)=>{ if (err) { callback(err); } else { callback(null, new GlbContainerResource(result, asset, this._assets, this._defaultMaterial)); } }); } }, asset, this.maxRetries); } open(url, data, asset) { return data; } patch(asset, assets) {} constructor(device, assets, maxRetries){ this._device = device; this._assets = assets; this._defaultMaterial = GlbParser.createDefaultMaterial(); this.maxRetries = maxRetries; } } class ContainerHandler extends ResourceHandler { set maxRetries(value) { this.glbContainerParser.maxRetries = value; for(const parser in this.parsers){ if (this.parsers.hasOwnProperty(parser)) { this.parsers[parser].maxRetries = value; } } } get maxRetries() { return this.glbContainerParser.maxRetries; } _getUrlWithoutParams(url) { return url.indexOf('?') >= 0 ? url.split('?')[0] : url; } _getParser(url) { const ext = url ? path.getExtension(this._getUrlWithoutParams(url)).toLowerCase().replace('.', '') : null; return this.parsers[ext] || this.glbContainerParser; } load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } this._getParser(url.original).load(url, callback, asset); } open(url, data, asset) { return this._getParser(url).open(url, data, asset); } constructor(app){ super(app, 'container'); this.glbContainerParser = new GlbContainerParser(app.graphicsDevice, app.assets, 0); this.parsers = {}; } } class CssHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { callback(null, response); } else { callback(`Error loading css resource: ${url.original} [${err}]`); } }); } openBinary(data) { this.decoder ?? (this.decoder = new TextDecoder('utf-8')); return this.decoder.decode(data); } constructor(app){ super(app, 'css'), this.decoder = null; } } class CubemapHandler extends ResourceHandler { load(url, callback, asset) { this.loadAssets(asset, callback); } open(url, data, asset) { return asset ? asset.resource : null; } patch(asset, registry) { this.loadAssets(asset, (err, result)=>{ if (err) { registry.fire('error', asset); registry.fire(`error:${asset.id}`, err, asset); asset.fire('error', asset); } }); } getAssetIds(cubemapAsset) { const result = []; result[0] = cubemapAsset.file; if ((cubemapAsset.loadFaces || !cubemapAsset.file) && cubemapAsset.data && cubemapAsset.data.textures) { for(let i = 0; i < 6; ++i){ result[i + 1] = cubemapAsset.data.textures[i]; } } else { result[1] = result[2] = result[3] = result[4] = result[5] = result[6] = null; } return result; } compareAssetIds(assetIdA, assetIdB) { if (assetIdA && assetIdB) { if (parseInt(assetIdA, 10) === assetIdA || typeof assetIdA === 'string') { return assetIdA === assetIdB; } return assetIdA.url === assetIdB.url; } return assetIdA !== null === (assetIdB !== null); } update(cubemapAsset, assetIds, assets) { const assetData = cubemapAsset.data || {}; const oldAssets = cubemapAsset._handlerState.assets; const oldResources = cubemapAsset._resources; let tex, mip, i; const resources = [ null, null, null, null, null, null, null ]; const getType = function() { if (assetData.hasOwnProperty('type')) { return assetData.type; } if (assetData.hasOwnProperty('rgbm')) { return assetData.rgbm ? TEXTURETYPE_RGBM : TEXTURETYPE_DEFAULT; } return null; }; if (!cubemapAsset.loaded || assets[0] !== oldAssets[0]) { if (assets[0]) { tex = assets[0].resource; if (tex.cubemap) { for(i = 0; i < 6; ++i){ resources[i + 1] = new Texture(this._device, { name: `${cubemapAsset.name}_prelitCubemap${tex.width >> i}`, cubemap: true, type: getType() || tex.type, width: tex.width >> i, height: tex.height >> i, format: tex.format, levels: [ tex._levels[i] ], addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, mipmaps: i === 0 }); } } else { resources[1] = tex; } } } else { resources[1] = oldResources[1] || null; resources[2] = oldResources[2] || null; resources[3] = oldResources[3] || null; resources[4] = oldResources[4] || null; resources[5] = oldResources[5] || null; resources[6] = oldResources[6] || null; } const faceAssets = assets.slice(1); if (!cubemapAsset.loaded || !this.cmpArrays(faceAssets, oldAssets.slice(1))) { if (faceAssets.indexOf(null) === -1) { const faceTextures = faceAssets.map((asset)=>{ return asset.resource; }); const faceLevels = []; for(mip = 0; mip < faceTextures[0]._levels.length; ++mip){ faceLevels.push(faceTextures.map((faceTexture)=>{ return faceTexture._levels[mip]; })); } const format = faceTextures[0].format; const faces = new Texture(this._device, { name: `${cubemapAsset.name}_faces`, cubemap: true, type: getType() || faceTextures[0].type, width: faceTextures[0].width, height: faceTextures[0].height, format: format === PIXELFORMAT_RGB8 ? PIXELFORMAT_RGBA8 : format, mipmaps: assetData.mipmaps ?? true, levels: faceLevels, minFilter: assetData.hasOwnProperty('minFilter') ? assetData.minFilter : faceTextures[0].minFilter, magFilter: assetData.hasOwnProperty('magFilter') ? assetData.magFilter : faceTextures[0].magFilter, anisotropy: assetData.hasOwnProperty('anisotropy') ? assetData.anisotropy : 1, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); resources[0] = faces; } } else { resources[0] = oldResources[0] || null; } if (!this.cmpArrays(resources, oldResources)) { cubemapAsset.resources = resources; cubemapAsset._handlerState.assetIds = assetIds; cubemapAsset._handlerState.assets = assets; for(i = 0; i < oldResources.length; ++i){ if (oldResources[i] !== null && resources.indexOf(oldResources[i]) === -1) { oldResources[i].destroy(); } } } for(i = 0; i < oldAssets.length; ++i){ if (oldAssets[i] !== null && assets.indexOf(oldAssets[i]) === -1) { oldAssets[i].unload(); } } } cmpArrays(arr1, arr2) { if (arr1.length !== arr2.length) { return false; } for(let i = 0; i < arr1.length; ++i){ if (arr1[i] !== arr2[i]) { return false; } } return true; } resolveId(value) { const valueInt = parseInt(value, 10); return valueInt === value || valueInt.toString() === value ? valueInt : value; } loadAssets(cubemapAsset, callback) { if (!cubemapAsset.hasOwnProperty('_handlerState')) { cubemapAsset._handlerState = { assetIds: [ null, null, null, null, null, null, null ], assets: [ null, null, null, null, null, null, null ] }; } const self = this; const assetIds = self.getAssetIds(cubemapAsset); const assets = [ null, null, null, null, null, null, null ]; const loadedAssetIds = cubemapAsset._handlerState.assetIds; const loadedAssets = cubemapAsset._handlerState.assets; const registry = self._registry; let awaiting = 7; const onLoad = function(index, asset) { assets[index] = asset; awaiting--; if (awaiting === 0) { self.update(cubemapAsset, assetIds, assets); callback(null, cubemapAsset.resources); } }; const onError = function(index, err, asset) { callback(err); }; const processTexAsset = function(index, texAsset) { if (texAsset.loaded) { onLoad(index, texAsset); } else { registry.once(`load:${texAsset.id}`, onLoad.bind(self, index)); registry.once(`error:${texAsset.id}`, onError.bind(self, index)); if (!texAsset.loading) { registry.load(texAsset); } } }; let texAsset; for(let i = 0; i < 7; ++i){ const assetId = this.resolveId(assetIds[i]); if (!assetId) { onLoad(i, null); } else if (self.compareAssetIds(assetId, loadedAssetIds[i])) { processTexAsset(i, loadedAssets[i]); } else if (parseInt(assetId, 10) === assetId) { texAsset = registry.get(assetId); if (texAsset) { processTexAsset(i, texAsset); } else { setTimeout(((index, assetId_)=>{ const texAsset = registry.get(assetId_); if (texAsset) { processTexAsset(index, texAsset); } else { onError(index, `failed to find dependent cubemap asset=${assetId_}`); } }).bind(null, i, assetId)); } } else { const file = typeof assetId === 'string' ? { url: assetId, filename: assetId } : assetId; const data = file.url.search('.dds') === -1 ? { type: 'rgbp', addressu: 'clamp', addressv: 'clamp', mipmaps: false } : null; texAsset = new Asset(`${cubemapAsset.name}_part_${i}`, 'texture', file, data); registry.add(texAsset); processTexAsset(i, texAsset); } } } constructor(app){ super(app, 'cubemap'); this._device = app.graphicsDevice; this._registry = app.assets; this._loader = app.loader; } } class FolderHandler extends ResourceHandler { load(url, callback) { callback(null, null); } constructor(app){ super(app, 'folder'); } } class Font { set data(value) { this._data = value; if (!value) { return; } if (this._data.intensity !== undefined) { this.intensity = this._data.intensity; } if (!this._data.info) { this._data.info = {}; } if (!this._data.version || this._data.version < 2) { this._data.info.maps = [ { width: this._data.info.width, height: this._data.info.height } ]; if (this._data.chars) { for(const key in this._data.chars){ this._data.chars[key].map = 0; } } } } get data() { return this._data; } constructor(textures, data){ this.type = data ? data.type || FONT_MSDF : FONT_MSDF; this.em = 1; this.textures = textures; this.intensity = 0.0; this._data = null; this.data = data; } } function upgradeDataSchema(data) { if (data.version < 3) { if (data.version < 2) { data.info.maps = data.info.maps || [ { width: data.info.width, height: data.info.height } ]; } data.chars = Object.keys(data.chars || {}).reduce((newChars, key)=>{ const existing = data.chars[key]; const newKey = existing.letter !== undefined ? existing.letter : string.fromCodePoint(key); if (data.version < 2) { existing.map = existing.map || 0; } newChars[newKey] = existing; return newChars; }, {}); data.version = 3; } return data; } class FontHandler extends ResourceHandler { load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } const self = this; if (path.getExtension(url.original) === '.json') { http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { const data = upgradeDataSchema(response); self._loadTextures(url.load.replace('.json', '.png'), data, (err, textures)=>{ if (err) { callback(err); } else { callback(null, { data: data, textures: textures }); } }); } else { callback(`Error loading font resource: ${url.original} [${err}]`); } }); } else { if (asset && asset.data) { asset.data = upgradeDataSchema(asset.data); } this._loadTextures(url.load, asset && asset.data, callback); } } _loadTextures(url, data, callback) { const numTextures = data.info.maps.length; let numLoaded = 0; let error = null; const textures = new Array(numTextures); const loader = this._loader; const loadTexture = function(index) { const onLoaded = function(err, texture) { if (error) return; if (err) { error = err; callback(err); return; } texture.upload(); textures[index] = texture; numLoaded++; if (numLoaded === numTextures) { callback(null, textures); } }; if (index === 0) { loader.load(url, 'texture', onLoaded); } else { loader.load(url.replace('.png', `${index}.png`), 'texture', onLoaded); } }; for(let i = 0; i < numTextures; i++){ loadTexture(i); } } open(url, data, asset) { let font; if (data.textures) { font = new Font(data.textures, data.data); } else { font = new Font(data, null); } return font; } patch(asset, assets) { const font = asset.resource; if (!font.data && asset.data) { font.data = asset.data; } else if (!asset.data && font.data) { asset.data = font.data; } if (asset.data) { asset.data = upgradeDataSchema(asset.data); } } constructor(app){ super(app, 'font'); this._loader = app.loader; this.maxRetries = 0; } } const SH_C0 = 0.28209479177387814; class SplatCompressedIterator { constructor(gsplatData, p, r, s, c, sh){ const unpackUnorm = (value, bits)=>{ const t = (1 << bits) - 1; return (value & t) / t; }; const unpack111011 = (result, value)=>{ result.x = unpackUnorm(value >>> 21, 11); result.y = unpackUnorm(value >>> 11, 10); result.z = unpackUnorm(value, 11); }; const unpack8888 = (result, value)=>{ result.x = unpackUnorm(value >>> 24, 8); result.y = unpackUnorm(value >>> 16, 8); result.z = unpackUnorm(value >>> 8, 8); result.w = unpackUnorm(value, 8); }; const unpackRot = (result, value)=>{ const norm = Math.SQRT2; const a = (unpackUnorm(value >>> 20, 10) - 0.5) * norm; const b = (unpackUnorm(value >>> 10, 10) - 0.5) * norm; const c = (unpackUnorm(value, 10) - 0.5) * norm; const m = Math.sqrt(1.0 - (a * a + b * b + c * c)); switch(value >>> 30){ case 0: result.set(a, b, c, m); break; case 1: result.set(m, b, c, a); break; case 2: result.set(b, m, c, a); break; case 3: result.set(b, c, m, a); break; } }; const lerp = (a, b, t)=>a * (1 - t) + b * t; const { chunkData, chunkSize, vertexData, shData0, shData1, shData2, shBands } = gsplatData; const shCoeffs = [ 3, 8, 15 ][shBands - 1]; this.read = (i)=>{ const ci = Math.floor(i / 256) * chunkSize; if (p) { unpack111011(p, vertexData[i * 4 + 0]); p.x = lerp(chunkData[ci + 0], chunkData[ci + 3], p.x); p.y = lerp(chunkData[ci + 1], chunkData[ci + 4], p.y); p.z = lerp(chunkData[ci + 2], chunkData[ci + 5], p.z); } if (r) { unpackRot(r, vertexData[i * 4 + 1]); } if (s) { unpack111011(s, vertexData[i * 4 + 2]); s.x = lerp(chunkData[ci + 6], chunkData[ci + 9], s.x); s.y = lerp(chunkData[ci + 7], chunkData[ci + 10], s.y); s.z = lerp(chunkData[ci + 8], chunkData[ci + 11], s.z); } if (c) { unpack8888(c, vertexData[i * 4 + 3]); if (chunkSize > 12) { c.x = lerp(chunkData[ci + 12], chunkData[ci + 15], c.x); c.y = lerp(chunkData[ci + 13], chunkData[ci + 16], c.y); c.z = lerp(chunkData[ci + 14], chunkData[ci + 17], c.z); } } if (sh && shBands > 0) { const shData = [ shData0, shData1, shData2 ]; for(let j = 0; j < 3; ++j){ for(let k = 0; k < 15; ++k){ sh[j * 15 + k] = k < shCoeffs ? shData[j][i * 16 + k] * (8 / 255) - 4 : 0; } } } }; } } class GSplatCompressedData { createIter(p, r, s, c, sh) { return new SplatCompressedIterator(this, p, r, s, c, sh); } calcAabb(result) { const { chunkData, numChunks, chunkSize } = this; let s = Math.exp(Math.max(chunkData[9], chunkData[10], chunkData[11])); let mx = chunkData[0] - s; let my = chunkData[1] - s; let mz = chunkData[2] - s; let Mx = chunkData[3] + s; let My = chunkData[4] + s; let Mz = chunkData[5] + s; for(let i = 1; i < numChunks; ++i){ const off = i * chunkSize; s = Math.exp(Math.max(chunkData[off + 9], chunkData[off + 10], chunkData[off + 11])); mx = Math.min(mx, chunkData[off + 0] - s); my = Math.min(my, chunkData[off + 1] - s); mz = Math.min(mz, chunkData[off + 2] - s); Mx = Math.max(Mx, chunkData[off + 3] + s); My = Math.max(My, chunkData[off + 4] + s); Mz = Math.max(Mz, chunkData[off + 5] + s); } result.center.set((mx + Mx) * 0.5, (my + My) * 0.5, (mz + Mz) * 0.5); result.halfExtents.set((Mx - mx) * 0.5, (My - my) * 0.5, (Mz - mz) * 0.5); return true; } getCenters() { const { vertexData, chunkData, numChunks, chunkSize } = this; const result = new Float32Array(this.numSplats * 3); let mx, my, mz, Mx, My, Mz; for(let c = 0; c < numChunks; ++c){ const off = c * chunkSize; mx = chunkData[off + 0]; my = chunkData[off + 1]; mz = chunkData[off + 2]; Mx = chunkData[off + 3]; My = chunkData[off + 4]; Mz = chunkData[off + 5]; const end = Math.min(this.numSplats, (c + 1) * 256); for(let i = c * 256; i < end; ++i){ const p = vertexData[i * 4]; const px = (p >>> 21) / 2047; const py = (p >>> 11 & 0x3ff) / 1023; const pz = (p & 0x7ff) / 2047; result[i * 3 + 0] = (1 - px) * mx + px * Mx; result[i * 3 + 1] = (1 - py) * my + py * My; result[i * 3 + 2] = (1 - pz) * mz + pz * Mz; } } return result; } getChunks(result) { const { chunkData, numChunks, chunkSize } = this; let mx, my, mz, Mx, My, Mz; for(let c = 0; c < numChunks; ++c){ const off = c * chunkSize; mx = chunkData[off + 0]; my = chunkData[off + 1]; mz = chunkData[off + 2]; Mx = chunkData[off + 3]; My = chunkData[off + 4]; Mz = chunkData[off + 5]; result[c * 6 + 0] = mx; result[c * 6 + 1] = my; result[c * 6 + 2] = mz; result[c * 6 + 3] = Mx; result[c * 6 + 4] = My; result[c * 6 + 5] = Mz; } } calcFocalPoint(result) { const { chunkData, numChunks, chunkSize } = this; result.x = 0; result.y = 0; result.z = 0; for(let i = 0; i < numChunks; ++i){ const off = i * chunkSize; result.x += chunkData[off + 0] + chunkData[off + 3]; result.y += chunkData[off + 1] + chunkData[off + 4]; result.z += chunkData[off + 2] + chunkData[off + 5]; } result.mulScalar(0.5 / numChunks); } get isCompressed() { return true; } get numChunks() { return Math.ceil(this.numSplats / 256); } get chunkSize() { return this.chunkData.length / this.numChunks; } decompress() { const members = [ 'x', 'y', 'z', 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3' ]; const { shBands } = this; if (shBands > 0) { const shMembers = []; for(let i = 0; i < 45; ++i){ shMembers.push(`f_rest_${i}`); } const location = Math.max(...[ 'f_dc_0', 'f_dc_1', 'f_dc_2' ].map((name)=>members.indexOf(name))); members.splice(location + 1, 0, ...shMembers); } const data = {}; members.forEach((name)=>{ data[name] = new Float32Array(this.numSplats); }); const p = new Vec3(); const r = new Quat(); const s = new Vec3(); const c = new Vec4(); const sh = shBands > 0 ? new Float32Array(45) : null; const iter = this.createIter(p, r, s, c, sh); for(let i = 0; i < this.numSplats; ++i){ iter.read(i); data.x[i] = p.x; data.y[i] = p.y; data.z[i] = p.z; data.rot_1[i] = r.x; data.rot_2[i] = r.y; data.rot_3[i] = r.z; data.rot_0[i] = r.w; data.scale_0[i] = s.x; data.scale_1[i] = s.y; data.scale_2[i] = s.z; data.f_dc_0[i] = (c.x - 0.5) / SH_C0; data.f_dc_1[i] = (c.y - 0.5) / SH_C0; data.f_dc_2[i] = (c.z - 0.5) / SH_C0; data.opacity[i] = c.w <= 0 ? -40 : c.w >= 1 ? 40 : -Math.log(1 / c.w - 1); if (sh) { for(let c = 0; c < 45; ++c){ data[`f_rest_${c}`][i] = sh[c]; } } } return new GSplatData([ { name: 'vertex', count: this.numSplats, properties: members.map((name)=>{ return { name: name, type: 'float', byteSize: 4, storage: data[name] }; }) } ], this.comments); } } const strideCopy = (target, targetStride, src, srcStride, numEntries)=>{ for(let i = 0; i < numEntries; ++i){ for(let j = 0; j < srcStride; ++j){ target[i * targetStride + j] = src[i * srcStride + j]; } } }; class GSplatCompressedResource extends GSplatResourceBase { destroy() { this.packedTexture?.destroy(); this.chunkTexture?.destroy(); this.shTexture0?.destroy(); this.shTexture1?.destroy(); this.shTexture2?.destroy(); super.destroy(); } configureMaterialDefines(defines) { defines.set('GSPLAT_COMPRESSED_DATA', true); defines.set('SH_BANDS', this.shTexture0 ? 3 : 0); } configureMaterial(material) { this.configureMaterialDefines(material.defines); material.setParameter('packedTexture', this.packedTexture); material.setParameter('chunkTexture', this.chunkTexture); if (this.shTexture0) { material.setParameter('shTexture0', this.shTexture0); material.setParameter('shTexture1', this.shTexture1); material.setParameter('shTexture2', this.shTexture2); } } evalTextureSize(count) { const width = Math.ceil(Math.sqrt(count)); const height = Math.ceil(count / width); return new Vec2(width, height); } constructor(device, gsplatData){ super(device, gsplatData); const { chunkData, chunkSize, numChunks, numSplats, vertexData, shBands } = gsplatData; this.chunks = new Float32Array(numChunks * 6); gsplatData.getChunks(this.chunks); this.packedTexture = this.createTexture('packedData', PIXELFORMAT_RGBA32U, this.evalTextureSize(numSplats), vertexData); const chunkTextureSize = this.evalTextureSize(numChunks); chunkTextureSize.x *= 5; this.chunkTexture = this.createTexture('chunkData', PIXELFORMAT_RGBA32F, chunkTextureSize); const chunkTextureData = this.chunkTexture.lock(); strideCopy(chunkTextureData, 20, chunkData, chunkSize, numChunks); if (chunkSize === 12) { for(let i = 0; i < numChunks; ++i){ chunkTextureData[i * 20 + 15] = 1; chunkTextureData[i * 20 + 16] = 1; chunkTextureData[i * 20 + 17] = 1; } } this.chunkTexture.unlock(); if (shBands > 0) { const size = this.evalTextureSize(numSplats); this.shTexture0 = this.createTexture('shTexture0', PIXELFORMAT_RGBA32U, size, new Uint32Array(gsplatData.shData0.buffer)); this.shTexture1 = this.createTexture('shTexture1', PIXELFORMAT_RGBA32U, size, new Uint32Array(gsplatData.shData1.buffer)); this.shTexture2 = this.createTexture('shTexture2', PIXELFORMAT_RGBA32U, size, new Uint32Array(gsplatData.shData2.buffer)); } else { this.shTexture0 = null; this.shTexture1 = null; this.shTexture2 = null; } } } const magicBytes = new Uint8Array([ 112, 108, 121, 10 ]); const endHeaderBytes = new Uint8Array([ 10, 101, 110, 100, 95, 104, 101, 97, 100, 101, 114, 10 ]); const dataTypeMap = new Map([ [ 'char', Int8Array ], [ 'uchar', Uint8Array ], [ 'short', Int16Array ], [ 'ushort', Uint16Array ], [ 'int', Int32Array ], [ 'uint', Uint32Array ], [ 'float', Float32Array ], [ 'double', Float64Array ] ]); class StreamBuf { async read() { const { value, done } = await this.reader.read(); if (done) { throw new Error('Stream finished before end of header'); } this.push(value); this.progressFunc?.(value.byteLength); } push(data) { if (!this.data) { this.data = data; this.view = new DataView(this.data.buffer); this.tail = data.length; } else { const remaining = this.tail - this.head; const newSize = remaining + data.length; if (this.data.length >= newSize) { if (this.head > 0) { this.data.copyWithin(0, this.head, this.tail); this.data.set(data, remaining); this.head = 0; this.tail = newSize; } else { this.data.set(data, this.tail); this.tail += data.length; } } else { const tmp = new Uint8Array(newSize); if (this.head > 0 || this.tail < this.data.length) { tmp.set(this.data.subarray(this.head, this.tail), 0); } else { tmp.set(this.data, 0); } tmp.set(data, remaining); this.data = tmp; this.view = new DataView(this.data.buffer); this.head = 0; this.tail = newSize; } } } compact() { if (this.head > 0) { this.data.copyWithin(0, this.head, this.tail); this.tail -= this.head; this.head = 0; } } get remaining() { return this.tail - this.head; } getInt8() { const result = this.view.getInt8(this.head); this.head++; return result; } getUint8() { const result = this.view.getUint8(this.head); this.head++; return result; } getInt16() { const result = this.view.getInt16(this.head, true); this.head += 2; return result; } getUint16() { const result = this.view.getUint16(this.head, true); this.head += 2; return result; } getInt32() { const result = this.view.getInt32(this.head, true); this.head += 4; return result; } getUint32() { const result = this.view.getUint32(this.head, true); this.head += 4; return result; } getFloat32() { const result = this.view.getFloat32(this.head, true); this.head += 4; return result; } getFloat64() { const result = this.view.getFloat64(this.head, true); this.head += 8; return result; } constructor(reader, progressFunc){ this.head = 0; this.tail = 0; this.reader = reader; this.progressFunc = progressFunc; } } const parseHeader = (lines)=>{ const elements = []; const comments = []; let format; for(let i = 1; i < lines.length; ++i){ const words = lines[i].split(' '); switch(words[0]){ case 'comment': comments.push(words.slice(1).join(' ')); break; case 'format': format = words[1]; break; case 'element': elements.push({ name: words[1], count: parseInt(words[2], 10), properties: [] }); break; case 'property': { if (!dataTypeMap.has(words[1])) { throw new Error(`Unrecognized property data type '${words[1]}' in ply header`); } const element = elements[elements.length - 1]; element.properties.push({ type: words[1], name: words[2], storage: null, byteSize: dataTypeMap.get(words[1]).BYTES_PER_ELEMENT }); break; } default: throw new Error(`Unrecognized header value '${words[0]}' in ply header`); } } return { elements, format, comments }; }; const isCompressedPly = (elements)=>{ const chunkProperties = [ 'min_x', 'min_y', 'min_z', 'max_x', 'max_y', 'max_z', 'min_scale_x', 'min_scale_y', 'min_scale_z', 'max_scale_x', 'max_scale_y', 'max_scale_z', 'min_r', 'min_g', 'min_b', 'max_r', 'max_g', 'max_b' ]; const vertexProperties = [ 'packed_position', 'packed_rotation', 'packed_scale', 'packed_color' ]; const shProperties = new Array(45).fill('').map((_, i)=>`f_rest_${i}`); const hasBaseElements = ()=>{ return elements[0].name === 'chunk' && elements[0].properties.every((p, i)=>p.name === chunkProperties[i] && p.type === 'float') && elements[1].name === 'vertex' && elements[1].properties.every((p, i)=>p.name === vertexProperties[i] && p.type === 'uint'); }; const hasSHElements = ()=>{ return elements[2].name === 'sh' && [ 9, 24, 45 ].indexOf(elements[2].properties.length) !== -1 && elements[2].properties.every((p, i)=>p.name === shProperties[i] && p.type === 'uchar'); }; return elements.length === 2 && hasBaseElements() || elements.length === 3 && hasBaseElements() && hasSHElements(); }; const isFloatPly = (elements)=>{ return elements.length === 1 && elements[0].name === 'vertex' && elements[0].properties.every((p)=>p.type === 'float'); }; const readCompressedPly = async (streamBuf, elements, comments)=>{ const result = new GSplatCompressedData(); result.comments = comments; const numChunks = elements[0].count; const numChunkProperties = elements[0].properties.length; const numVertices = elements[1].count; const evalStorageSize = (count)=>{ const width = Math.ceil(Math.sqrt(count)); const height = Math.ceil(count / width); return width * height; }; const storageSize = evalStorageSize(numVertices); result.numSplats = numVertices; result.chunkData = new Float32Array(numChunks * numChunkProperties); result.vertexData = new Uint32Array(storageSize * 4); const read = async (buffer, length)=>{ const target = new Uint8Array(buffer); let cursor = 0; while(cursor < length){ while(streamBuf.remaining === 0){ await streamBuf.read(); } const toCopy = Math.min(length - cursor, streamBuf.remaining); const src = streamBuf.data; for(let i = 0; i < toCopy; ++i){ target[cursor++] = src[streamBuf.head++]; } } }; await read(result.chunkData.buffer, numChunks * numChunkProperties * 4); await read(result.vertexData.buffer, numVertices * 4 * 4); if (elements.length === 3) { const texStorageSize = storageSize * 16; const shData0 = new Uint8Array(texStorageSize); const shData1 = new Uint8Array(texStorageSize); const shData2 = new Uint8Array(texStorageSize); const chunkSize = 1024; const srcCoeffs = elements[2].properties.length / 3; const tmpBuf = new Uint8Array(chunkSize * srcCoeffs * 3); for(let i = 0; i < result.numSplats; i += chunkSize){ const toRead = Math.min(chunkSize, result.numSplats - i); await read(tmpBuf.buffer, toRead * srcCoeffs * 3); for(let j = 0; j < toRead; ++j){ for(let k = 0; k < 15; ++k){ const tidx = (i + j) * 16 + k; if (k < srcCoeffs) { shData0[tidx] = tmpBuf[(j * 3 + 0) * srcCoeffs + k]; shData1[tidx] = tmpBuf[(j * 3 + 1) * srcCoeffs + k]; shData2[tidx] = tmpBuf[(j * 3 + 2) * srcCoeffs + k]; } else { shData0[tidx] = 127; shData1[tidx] = 127; shData2[tidx] = 127; } } } } result.shData0 = shData0; result.shData1 = shData1; result.shData2 = shData2; result.shBands = ({ 3: 1, 8: 2, 15: 3 })[srcCoeffs]; } else { result.shBands = 0; } return result; }; const readFloatPly = async (streamBuf, elements, comments)=>{ const element = elements[0]; const properties = element.properties; const numProperties = properties.length; const storage = properties.map((p)=>p.storage); const inputSize = properties.reduce((a, p)=>a + p.byteSize, 0); let vertexIdx = 0; let floatData; const checkFloatData = ()=>{ const buffer = streamBuf.data.buffer; if (floatData?.buffer !== buffer) { floatData = new Float32Array(buffer, 0, buffer.byteLength / 4); } }; checkFloatData(); while(vertexIdx < element.count){ while(streamBuf.remaining < inputSize){ await streamBuf.read(); checkFloatData(); } const toRead = Math.min(element.count - vertexIdx, Math.floor(streamBuf.remaining / inputSize)); for(let j = 0; j < numProperties; ++j){ const s = storage[j]; for(let n = 0; n < toRead; ++n){ s[n + vertexIdx] = floatData[n * numProperties + j]; } } vertexIdx += toRead; streamBuf.head += toRead * inputSize; } return new GSplatData(elements, comments); }; const readGeneralPly = async (streamBuf, elements, comments)=>{ for(let i = 0; i < elements.length; ++i){ const element = elements[i]; const inputSize = element.properties.reduce((a, p)=>a + p.byteSize, 0); const propertyParsingFunctions = element.properties.map((p)=>{ if (p.storage) { switch(p.type){ case 'char': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getInt8(); }; case 'uchar': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getUint8(); }; case 'short': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getInt16(); }; case 'ushort': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getUint16(); }; case 'int': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getInt32(); }; case 'uint': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getUint32(); }; case 'float': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getFloat32(); }; case 'double': return (streamBuf, c)=>{ p.storage[c] = streamBuf.getFloat64(); }; default: throw new Error(`Unsupported property data type '${p.type}' in ply header`); } } else { return (streamBuf)=>{ streamBuf.head += p.byteSize; }; } }); let c = 0; while(c < element.count){ while(streamBuf.remaining < inputSize){ await streamBuf.read(); } const toRead = Math.min(element.count - c, Math.floor(streamBuf.remaining / inputSize)); for(let n = 0; n < toRead; ++n){ for(let j = 0; j < element.properties.length; ++j){ propertyParsingFunctions[j](streamBuf, c); } c++; } } } return new GSplatData(elements, comments); }; const readPly = async (reader, propertyFilter = null, progressFunc = null)=>{ const find = (buf, search)=>{ const endIndex = buf.length - search.length; let i, j; for(i = 0; i <= endIndex; ++i){ for(j = 0; j < search.length; ++j){ if (buf[i + j] !== search[j]) { break; } } if (j === search.length) { return i; } } return -1; }; const startsWith = (a, b)=>{ if (a.length < b.length) { return false; } for(let i = 0; i < b.length; ++i){ if (a[i] !== b[i]) { return false; } } return true; }; const streamBuf = new StreamBuf(reader, progressFunc); let headerLength; while(true){ await streamBuf.read(); if (streamBuf.tail >= magicBytes.length && !startsWith(streamBuf.data, magicBytes)) { throw new Error('Invalid ply header'); } headerLength = find(streamBuf.data, endHeaderBytes); if (headerLength !== -1) { break; } } const lines = new TextDecoder('ascii').decode(streamBuf.data.subarray(0, headerLength)).split('\n'); const { elements, format, comments } = parseHeader(lines); if (format !== 'binary_little_endian') { throw new Error('Unsupported ply format'); } streamBuf.head = headerLength + endHeaderBytes.length; streamBuf.compact(); const readData = async ()=>{ if (isCompressedPly(elements)) { return await readCompressedPly(streamBuf, elements, comments); } elements.forEach((e)=>{ e.properties.forEach((p)=>{ const storageType = dataTypeMap.get(p.type); if (storageType) { const storage = !propertyFilter || propertyFilter(p.name) ? new storageType(e.count) : null; p.storage = storage; } }); }); if (isFloatPly(elements)) { return await readFloatPly(streamBuf, elements, comments); } return await readGeneralPly(streamBuf, elements, comments); }; return await readData(); }; const defaultElementFilter = (val)=>true; class PlyParser { async load(url, callback, asset) { try { const response = await (asset.file?.contents ?? fetch(url.load)); if (!response || !response.body) { callback('Error loading resource', null); } else { const totalLength = parseInt(response.headers.get('content-length') ?? '0', 10); let totalReceived = 0; const data = await readPly(response.body.getReader(), asset.data.elementFilter ?? defaultElementFilter, (bytes)=>{ totalReceived += bytes; if (asset) { asset.fire('progress', totalReceived, totalLength); } }); asset.fire('load:data', data); if (!data.isCompressed) { if (asset.data.reorder ?? true) { data.reorderData(); } } const resource = data.isCompressed && !asset.data.decompress ? new GSplatCompressedResource(this.app.graphicsDevice, data) : new GSplatResource(this.app.graphicsDevice, data.isCompressed ? data.decompress() : data); callback(null, resource); } } catch (err) { callback(err, null); } } open(url, data) { return data; } constructor(app, maxRetries){ this.app = app; this.maxRetries = maxRetries; } } const combineProgress = (target, assets)=>{ const map = new Map(); const fire = ()=>{ let loaded = 0; let total = 0; map.forEach((value)=>{ loaded += value.loaded; total += value.total; }); target.fire('progress', loaded, total); }; assets.forEach((asset)=>{ const progress = (loaded, total)=>{ map.set(asset, { loaded, total }); fire(); }; const done = ()=>{ asset.off('progress', progress); asset.off('load', done); asset.off('error', done); }; asset.on('progress', progress); asset.on('load', done); asset.on('error', done); }); }; const upgradeMeta = (meta)=>{ const result = { version: 1, count: meta.means.shape[0], means: { mins: meta.means.mins, maxs: meta.means.maxs, files: meta.means.files }, scales: { mins: meta.scales.mins, maxs: meta.scales.maxs, files: meta.scales.files }, quats: { files: meta.quats.files }, sh0: { mins: meta.sh0.mins, maxs: meta.sh0.maxs, files: meta.sh0.files } }; if (meta.shN) { result.shN = { mins: meta.shN.mins, maxs: meta.shN.maxs, files: meta.shN.files }; } return result; }; class SogsParser { _shouldAbort(asset, unloaded) { if (unloaded || !this.app.assets.get(asset.id)) return true; if (!this.app?.graphicsDevice || this.app.graphicsDevice._destroyed) return true; return false; } async loadTextures(url, callback, asset, meta) { if (meta.version !== 2) { meta = upgradeMeta(meta); } const { assets } = this.app; const subs = [ 'means', 'quats', 'scales', 'sh0', 'shN' ]; const textures = {}; const promises = []; subs.forEach((sub)=>{ const files = meta[sub]?.files ?? []; textures[sub] = files.map((filename)=>{ const texture = new Asset(filename, 'texture', { url: asset.options?.mapUrl?.(filename) ?? new URL(filename, new URL(url.load, window.location.href).toString()).toString(), filename }, { mipmaps: false }, { crossOrigin: 'anonymous' }); const promise = new Promise((resolve, reject)=>{ texture.on('load', ()=>resolve(null)); texture.on('error', (err)=>reject(err)); }); assets.add(texture); promises.push(promise); return texture; }); }); const textureAssets = subs.map((sub)=>textures[sub]).flat(); let unloaded = false; asset.once('unload', ()=>{ unloaded = true; textureAssets.forEach((t)=>{ assets.remove(t); t.unload(); }); }); combineProgress(asset, textureAssets); textureAssets.forEach((t)=>assets.load(t)); await Promise.allSettled(promises); if (this._shouldAbort(asset, unloaded)) { textureAssets.forEach((t)=>{ assets.remove(t); t.unload(); }); callback(null, null); return; } const data = new GSplatSogsData(); data.url = url.original; data.meta = meta; data.numSplats = meta.count; data.means_l = textures.means[0].resource; data.means_u = textures.means[1].resource; data.quats = textures.quats[0].resource; data.scales = textures.scales[0].resource; data.sh0 = textures.sh0[0].resource; data.sh_centroids = textures.shN?.[0]?.resource; data.sh_labels = textures.shN?.[1]?.resource; data.shBands = GSplatSogsData.calcBands(data.sh_centroids?.width); const decompress = asset.data?.decompress; const minimalMemory = asset.options?.minimalMemory ?? false; data.minimalMemory = minimalMemory; if (!decompress) { if (this._shouldAbort(asset, unloaded)) { data.destroy(); callback(null, null); return; } await data.prepareGpuData(); } if (this._shouldAbort(asset, unloaded)) { data.destroy(); callback(null, null); return; } const resource = decompress ? new GSplatResource(this.app.graphicsDevice, await data.decompress()) : new GSplatSogsResource(this.app.graphicsDevice, data); if (this._shouldAbort(asset, unloaded)) { resource.destroy(); callback(null, null); return; } callback(null, resource); } load(url, callback, asset) { if (asset.data?.means) { this.loadTextures(url, callback, asset, asset.data); } else { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries, responseType: Http.ResponseType.JSON }; http.get(url.load, options, (err, meta)=>{ if (this._shouldAbort(asset, false)) { callback(null, null); return; } if (!err) { this.loadTextures(url, callback, asset, meta); } else { callback(`Error loading gsplat meta: ${url.original} [${err}]`); } }); } } constructor(app, maxRetries){ this.app = app; this.maxRetries = maxRetries; } } const parseZipArchive = (data)=>{ const dataView = new DataView(data); const u16 = (offset)=>dataView.getUint16(offset, true); const u32 = (offset)=>dataView.getUint32(offset, true); const extractEocd = (offset)=>{ return { magic: u32(offset), numFiles: u16(offset + 8), cdSizeBytes: u32(offset + 12), cdOffsetBytes: u32(offset + 16) }; }; const extractCdr = (offset)=>{ const filenameLength = u16(offset + 28); const extraFieldLength = u16(offset + 30); const fileCommentLength = u16(offset + 32); return { magic: u32(offset), compressionMethod: u16(offset + 10), compressedSizeBytes: u32(offset + 20), uncompressedSizeBytes: u32(offset + 24), lfhOffsetBytes: u32(offset + 42), filename: new TextDecoder().decode(new Uint8Array(data, offset + 46, filenameLength)), recordSizeBytes: 46 + filenameLength + extraFieldLength + fileCommentLength }; }; const extractLfh = (offset)=>{ const filenameLength = u16(offset + 26); const extraLength = u16(offset + 28); return { magic: u32(offset), offsetBytes: offset + 30 + filenameLength + extraLength }; }; const eocd = extractEocd(dataView.byteLength - 22); if (eocd.magic !== 0x06054b50) { throw new Error('Invalid zip file: EOCDR not found'); } if (eocd.cdOffsetBytes === 0xffffffff || eocd.cdSizeBytes === 0xffffffff) { throw new Error('Invalid zip file: Zip64 not supported'); } const result = []; let offset = eocd.cdOffsetBytes; for(let i = 0; i < eocd.numFiles; i++){ const cdr = extractCdr(offset); if (cdr.magic !== 0x02014b50) { throw new Error('Invalid zip file: CDR not found'); } const lfh = extractLfh(cdr.lfhOffsetBytes); if (lfh.magic !== 0x04034b50) { throw new Error('Invalid zip file: LFH not found'); } result.push({ filename: cdr.filename, compression: { 0: 'none', 8: 'deflate' }[cdr.compressionMethod] ?? 'unknown', data: new Uint8Array(data, lfh.offsetBytes, cdr.compressedSizeBytes) }); offset += cdr.recordSizeBytes; } return result; }; const inflate = async (compressed)=>{ const ds = new DecompressionStream('deflate-raw'); const out = new Blob([ compressed ]).stream().pipeThrough(ds); const ab = await new Response(out).arrayBuffer(); return new Uint8Array(ab); }; const downloadArrayBuffer = async (url, asset)=>{ const response = await (asset.file?.contents ?? fetch(url.load)); if (!response) { throw new Error('Error loading resource'); } if (response instanceof Response) { if (!response.ok) { throw new Error(`Error loading resource: ${response.status} ${response.statusText}`); } const totalLength = parseInt(response.headers.get('content-length') ?? '0', 10); if (!response.body || !response.body.getReader) { const buf = await response.arrayBuffer(); asset.fire('progress', buf.byteLength, totalLength); return buf; } const reader = response.body.getReader(); const chunks = []; let totalReceived = 0; try { while(true){ const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); totalReceived += value.byteLength; asset.fire('progress', totalReceived, totalLength); } } finally{ reader.releaseLock(); } return new Blob(chunks).arrayBuffer(); } return response; }; class SogBundleParser { async load(url, callback, asset) { try { const arrayBuffer = await downloadArrayBuffer(url, asset); const files = parseZipArchive(arrayBuffer); for (const file of files){ if (file.compression === 'deflate') { file.data = await inflate(file.data); } } const metaFile = files.find((f)=>f.filename === 'meta.json'); if (!metaFile) { callback('Error: meta.json not found'); return; } let meta; try { meta = JSON.parse(new TextDecoder().decode(metaFile.data)); } catch (err) { callback(`Error parsing meta.json: ${err}`); return; } const filenames = [ 'means', 'scales', 'quats', 'sh0', 'shN' ].map((key)=>meta[key]?.files ?? []).flat(); const textures = {}; const promises = []; for (const filename of filenames){ const file = files.find((f)=>f.filename === filename); let texture; if (file) { texture = new Asset(filename, 'texture', { url: `${url.load}/${filename}`, filename, contents: file.data }, { mipmaps: false }, { crossOrigin: 'anonymous' }); } else { const url = new URL(filename, new URL(filename, window.location.href).toString()).toString(); texture = new Asset(filename, 'texture', { url, filename }, { mipmaps: false }, { crossOrigin: 'anonymous' }); } const promise = new Promise((resolve, reject)=>{ texture.on('load', ()=>resolve(null)); texture.on('error', (err)=>reject(err)); }); this.app.assets.add(texture); textures[filename] = texture; promises.push(promise); } Object.values(textures).forEach((t)=>this.app.assets.load(t)); await Promise.allSettled(promises); const { assets } = this.app; asset.once('unload', ()=>{ Object.values(textures).forEach((t)=>{ assets.remove(t); t.unload(); }); }); const decompress = asset.data?.decompress; const minimalMemory = asset.options?.minimalMemory ?? false; const data = new GSplatSogsData(); data.url = url.original; data.minimalMemory = minimalMemory; data.meta = meta; data.numSplats = meta.count; data.means_l = textures[meta.means.files[0]].resource; data.means_u = textures[meta.means.files[1]].resource; data.quats = textures[meta.quats.files[0]].resource; data.scales = textures[meta.scales.files[0]].resource; data.sh0 = textures[meta.sh0.files[0]].resource; data.sh_centroids = textures[meta.shN?.files[0]]?.resource; data.sh_labels = textures[meta.shN?.files[1]]?.resource; data.shBands = GSplatSogsData.calcBands(data.sh_centroids?.width); if (!decompress) { await data.prepareGpuData(); } const resource = decompress ? new GSplatResource(this.app.graphicsDevice, await data.decompress()) : new GSplatSogsResource(this.app.graphicsDevice, data); callback(null, resource); } catch (err) { callback(err); } } constructor(app, maxRetries = 3){ this.app = app; this.maxRetries = maxRetries; } } class GSplatAssetLoaderBase { load(url) {} unload(url) {} getResource(url) {} destroy() {} } class GSplatAssetLoader extends GSplatAssetLoaderBase { destroy() { this._destroyed = true; for (const asset of this._urlToAsset.values()){ asset.fire('unload', asset); asset.off('load'); asset.off('error'); this._registry.remove(asset); asset.unload(); } this._urlToAsset.clear(); this._loadQueue.length = 0; this._currentlyLoading.clear(); this._retryCount.clear(); } _canLoad() { return !!this._registry.loader?.getHandler('gsplat'); } load(url) { const asset = this._urlToAsset.get(url); if (asset?.loaded || this._currentlyLoading.has(url)) { return; } if (this._loadQueue.includes(url)) { return; } if (this._currentlyLoading.size < this.maxConcurrentLoads) { this._startLoading(url); } else { this._loadQueue.push(url); } } _startLoading(url) { this._currentlyLoading.add(url); let asset = this._urlToAsset.get(url); if (!asset) { asset = new Asset(url, 'gsplat', { url }, {}, { minimalMemory: true }); this._registry.add(asset); this._urlToAsset.set(url, asset); } asset.once('load', ()=>this._onAssetLoadSuccess(url, asset)); asset.once('error', (err)=>this._onAssetLoadError(url, asset, err)); if (!asset.loaded && !asset.loading) { this._registry.load(asset); } } _onAssetLoadSuccess(url, asset) { if (this._destroyed || !this._urlToAsset.has(url)) { return; } this._currentlyLoading.delete(url); this._retryCount.delete(url); this._processQueue(); } _onAssetLoadError(url, asset, err) { if (this._destroyed || !this._canLoad() || !this._urlToAsset.has(url)) { return; } const retryCount = this._retryCount.get(url) || 0; if (retryCount < this.maxRetries) { this._retryCount.set(url, retryCount + 1); asset.loaded = false; asset.loading = false; this._registry.load(asset); } else { this._currentlyLoading.delete(url); this._retryCount.delete(url); this._processQueue(); } } _processQueue() { if (this._destroyed || !this._canLoad()) { return; } while(this._currentlyLoading.size < this.maxConcurrentLoads && this._loadQueue.length > 0){ const url = this._loadQueue.shift(); if (url) { this._startLoading(url); } } } unload(url) { this._currentlyLoading.delete(url); const queueIndex = this._loadQueue.indexOf(url); if (queueIndex !== -1) { this._loadQueue.splice(queueIndex, 1); } this._retryCount.delete(url); const asset = this._urlToAsset.get(url); if (asset) { asset.fire('unload', asset); asset.off('load'); asset.off('error'); this._registry.remove(asset); asset.unload(); this._urlToAsset.delete(url); } this._processQueue(); } getResource(url) { const asset = this._urlToAsset.get(url); return asset?.resource; } constructor(registry){ super(), this._urlToAsset = new Map(), this.maxConcurrentLoads = 2, this.maxRetries = 2, this._currentlyLoading = new Set(), this._loadQueue = [], this._retryCount = new Map(), this._destroyed = false; this._registry = registry; } } class GSplatOctreeParser { load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries, responseType: Http.ResponseType.JSON }; http.get(url.load, options, (err, data)=>{ if (!err) { const assetLoader = new GSplatAssetLoader(this.app.assets); const resource = new GSplatOctreeResource(asset.file.url, data, assetLoader); callback(null, resource); } else { callback(`Error loading gsplat octree: ${url.original} [${err}]`); } }); } constructor(app, maxRetries){ this.app = app; this.maxRetries = maxRetries; } } class GSplatHandler extends ResourceHandler { _getUrlWithoutParams(url) { return url.indexOf('?') >= 0 ? url.split('?')[0] : url; } _getParser(url) { const basename = path.getBasename(this._getUrlWithoutParams(url)).toLowerCase(); if (basename === 'lod-meta.json') { return this.parsers.octree; } const ext = path.getExtension(basename).replace('.', ''); return this.parsers[ext] || this.parsers.ply; } load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } this._getParser(url.original).load(url, callback, asset); } open(url, data, asset) { return data; } constructor(app){ super(app, 'gsplat'); this.parsers = { ply: new PlyParser(app, 3), sog: new SogBundleParser(app), json: new SogsParser(app, 3), octree: new GSplatOctreeParser(app, 3) }; } } class CompressUtils { static setCompressedPRS(entity, data, compressed) { const a = compressed.singleVecs; let b, i; const v = data.___1; if (!v) { b = compressed.tripleVecs; i = data.___2; } let n = v ? v[0] : b[i]; entity.setLocalPosition(a[n], a[n + 1], a[n + 2]); n = v ? v[1] : b[i + 1]; entity.setLocalEulerAngles(a[n], a[n + 1], a[n + 2]); n = v ? v[2] : b[i + 2]; entity.setLocalScale(a[n], a[n + 1], a[n + 2]); } static oneCharToKey(s, data) { const i = s.charCodeAt(0) - data.fieldFirstCode; return data.fieldArray[i]; } static multCharToKey(s, data) { let ind = 0; for(let i = 0; i < s.length; i++){ ind = ind * data.fieldCodeBase + s.charCodeAt(i) - data.fieldFirstCode; } return data.fieldArray[ind]; } } class Decompress { run() { const type = Object.prototype.toString.call(this._node); if (type === '[object Object]') { this._handleMap(); } else if (type === '[object Array]') { this._handleArray(); } else { this._result = this._node; } return this._result; } _handleMap() { this._result = {}; const a = Object.keys(this._node); a.forEach(this._handleKey, this); } _handleKey(origKey) { let newKey = origKey; const len = origKey.length; if (len === 1) { newKey = CompressUtils.oneCharToKey(origKey, this._data); } else if (len === 2) { newKey = CompressUtils.multCharToKey(origKey, this._data); } this._result[newKey] = new Decompress(this._node[origKey], this._data).run(); } _handleArray() { this._result = []; this._node.forEach(this._handleArElt, this); } _handleArElt(elt) { const v = new Decompress(elt, this._data).run(); this._result.push(v); } constructor(node, data){ this._node = node; this._data = data; } } class SceneParser { parse(data) { const entities = {}; let parent = null; const compressed = data.compressedFormat; if (compressed && !data.entDecompressed) { data.entDecompressed = true; data.entities = new Decompress(data.entities, compressed).run(); } for(const id in data.entities){ const curData = data.entities[id]; const curEnt = this._createEntity(curData, compressed); entities[id] = curEnt; if (curData.parent === null) { parent = curEnt; } } for(const id in data.entities){ const curEnt = entities[id]; const children = data.entities[id].children; const len = children.length; for(let i = 0; i < len; i++){ const childEnt = entities[children[i]]; if (childEnt) { curEnt.addChild(childEnt); } } } this._openComponentData(parent, data.entities); return parent; } _createEntity(data, compressed) { const entity = new Entity(data.name, this._app); entity.setGuid(data.resource_id); this._setPosRotScale(entity, data, compressed); entity._enabled = data.enabled ?? true; if (this._isTemplate) { entity._template = true; } else { entity._enabledInHierarchy = entity._enabled; } entity.template = data.template; if (data.tags) { for(let i = 0; i < data.tags.length; i++){ entity.tags.add(data.tags[i]); } } return entity; } _setPosRotScale(entity, data, compressed) { if (compressed) { CompressUtils.setCompressedPRS(entity, data, compressed); } else { const p = data.position; const r = data.rotation; const s = data.scale; entity.setLocalPosition(p[0], p[1], p[2]); entity.setLocalEulerAngles(r[0], r[1], r[2]); entity.setLocalScale(s[0], s[1], s[2]); } } _openComponentData(entity, entities) { const systemsList = this._app.systems.list; let len = systemsList.length; const entityData = entities[entity.getGuid()]; for(let i = 0; i < len; i++){ const system = systemsList[i]; const componentData = entityData.components[system.id]; if (componentData) { system.addComponent(entity, componentData); } } len = entityData.children.length; const children = entity._children; for(let i = 0; i < len; i++){ if (children[i]) { children[i] = this._openComponentData(children[i], entities); } } return entity; } constructor(app, isTemplate){ this._app = app; this._isTemplate = isTemplate; } } class SceneUtils { static load(url, maxRetries, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { retry: maxRetries > 0, maxRetries: maxRetries }, (err, response)=>{ if (!err) { callback(err, response); } else { let errMsg = `Error while loading scene JSON ${url.original}`; if (err.message) { errMsg += `: ${err.message}`; if (err.stack) { errMsg += `\n${err.stack}`; } } else { errMsg += `: ${err}`; } callback(errMsg); } }); } } class HierarchyHandler extends ResourceHandler { load(url, callback) { SceneUtils.load(url, this.maxRetries, callback); } open(url, data) { this._app.systems.script.preloading = true; const parser = new SceneParser(this._app, false); const parent = parser.parse(data); this._app.systems.script.preloading = false; return parent; } constructor(app){ super(app, 'hierarchy'); } } class HtmlHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { callback(null, response); } else { callback(`Error loading html resource: ${url.original} [${err}]`); } }); } openBinary(data) { this.decoder ?? (this.decoder = new TextDecoder('utf-8')); return this.decoder.decode(data); } constructor(app){ super(app, 'html'), this.decoder = null; } } class JsonHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; if (url.load.startsWith('blob:')) { options.responseType = Http.ResponseType.JSON; } http.get(url.load, options, (err, response)=>{ if (!err) { callback(null, response); } else { callback(`Error loading JSON resource: ${url.original} [${err}]`); } }); } openBinary(data) { this.decoder ?? (this.decoder = new TextDecoder('utf-8')); return JSON.parse(this.decoder.decode(data)); } constructor(app){ super(app, 'json'), this.decoder = null; } } class StandardMaterialValidator { setInvalid(key, data) { this.valid = false; if (this.removeInvalid) { delete data[key]; } } validate(data) { const TYPES = standardMaterialParameterTypes; const REMOVED = standardMaterialRemovedParameters; const pathMapping = data.mappingFormat === 'path'; for(const key in data){ const type = TYPES[key]; if (!type) { if (REMOVED[key]) { delete data[key]; } else { this.valid = false; } continue; } if (type.startsWith('enum')) { const enumType = type.split(':')[1]; if (this.enumValidators[enumType]) { if (!this.enumValidators[enumType](data[key])) { this.setInvalid(key, data); } } } else if (type === 'number') { if (typeof data[key] !== 'number') { this.setInvalid(key, data); } } else if (type === 'boolean') { if (typeof data[key] !== 'boolean') { this.setInvalid(key, data); } } else if (type === 'string') { if (typeof data[key] !== 'string') { this.setInvalid(key, data); } } else if (type === 'vec2') { if (!(data[key] instanceof Array && data[key].length === 2)) { this.setInvalid(key, data); } } else if (type === 'rgb') { if (!(data[key] instanceof Array && data[key].length === 3)) { this.setInvalid(key, data); } } else if (type === 'texture') { if (!pathMapping) { if (!(typeof data[key] === 'number' || data[key] === null)) { if (!(data[key] instanceof Texture)) { this.setInvalid(key, data); } } } else { if (!(typeof data[key] === 'string' || data[key] === null)) { if (!(data[key] instanceof Texture)) { this.setInvalid(key, data); } } } } else if (type === 'boundingbox') { if (!(data[key].center && data[key].center instanceof Array && data[key].center.length === 3)) { this.setInvalid(key, data); } if (!(data[key].halfExtents && data[key].halfExtents instanceof Array && data[key].halfExtents.length === 3)) { this.setInvalid(key, data); } } else if (type === 'cubemap') { if (!(typeof data[key] === 'number' || data[key] === null || data[key] === undefined)) { if (!(data[key] instanceof Texture && data[key].cubemap)) { this.setInvalid(key, data); } } } else if (type === 'chunks') { const chunkNames = Object.keys(data[key]); for(let i = 0; i < chunkNames.length; i++){ if (typeof data[key][chunkNames[i]] !== 'string') { this.setInvalid(chunkNames[i], data[key]); } } } else { console.error(`Unknown material type: ${type}`); } } data.validated = true; return this.valid; } _createEnumValidator(values) { return function(value) { return values.indexOf(value) >= 0; }; } constructor(){ this.removeInvalid = true; this.valid = true; this.enumValidators = { occludeSpecular: this._createEnumValidator([ SPECOCC_NONE, SPECOCC_AO, SPECOCC_GLOSSDEPENDENT ]), cull: this._createEnumValidator([ CULLFACE_NONE, CULLFACE_BACK, CULLFACE_FRONT, CULLFACE_FRONTANDBACK ]), blendType: this._createEnumValidator([ BLEND_SUBTRACTIVE, BLEND_ADDITIVE, BLEND_NORMAL, BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_ADDITIVEALPHA, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, BLEND_MIN, BLEND_MAX ]), depthFunc: this._createEnumValidator([ FUNC_NEVER, FUNC_LESS, FUNC_EQUAL, FUNC_LESSEQUAL, FUNC_GREATER, FUNC_NOTEQUAL, FUNC_GREATEREQUAL, FUNC_ALWAYS ]) }; } } class JsonStandardMaterialParser { parse(input) { const migrated = this.migrate(input); const validated = this._validate(migrated); const material = new StandardMaterial(); this.initialize(material, validated); return material; } initialize(material, data) { if (!data.validated) { data = this._validate(data); } if (data.chunks) { if (data.chunks && Object.keys(data.chunks).length > 0) { const dstMap = material.shaderChunks.glsl; Object.entries(data.chunks).forEach(([key, value])=>dstMap.set(key, value)); } } for(const key in data){ const type = standardMaterialParameterTypes[key]; const value = data[key]; if (type === 'vec2') { material[key] = new Vec2(value[0], value[1]); } else if (type === 'rgb') { material[key] = new Color(value[0], value[1], value[2]); } else if (type === 'texture') { if (value instanceof Texture) { material[key] = value; } else if (!(material[key] instanceof Texture && typeof value === 'number' && value > 0)) { material[key] = null; } } else if (type === 'cubemap') { if (value instanceof Texture) { material[key] = value; } else if (!(material[key] instanceof Texture && typeof value === 'number' && value > 0)) { material[key] = null; } if (key === 'cubeMap' && !value) { material.prefilteredCubemaps = null; } } else if (type === 'boundingbox') { const center = new Vec3(value.center[0], value.center[1], value.center[2]); const halfExtents = new Vec3(value.halfExtents[0], value.halfExtents[1], value.halfExtents[2]); material[key] = new BoundingBox(center, halfExtents); } else { material[key] = data[key]; } } material.update(); } migrate(data) { if (data.shader) delete data.shader; if (data.mapping_format) { data.mappingFormat = data.mapping_format; delete data.mapping_format; } let i; const RENAMED_PROPERTIES = [ [ 'bumpMapFactor', 'bumpiness' ], [ 'aoUvSet', 'aoMapUv' ], [ 'aoMapVertexColor', 'aoVertexColor' ], [ 'diffuseMapVertexColor', 'diffuseVertexColor' ], [ 'emissiveMapVertexColor', 'emissiveVertexColor' ], [ 'specularMapVertexColor', 'specularVertexColor' ], [ 'metalnessMapVertexColor', 'metalnessVertexColor' ], [ 'opacityMapVertexColor', 'opacityVertexColor' ], [ 'glossMapVertexColor', 'glossVertexColor' ], [ 'lightMapVertexColor', 'lightVertexColor' ], [ 'specularMapTint', 'specularTint' ], [ 'metalnessMapTint', 'metalnessTint' ], [ 'clearCoatGlossiness', 'clearCoatGloss' ] ]; for(i = 0; i < RENAMED_PROPERTIES.length; i++){ const _old = RENAMED_PROPERTIES[i][0]; const _new = RENAMED_PROPERTIES[i][1]; if (data[_old] !== undefined) { if (data[_new] === undefined) { data[_new] = data[_old]; } delete data[_old]; } } const DEPRECATED_PROPERTIES = [ 'fresnelFactor', 'shadowSampleType' ]; for(i = 0; i < DEPRECATED_PROPERTIES.length; i++){ const name = DEPRECATED_PROPERTIES[i]; if (data.hasOwnProperty(name)) { delete data[name]; } } return data; } _validate(data) { if (!data.validated) { if (!this._validator) { this._validator = new StandardMaterialValidator(); } this._validator.validate(data); } return data; } constructor(){ this._validator = null; } } const PLACEHOLDER_MAP = { aoMap: 'white', aoDetailMap: 'white', diffuseMap: 'gray', diffuseDetailMap: 'gray', specularMap: 'gray', specularityFactorMap: 'white', metalnessMap: 'black', glossMap: 'gray', sheenMap: 'black', sheenGlossMap: 'gray', clearCoatMap: 'black', clearCoatGlossMap: 'gray', clearCoatNormalMap: 'normal', refractionMap: 'white', emissiveMap: 'gray', normalMap: 'normal', normalDetailMap: 'normal', heightMap: 'gray', opacityMap: 'gray', sphereMap: 'gray', lightMap: 'white', thicknessMap: 'black', iridescenceMap: 'black', iridescenceThicknessMap: 'black', envAtlas: 'black', anisotropyMap: 'black' }; class MaterialHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { if (callback) { response._engine = true; callback(null, response); } } else { if (callback) { callback(`Error loading material: ${url.original} [${err}]`); } } }); } open(url, data) { const material = this._parser.parse(data); if (data._engine) { material._data = data; delete data._engine; } return material; } patch(asset, assets) { if (asset.resource._data) { asset._data = asset.resource._data; delete asset.resource._data; } asset.data.name = asset.name; asset.resource.name = asset.name; this._bindAndAssignAssets(asset, assets); asset.off('unload', this._onAssetUnload, this); asset.on('unload', this._onAssetUnload, this); } _onAssetUnload(asset) { delete asset.data.parameters; delete asset.data.chunks; delete asset.data.name; } _assignTexture(parameterName, materialAsset, texture) { materialAsset.resource[parameterName] = texture; } _getPlaceholderTexture(parameterName) { const placeholder = PLACEHOLDER_MAP[parameterName]; return getBuiltInTexture(this._device, placeholder); } _assignPlaceholderTexture(parameterName, materialAsset) { materialAsset.resource[parameterName] = this._getPlaceholderTexture(parameterName); } _onTextureLoad(parameterName, materialAsset, textureAsset) { this._assignTexture(parameterName, materialAsset, textureAsset.resource); materialAsset.resource.update(); } _onTextureAdd(parameterName, materialAsset, textureAsset) { this._assets.load(textureAsset); } _onTextureRemoveOrUnload(parameterName, materialAsset, textureAsset) { const material = materialAsset.resource; if (material) { if (materialAsset.resource[parameterName] === textureAsset.resource) { this._assignPlaceholderTexture(parameterName, materialAsset); material.update(); } } } _assignCubemap(parameterName, materialAsset, textures) { materialAsset.resource[parameterName] = textures[0]; if (parameterName === 'cubeMap') { const prefiltered = textures.slice(1); if (prefiltered.every((t)=>t)) { materialAsset.resource.prefilteredCubemaps = prefiltered; } else if (prefiltered[0]) { materialAsset.resource.envAtlas = prefiltered[0]; } } } _onCubemapLoad(parameterName, materialAsset, cubemapAsset) { this._assignCubemap(parameterName, materialAsset, cubemapAsset.resources); this._parser.initialize(materialAsset.resource, materialAsset.data); } _onCubemapAdd(parameterName, materialAsset, cubemapAsset) { this._assets.load(cubemapAsset); } _onCubemapRemoveOrUnload(parameterName, materialAsset, cubemapAsset) { const material = materialAsset.resource; if (materialAsset.data.prefilteredCubeMap128 === cubemapAsset.resources[1]) { this._assignCubemap(parameterName, materialAsset, [ null, null, null, null, null, null, null ]); material.update(); } } _bindAndAssignAssets(materialAsset, assets) { const data = this._parser.migrate(materialAsset.data); const material = materialAsset.resource; const pathMapping = data.mappingFormat === 'path'; const TEXTURES = standardMaterialTextureParameters; let i, name, assetReference; for(i = 0; i < TEXTURES.length; i++){ name = TEXTURES[i]; assetReference = material._assetReferences[name]; const dataAssetId = data[name]; const materialTexture = material[name]; const isPlaceHolderTexture = materialTexture === this._getPlaceholderTexture(name); const dataValidated = data.validated; if (dataAssetId && (!materialTexture || !dataValidated || isPlaceHolderTexture)) { if (!assetReference) { assetReference = new AssetReference(name, materialAsset, assets, { load: this._onTextureLoad, add: this._onTextureAdd, remove: this._onTextureRemoveOrUnload, unload: this._onTextureRemoveOrUnload }, this); material._assetReferences[name] = assetReference; } if (pathMapping) { assetReference.url = materialAsset.getAbsoluteUrl(dataAssetId); } else { assetReference.id = dataAssetId; } if (assetReference.asset) { if (assetReference.asset.resource) { this._assignTexture(name, materialAsset, assetReference.asset.resource); } else { this._assignPlaceholderTexture(name, materialAsset); } assets.load(assetReference.asset); } } else { if (assetReference) { if (pathMapping) { assetReference.url = null; } else { assetReference.id = null; } } } } const CUBEMAPS = standardMaterialCubemapParameters; for(i = 0; i < CUBEMAPS.length; i++){ name = CUBEMAPS[i]; assetReference = material._assetReferences[name]; if (data[name] && !materialAsset.data.prefilteredCubeMap128) { if (!assetReference) { assetReference = new AssetReference(name, materialAsset, assets, { load: this._onCubemapLoad, add: this._onCubemapAdd, remove: this._onCubemapRemoveOrUnload, unload: this._onCubemapRemoveOrUnload }, this); material._assetReferences[name] = assetReference; } if (pathMapping) { assetReference.url = data[name]; } else { assetReference.id = data[name]; } if (assetReference.asset) { if (assetReference.asset.loaded) { this._assignCubemap(name, materialAsset, assetReference.asset.resources); } assets.load(assetReference.asset); } } } this._parser.initialize(material, data); } constructor(app){ super(app, 'material'); this._assets = app.assets; this._device = app.graphicsDevice; this._parser = new JsonStandardMaterialParser(); } } class GlbModelParser { parse(data, callback, asset) { GlbParser.parse('filename.glb', '', data, this._device, this._assets, asset?.options ?? {}, (err, result)=>{ if (err) { callback(err); } else { const model = GlbContainerResource.createModel(result, this._defaultMaterial); result.destroy(); callback(null, model); } }); } constructor(modelHandler){ this._device = modelHandler.device; this._defaultMaterial = modelHandler.defaultMaterial; this._assets = modelHandler.assets; } } const JSON_PRIMITIVE_TYPE = { 'points': PRIMITIVE_POINTS, 'lines': PRIMITIVE_LINES, 'lineloop': PRIMITIVE_LINELOOP, 'linestrip': PRIMITIVE_LINESTRIP, 'triangles': PRIMITIVE_TRIANGLES, 'trianglestrip': PRIMITIVE_TRISTRIP, 'trianglefan': PRIMITIVE_TRIFAN }; const JSON_VERTEX_ELEMENT_TYPE = { 'int8': TYPE_INT8, 'uint8': TYPE_UINT8, 'int16': TYPE_INT16, 'uint16': TYPE_UINT16, 'int32': TYPE_INT32, 'uint32': TYPE_UINT32, 'float32': TYPE_FLOAT32 }; class JsonModelParser { parse(data, callback) { const modelData = data.model; if (!modelData) { callback(null, null); return; } if (modelData.version <= 1) { callback('JsonModelParser#parse: Trying to parse unsupported model format.'); return; } const nodes = this._parseNodes(data); const skins = this._parseSkins(data, nodes); const vertexBuffers = this._parseVertexBuffers(data); const indices = this._parseIndexBuffers(data, vertexBuffers); const morphs = this._parseMorphs(data, nodes, vertexBuffers); const meshes = this._parseMeshes(data, skins.skins, morphs.morphs, vertexBuffers, indices.buffer, indices.data); const meshInstances = this._parseMeshInstances(data, nodes, meshes, skins.skins, skins.instances, morphs.morphs, morphs.instances); const model = new Model(); model.graph = nodes[0]; model.meshInstances = meshInstances; model.skinInstances = skins.instances; model.morphInstances = morphs.instances; model.getGraph().syncHierarchy(); callback(null, model); } _parseNodes(data) { const modelData = data.model; const nodes = []; let i; for(i = 0; i < modelData.nodes.length; i++){ const nodeData = modelData.nodes[i]; const node = new GraphNode(nodeData.name); node.setLocalPosition(nodeData.position[0], nodeData.position[1], nodeData.position[2]); node.setLocalEulerAngles(nodeData.rotation[0], nodeData.rotation[1], nodeData.rotation[2]); node.setLocalScale(nodeData.scale[0], nodeData.scale[1], nodeData.scale[2]); node.scaleCompensation = !!nodeData.scaleCompensation; nodes.push(node); } for(i = 1; i < modelData.parents.length; i++){ nodes[modelData.parents[i]].addChild(nodes[i]); } return nodes; } _parseSkins(data, nodes) { const modelData = data.model; const skins = []; const skinInstances = []; let i, j; for(i = 0; i < modelData.skins.length; i++){ const skinData = modelData.skins[i]; const inverseBindMatrices = []; for(j = 0; j < skinData.inverseBindMatrices.length; j++){ const ibm = skinData.inverseBindMatrices[j]; inverseBindMatrices[j] = new Mat4().set(ibm); } const skin = new Skin(this._device, inverseBindMatrices, skinData.boneNames); skins.push(skin); const skinInstance = new SkinInstance(skin); const bones = []; for(j = 0; j < skin.boneNames.length; j++){ const boneName = skin.boneNames[j]; const bone = nodes[0].findByName(boneName); bones.push(bone); } skinInstance.bones = bones; skinInstances.push(skinInstance); } return { skins: skins, instances: skinInstances }; } _getMorphVertexCount(modelData, morphIndex, vertexBuffers) { for(let i = 0; i < modelData.meshes.length; i++){ const meshData = modelData.meshes[i]; if (meshData.morph === morphIndex) { const vertexBuffer = vertexBuffers[meshData.vertices]; return vertexBuffer.numVertices; } } return undefined; } _parseMorphs(data, nodes, vertexBuffers) { const modelData = data.model; const morphs = []; const morphInstances = []; let i, j, vertexCount; let targets, morphTarget, morphTargetArray; if (modelData.morphs) { const sparseToFull = function(data, indices, totalCount) { const full = new Float32Array(totalCount * 3); for(let s = 0; s < indices.length; s++){ const dstIndex = indices[s] * 3; full[dstIndex] = data[s * 3]; full[dstIndex + 1] = data[s * 3 + 1]; full[dstIndex + 2] = data[s * 3 + 2]; } return full; }; for(i = 0; i < modelData.morphs.length; i++){ targets = modelData.morphs[i].targets; morphTargetArray = []; vertexCount = this._getMorphVertexCount(modelData, i, vertexBuffers); for(j = 0; j < targets.length; j++){ const targetAabb = targets[j].aabb; const min = targetAabb.min; const max = targetAabb.max; const aabb = new BoundingBox(new Vec3((max[0] + min[0]) * 0.5, (max[1] + min[1]) * 0.5, (max[2] + min[2]) * 0.5), new Vec3((max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5)); const indices = targets[j].indices; let deltaPositions = targets[j].deltaPositions; let deltaNormals = targets[j].deltaNormals; if (indices) { deltaPositions = sparseToFull(deltaPositions, indices, vertexCount); deltaNormals = sparseToFull(deltaNormals, indices, vertexCount); } morphTarget = new MorphTarget({ deltaPositions: deltaPositions, deltaNormals: deltaNormals, name: targets[j].name, aabb: aabb }); morphTargetArray.push(morphTarget); } const morph = new Morph(morphTargetArray, this._device); morphs.push(morph); const morphInstance = new MorphInstance(morph); morphInstances.push(morphInstance); } } return { morphs: morphs, instances: morphInstances }; } _parseVertexBuffers(data) { const modelData = data.model; const vertexBuffers = []; const attributeMap = { position: SEMANTIC_POSITION, normal: SEMANTIC_NORMAL, tangent: SEMANTIC_TANGENT, blendWeight: SEMANTIC_BLENDWEIGHT, blendIndices: SEMANTIC_BLENDINDICES, color: SEMANTIC_COLOR, texCoord0: SEMANTIC_TEXCOORD0, texCoord1: SEMANTIC_TEXCOORD1, texCoord2: SEMANTIC_TEXCOORD2, texCoord3: SEMANTIC_TEXCOORD3, texCoord4: SEMANTIC_TEXCOORD4, texCoord5: SEMANTIC_TEXCOORD5, texCoord6: SEMANTIC_TEXCOORD6, texCoord7: SEMANTIC_TEXCOORD7 }; for(let i = 0; i < modelData.vertices.length; i++){ const vertexData = modelData.vertices[i]; const formatDesc = []; for(const attributeName in vertexData){ const attribute = vertexData[attributeName]; formatDesc.push({ semantic: attributeMap[attributeName], components: attribute.components, type: JSON_VERTEX_ELEMENT_TYPE[attribute.type], normalize: attributeMap[attributeName] === SEMANTIC_COLOR }); } const vertexFormat = new VertexFormat(this._device, formatDesc); const numVertices = vertexData.position.data.length / vertexData.position.components; const vertexBuffer = new VertexBuffer(this._device, vertexFormat, numVertices); const iterator = new VertexIterator(vertexBuffer); for(let j = 0; j < numVertices; j++){ for(const attributeName in vertexData){ const attribute = vertexData[attributeName]; switch(attribute.components){ case 1: iterator.element[attributeMap[attributeName]].set(attribute.data[j]); break; case 2: iterator.element[attributeMap[attributeName]].set(attribute.data[j * 2], 1.0 - attribute.data[j * 2 + 1]); break; case 3: iterator.element[attributeMap[attributeName]].set(attribute.data[j * 3], attribute.data[j * 3 + 1], attribute.data[j * 3 + 2]); break; case 4: iterator.element[attributeMap[attributeName]].set(attribute.data[j * 4], attribute.data[j * 4 + 1], attribute.data[j * 4 + 2], attribute.data[j * 4 + 3]); break; } } iterator.next(); } iterator.end(); vertexBuffers.push(vertexBuffer); } return vertexBuffers; } _parseIndexBuffers(data, vertexBuffers) { const modelData = data.model; let indexBuffer = null; let indexData = null; let i; let numIndices = 0; for(i = 0; i < modelData.meshes.length; i++){ const meshData = modelData.meshes[i]; if (meshData.indices !== undefined) { numIndices += meshData.indices.length; } } let maxVerts = 0; for(i = 0; i < vertexBuffers.length; i++){ maxVerts = Math.max(maxVerts, vertexBuffers[i].numVertices); } if (numIndices > 0) { if (maxVerts > 0xFFFF) { indexBuffer = new IndexBuffer(this._device, INDEXFORMAT_UINT32, numIndices); indexData = new Uint32Array(indexBuffer.lock()); } else { indexBuffer = new IndexBuffer(this._device, INDEXFORMAT_UINT16, numIndices); indexData = new Uint16Array(indexBuffer.lock()); } } return { buffer: indexBuffer, data: indexData }; } _parseMeshes(data, skins, morphs, vertexBuffers, indexBuffer, indexData) { const modelData = data.model; const meshes = []; let indexBase = 0; for(let i = 0; i < modelData.meshes.length; i++){ const meshData = modelData.meshes[i]; const meshAabb = meshData.aabb; const min = meshAabb.min; const max = meshAabb.max; const aabb = new BoundingBox(new Vec3((max[0] + min[0]) * 0.5, (max[1] + min[1]) * 0.5, (max[2] + min[2]) * 0.5), new Vec3((max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5)); const indexed = meshData.indices !== undefined; const mesh = new Mesh(this._device); mesh.vertexBuffer = vertexBuffers[meshData.vertices]; mesh.indexBuffer[0] = indexed ? indexBuffer : null; mesh.primitive[0].type = JSON_PRIMITIVE_TYPE[meshData.type]; mesh.primitive[0].base = indexed ? meshData.base + indexBase : meshData.base; mesh.primitive[0].count = meshData.count; mesh.primitive[0].indexed = indexed; mesh.skin = meshData.skin !== undefined ? skins[meshData.skin] : null; mesh.morph = meshData.morph !== undefined ? morphs[meshData.morph] : null; mesh.aabb = aabb; if (indexed) { indexData.set(meshData.indices, indexBase); indexBase += meshData.indices.length; } meshes.push(mesh); } if (indexBuffer !== null) { indexBuffer.unlock(); } return meshes; } _parseMeshInstances(data, nodes, meshes, skins, skinInstances, morphs, morphInstances) { const modelData = data.model; const meshInstances = []; let i; for(i = 0; i < modelData.meshInstances.length; i++){ const meshInstanceData = modelData.meshInstances[i]; const node = nodes[meshInstanceData.node]; const mesh = meshes[meshInstanceData.mesh]; const meshInstance = new MeshInstance(mesh, this._defaultMaterial, node); if (mesh.skin) { const skinIndex = skins.indexOf(mesh.skin); meshInstance.skinInstance = skinInstances[skinIndex]; } if (mesh.morph) { const morphIndex = morphs.indexOf(mesh.morph); meshInstance.morphInstance = morphInstances[morphIndex]; } meshInstances.push(meshInstance); } return meshInstances; } constructor(modelHandler){ this._device = modelHandler.device; this._defaultMaterial = modelHandler.defaultMaterial; } } class ModelHandler extends ResourceHandler { load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; if (url.load.startsWith('blob:') || url.load.startsWith('data:')) { if (path.getExtension(url.original).toLowerCase() === '.glb') { options.responseType = Http.ResponseType.ARRAY_BUFFER; } else { options.responseType = Http.ResponseType.JSON; } } http.get(url.load, options, (err, response)=>{ if (!callback) { return; } if (!err) { for(let i = 0; i < this._parsers.length; i++){ const p = this._parsers[i]; if (p.decider(url.original, response)) { p.parser.parse(response, (err, parseResult)=>{ if (err) { callback(err); } else { callback(null, parseResult); } }, asset); return; } } callback('No parsers found'); } else { callback(`Error loading model: ${url.original} [${err}]`); } }); } open(url, data) { return data; } patch(asset, assets) { if (!asset.resource) { return; } const data = asset.data; const self = this; asset.resource.meshInstances.forEach((meshInstance, i)=>{ if (data.mapping) { const handleMaterial = function(asset) { if (asset.resource) { meshInstance.material = asset.resource; } else { asset.once('load', handleMaterial); assets.load(asset); } asset.once('remove', (asset)=>{ if (meshInstance.material === asset.resource) { meshInstance.material = self.defaultMaterial; } }); }; if (!data.mapping[i]) { meshInstance.material = self.defaultMaterial; return; } const id = data.mapping[i].material; const url = data.mapping[i].path; let material; if (id !== undefined) { if (!id) { meshInstance.material = self.defaultMaterial; } else { material = assets.get(id); if (material) { handleMaterial(material); } else { assets.once(`add:${id}`, handleMaterial); } } } else if (url) { const path = asset.getAbsoluteUrl(data.mapping[i].path); material = assets.getByUrl(path); if (material) { handleMaterial(material); } else { assets.once(`add:url:${path}`, handleMaterial); } } } }); } addParser(parser, decider) { this._parsers.push({ parser: parser, decider: decider }); } constructor(app){ super(app, 'model'); this._parsers = []; this.device = app.graphicsDevice; this.assets = app.assets; this.defaultMaterial = getDefaultMaterial(this.device); this.addParser(new JsonModelParser(this), (url, data)=>{ return path.getExtension(url) === '.json'; }); this.addParser(new GlbModelParser(this), (url, data)=>{ return path.getExtension(url) === '.glb'; }); } } class SceneHandler extends ResourceHandler { load(url, callback) { SceneUtils.load(url, this.maxRetries, callback); } open(url, data) { this._app.systems.script.preloading = true; const parser = new SceneParser(this._app, false); const parent = parser.parse(data); const scene = this._app.scene; scene.root = parent; this._app.applySceneSettings(data.settings); this._app.systems.script.preloading = false; return scene; } constructor(app){ super(app, 'scene'); } } class ScriptTypes { static push(Type) { ScriptTypes._types.push(Type); } } ScriptTypes._types = []; const reservedScriptNames = new Set([ 'system', 'entity', 'create', 'destroy', 'swap', 'move', 'data', "scripts", "_scripts", "_scriptsIndex", "_scriptsData", 'enabled', '_oldState', 'onEnable', 'onDisable', 'onPostStateChange', '_onSetEnabled', '_checkState', '_onBeforeRemove', '_onInitializeAttributes', '_onInitialize', '_onPostInitialize', '_onUpdate', '_onPostUpdate', '_callbacks', '_callbackActive', 'has', 'get', 'on', 'off', 'fire', 'once', 'hasEvent', 'worker' ]); const reservedAttributes = {}; ScriptAttributes.reservedNames.forEach((value, value2, set)=>{ reservedAttributes[value] = 1; }); function registerScript(script, name, app) { if (typeof script !== 'function') { throw new Error(`script class: '${script}' must be a constructor function (i.e. class).`); } if (!(script.prototype instanceof Script)) { throw new Error(`script class: '${ScriptType.__getScriptName(script)}' does not extend pc.Script.`); } name = name || script.__name || ScriptType.__getScriptName(script); if (reservedScriptNames.has(name)) { throw new Error(`script name: '${name}' is reserved, please change script name`); } script.__name = name; const registry = AppBase.getApplication().scripts; registry.add(script); ScriptTypes.push(script); } const toLowerCamelCase = (str)=>str[0].toLowerCase() + str.substring(1); class ScriptHandler extends ResourceHandler { clearCache() { for(const key in this._cache){ const element = this._cache[key]; const parent = element.parentNode; if (parent) { parent.removeChild(element); } } this._cache = {}; } load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const self = this; this._app; const onScriptLoad = (url.load, (err, url, extra)=>{ if (!err) { const obj = {}; for(let i = 0; i < ScriptTypes._types.length; i++){ obj[ScriptTypes._types[i].name] = ScriptTypes._types[i]; } ScriptTypes._types.length = 0; callback(null, obj, extra); const urlWithoutEndHash = url.split('&hash=')[0]; delete self._loader._cache[ResourceLoader.makeKey(urlWithoutEndHash, "script")]; } else { callback(err); } }); const [basePath] = url.load.split('?'); const isEsmScript = basePath.endsWith('.mjs'); if (isEsmScript) { this._loadModule(basePath, onScriptLoad); } else { this._loadScript(url.load, onScriptLoad); } } open(url, data) { return data; } patch(asset, assets) {} _loadScript(url, callback) { const head = document.head; const element = document.createElement("script"); this._cache[url] = element; element.async = false; element.addEventListener('error', (e)=>{ callback(`Script: ${e.target.src} failed to load`); }, false); let done = false; element.onload = element.onreadystatechange = function() { if (!done && (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')) { done = true; callback(null, url, element); } }; element.src = url; head.appendChild(element); } _loadModule(url, callback) { const isBrowserWithOrigin = platform.browser && window.location.origin !== 'null'; const baseUrl = isBrowserWithOrigin ? window.location.origin + window.location.pathname : import.meta.url; const importUrl = new URL(url, baseUrl); import(/* @vite-ignore */ /* webpackIgnore: true */ importUrl.toString()).then((module)=>{ const filename = importUrl.pathname.split('/').pop(); const scriptSchema = this._app.assets.find(filename, "script")?.data?.scripts; for(const key in module){ const scriptClass = module[key]; const extendsScriptType = scriptClass.prototype instanceof Script; if (extendsScriptType) { const lowerCamelCaseName = toLowerCamelCase(scriptClass.name); if (!scriptClass.scriptName) ; const scriptName = scriptClass.scriptName ?? lowerCamelCaseName; registerScript(scriptClass, scriptName); if (scriptSchema) this._app.scripts.addSchema(scriptName, scriptSchema[scriptName]); } } callback(null, url, null); }).catch((err)=>{ callback(err); }); } constructor(app){ super(app, "script"); this._scripts = {}; this._cache = {}; } } class ShaderHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { callback(null, response); } else { callback(`Error loading shader resource: ${url.original} [${err}]`); } }); } openBinary(data) { this.decoder ?? (this.decoder = new TextDecoder('utf-8')); return this.decoder.decode(data); } constructor(app){ super(app, 'shader'), this.decoder = null; } } function onTextureAtlasLoaded(atlasAsset) { const spriteAsset = this; if (spriteAsset.resource) { spriteAsset.resource.atlas = atlasAsset.resource; } } function onTextureAtlasAdded(atlasAsset) { const spriteAsset = this; spriteAsset.registry.load(atlasAsset); } class SpriteHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } if (path.getExtension(url.original) === '.json') { http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { callback(null, response); } else { callback(err); } }); } } open(url, data) { const sprite = new Sprite(this._device); if (url) { sprite.__data = data; } return sprite; } patch(asset, assets) { const sprite = asset.resource; if (sprite.__data) { asset.data.pixelsPerUnit = sprite.__data.pixelsPerUnit; asset.data.renderMode = sprite.__data.renderMode; asset.data.frameKeys = sprite.__data.frameKeys; if (sprite.__data.textureAtlasAsset) { const atlas = assets.getByUrl(sprite.__data.textureAtlasAsset); if (atlas) { asset.data.textureAtlasAsset = atlas.id; } else { console.warn(`Could not find textureatlas with url: ${sprite.__data.textureAtlasAsset}`); } } } sprite.startUpdate(); sprite.renderMode = asset.data.renderMode; sprite.pixelsPerUnit = asset.data.pixelsPerUnit; sprite.frameKeys = asset.data.frameKeys; this._updateAtlas(asset); sprite.endUpdate(); asset.off('change', this._onAssetChange, this); asset.on('change', this._onAssetChange, this); } _updateAtlas(asset) { const sprite = asset.resource; if (!asset.data.textureAtlasAsset) { sprite.atlas = null; return; } this._assets.off(`load:${asset.data.textureAtlasAsset}`, onTextureAtlasLoaded, asset); this._assets.on(`load:${asset.data.textureAtlasAsset}`, onTextureAtlasLoaded, asset); const atlasAsset = this._assets.get(asset.data.textureAtlasAsset); if (atlasAsset && atlasAsset.resource) { sprite.atlas = atlasAsset.resource; } else { if (!atlasAsset) { this._assets.off(`add:${asset.data.textureAtlasAsset}`, onTextureAtlasAdded, asset); this._assets.on(`add:${asset.data.textureAtlasAsset}`, onTextureAtlasAdded, asset); } else { this._assets.load(atlasAsset); } } } _onAssetChange(asset, attribute, value, oldValue) { if (attribute === 'data') { if (value && value.textureAtlasAsset && oldValue && value.textureAtlasAsset !== oldValue.textureAtlasAsset) { this._assets.off(`load:${oldValue.textureAtlasAsset}`, onTextureAtlasLoaded, asset); this._assets.off(`add:${oldValue.textureAtlasAsset}`, onTextureAtlasAdded, asset); } } } constructor(app){ super(app, 'sprite'); this._assets = app.assets; this._device = app.graphicsDevice; } } class Template { instantiate() { if (!this._templateRoot) { this._parseTemplate(); } return this._templateRoot.clone(); } _parseTemplate() { const parser = new SceneParser(this._app, true); this._templateRoot = parser.parse(this._data); } set data(value) { this._data = value; this._templateRoot = null; } get data() { return this._data; } constructor(app, data){ this._templateRoot = null; this._app = app; this._data = data; } } class TemplateHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries }; http.get(url.load, options, (err, response)=>{ if (err) { callback(`Error requesting template: ${url.original}`); } else { callback(err, response); } }); } open(url, data) { return new Template(this._app, data); } openBinary(data) { this.decoder ?? (this.decoder = new TextDecoder('utf-8')); return new Template(this._app, JSON.parse(this.decoder.decode(data))); } patch(asset, registry) { if (!asset || !asset.resource || !asset.data || !asset.data.entities) { return; } const template = asset.resource; template.data = asset.data; } constructor(app){ super(app, 'template'), this.decoder = null; } } class TextHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { callback(null, response); } else { callback(`Error loading text resource: ${url.original} [${err}]`); } }); } openBinary(data) { this.decoder ?? (this.decoder = new TextDecoder('utf-8')); return this.decoder.decode(data); } constructor(app){ super(app, 'text'), this.decoder = null; } } const JSON_ADDRESS_MODE$1 = { 'repeat': ADDRESS_REPEAT, 'clamp': ADDRESS_CLAMP_TO_EDGE, 'mirror': ADDRESS_MIRRORED_REPEAT }; const JSON_FILTER_MODE$1 = { 'nearest': FILTER_NEAREST, 'linear': FILTER_LINEAR, 'nearest_mip_nearest': FILTER_NEAREST_MIPMAP_NEAREST, 'linear_mip_nearest': FILTER_LINEAR_MIPMAP_NEAREST, 'nearest_mip_linear': FILTER_NEAREST_MIPMAP_LINEAR, 'linear_mip_linear': FILTER_LINEAR_MIPMAP_LINEAR }; const regexFrame = /^data\.frames\.(\d+)$/; class TextureAtlasHandler extends ResourceHandler { load(url, callback) { if (typeof url === 'string') { url = { load: url, original: url }; } const self = this; const handler = this._loader.getHandler('texture'); if (path.getExtension(url.original) === '.json') { http.get(url.load, { retry: this.maxRetries > 0, maxRetries: this.maxRetries }, (err, response)=>{ if (!err) { const textureUrl = url.original.replace('.json', '.png'); self._loader.load(textureUrl, 'texture', (err, texture)=>{ if (err) { callback(err); } else { callback(null, { data: response, texture: texture }); } }); } else { callback(err); } }); } else { handler.load(url, callback); } } open(url, data, asset) { const resource = new TextureAtlas(); if (data.texture && data.data) { resource.texture = data.texture; resource.__data = data.data; } else { const handler = this._loader.getHandler('texture'); const texture = handler.open(url, data, asset); if (!texture) return null; resource.texture = texture; } return resource; } patch(asset, assets) { if (!asset.resource) { return; } if (asset.resource.__data) { if (asset.resource.__data.minfilter !== undefined) asset.data.minfilter = asset.resource.__data.minfilter; if (asset.resource.__data.magfilter !== undefined) asset.data.magfilter = asset.resource.__data.magfilter; if (asset.resource.__data.addressu !== undefined) asset.data.addressu = asset.resource.__data.addressu; if (asset.resource.__data.addressv !== undefined) asset.data.addressv = asset.resource.__data.addressv; if (asset.resource.__data.mipmaps !== undefined) asset.data.mipmaps = asset.resource.__data.mipmaps; if (asset.resource.__data.anisotropy !== undefined) asset.data.anisotropy = asset.resource.__data.anisotropy; if (asset.resource.__data.rgbm !== undefined) asset.data.rgbm = !!asset.resource.__data.rgbm; asset.data.frames = asset.resource.__data.frames; delete asset.resource.__data; } const texture = asset.resource.texture; if (texture) { texture.name = asset.name; if (asset.data.hasOwnProperty('minfilter') && texture.minFilter !== JSON_FILTER_MODE$1[asset.data.minfilter]) { texture.minFilter = JSON_FILTER_MODE$1[asset.data.minfilter]; } if (asset.data.hasOwnProperty('magfilter') && texture.magFilter !== JSON_FILTER_MODE$1[asset.data.magfilter]) { texture.magFilter = JSON_FILTER_MODE$1[asset.data.magfilter]; } if (asset.data.hasOwnProperty('addressu') && texture.addressU !== JSON_ADDRESS_MODE$1[asset.data.addressu]) { texture.addressU = JSON_ADDRESS_MODE$1[asset.data.addressu]; } if (asset.data.hasOwnProperty('addressv') && texture.addressV !== JSON_ADDRESS_MODE$1[asset.data.addressv]) { texture.addressV = JSON_ADDRESS_MODE$1[asset.data.addressv]; } if (asset.data.hasOwnProperty('mipmaps') && texture.mipmaps !== asset.data.mipmaps) { texture.mipmaps = asset.data.mipmaps; } if (asset.data.hasOwnProperty('anisotropy') && texture.anisotropy !== asset.data.anisotropy) { texture.anisotropy = asset.data.anisotropy; } if (asset.data.hasOwnProperty('rgbm')) { const type = asset.data.rgbm ? TEXTURETYPE_RGBM : TEXTURETYPE_DEFAULT; if (texture.type !== type) { texture.type = type; } } } asset.resource.texture = texture; const frames = {}; for(const key in asset.data.frames){ const frame = asset.data.frames[key]; frames[key] = { rect: new Vec4(frame.rect), pivot: new Vec2(frame.pivot), border: new Vec4(frame.border) }; } asset.resource.frames = frames; asset.off('change', this._onAssetChange, this); asset.on('change', this._onAssetChange, this); } _onAssetChange(asset, attribute, value) { let frame; if (attribute === 'data' || attribute === 'data.frames') { const frames = {}; for(const key in value.frames){ frame = value.frames[key]; frames[key] = { rect: new Vec4(frame.rect), pivot: new Vec2(frame.pivot), border: new Vec4(frame.border) }; } asset.resource.frames = frames; } else { const match = attribute.match(regexFrame); if (match) { const frameKey = match[1]; if (value) { if (!asset.resource.frames[frameKey]) { asset.resource.frames[frameKey] = { rect: new Vec4(value.rect), pivot: new Vec2(value.pivot), border: new Vec4(value.border) }; } else { frame = asset.resource.frames[frameKey]; frame.rect.set(value.rect[0], value.rect[1], value.rect[2], value.rect[3]); frame.pivot.set(value.pivot[0], value.pivot[1]); frame.border.set(value.border[0], value.border[1], value.border[2], value.border[3]); } asset.resource.fire('set:frame', frameKey, asset.resource.frames[frameKey]); } else { if (asset.resource.frames[frameKey]) { delete asset.resource.frames[frameKey]; asset.resource.fire('remove:frame', frameKey); } } } } } constructor(app){ super(app, 'textureatlas'); this._loader = app.loader; } } function BasisWorker() { const BASIS_FORMAT = { cTFETC1: 0, cTFETC2: 1, cTFBC1: 2, cTFBC3: 3, cTFPVRTC1_4_RGB: 8, cTFPVRTC1_4_RGBA: 9, cTFASTC_4x4: 10, cTFATC_RGB: 11, cTFATC_RGBA_INTERPOLATED_ALPHA: 12, cTFRGBA32: 13, cTFRGB565: 14, cTFRGBA4444: 16 }; const opaqueMapping = { astc: BASIS_FORMAT.cTFASTC_4x4, dxt: BASIS_FORMAT.cTFBC1, etc1: BASIS_FORMAT.cTFETC1, etc2: BASIS_FORMAT.cTFETC1, pvr: BASIS_FORMAT.cTFPVRTC1_4_RGB, atc: BASIS_FORMAT.cTFATC_RGB, none: BASIS_FORMAT.cTFRGB565 }; const alphaMapping = { astc: BASIS_FORMAT.cTFASTC_4x4, dxt: BASIS_FORMAT.cTFBC3, etc1: BASIS_FORMAT.cTFRGBA4444, etc2: BASIS_FORMAT.cTFETC2, pvr: BASIS_FORMAT.cTFPVRTC1_4_RGBA, atc: BASIS_FORMAT.cTFATC_RGBA_INTERPOLATED_ALPHA, none: BASIS_FORMAT.cTFRGBA4444 }; const PIXEL_FORMAT = { ETC1: 21, ETC2_RGB: 22, ETC2_RGBA: 23, DXT1: 8, DXT5: 10, PVRTC_4BPP_RGB_1: 26, PVRTC_4BPP_RGBA_1: 27, ASTC_4x4: 28, ATC_RGB: 29, ATC_RGBA: 30, R8_G8_B8_A8: 7, R5_G6_B5: 3, R4_G4_B4_A4: 5 }; const basisToEngineMapping = (basisFormat, deviceDetails)=>{ switch(basisFormat){ case BASIS_FORMAT.cTFETC1: return deviceDetails.formats.etc2 ? PIXEL_FORMAT.ETC2_RGB : PIXEL_FORMAT.ETC1; case BASIS_FORMAT.cTFETC2: return PIXEL_FORMAT.ETC2_RGBA; case BASIS_FORMAT.cTFBC1: return PIXEL_FORMAT.DXT1; case BASIS_FORMAT.cTFBC3: return PIXEL_FORMAT.DXT5; case BASIS_FORMAT.cTFPVRTC1_4_RGB: return PIXEL_FORMAT.PVRTC_4BPP_RGB_1; case BASIS_FORMAT.cTFPVRTC1_4_RGBA: return PIXEL_FORMAT.PVRTC_4BPP_RGBA_1; case BASIS_FORMAT.cTFASTC_4x4: return PIXEL_FORMAT.ASTC_4x4; case BASIS_FORMAT.cTFATC_RGB: return PIXEL_FORMAT.ATC_RGB; case BASIS_FORMAT.cTFATC_RGBA_INTERPOLATED_ALPHA: return PIXEL_FORMAT.ATC_RGBA; case BASIS_FORMAT.cTFRGBA32: return PIXEL_FORMAT.R8_G8_B8_A8; case BASIS_FORMAT.cTFRGB565: return PIXEL_FORMAT.R5_G6_B5; case BASIS_FORMAT.cTFRGBA4444: return PIXEL_FORMAT.R4_G4_B4_A4; } }; const unswizzleGGGR = (data)=>{ const genB = function(R, G) { const r = R * (2.0 / 255.0) - 1.0; const g = G * (2.0 / 255.0) - 1.0; const b = Math.sqrt(1.0 - Math.min(1.0, r * r + g * g)); return Math.max(0, Math.min(255, Math.floor((b + 1.0) * 0.5 * 255.0))); }; for(let offset = 0; offset < data.length; offset += 4){ const R = data[offset + 3]; const G = data[offset + 1]; data[offset + 0] = R; data[offset + 2] = genB(R, G); data[offset + 3] = 255; } return data; }; const pack565 = (data)=>{ const result = new Uint16Array(data.length / 4); for(let offset = 0; offset < data.length; offset += 4){ const R = data[offset + 0]; const G = data[offset + 1]; const B = data[offset + 2]; result[offset / 4] = (R & 0xf8) << 8 | (G & 0xfc) << 3 | B >> 3; } return result; }; const isPOT = (width, height)=>{ return (width & width - 1) === 0 && (height & height - 1) === 0; }; const performanceNow = ()=>{ return typeof performance !== 'undefined' ? performance.now() : 0; }; let basis; let rgbPriority; let rgbaPriority; const chooseTargetFormat = (deviceDetails, hasAlpha, isUASTC)=>{ if (isUASTC) { if (deviceDetails.formats.astc) { return 'astc'; } } else { if (hasAlpha) { if (deviceDetails.formats.etc2) { return 'etc2'; } } else { if (deviceDetails.formats.etc2) { return 'etc2'; } if (deviceDetails.formats.etc1) { return 'etc1'; } } } const testInOrder = (priority)=>{ for(let i = 0; i < priority.length; ++i){ const format = priority[i]; if (deviceDetails.formats[format]) { return format; } } return 'none'; }; return testInOrder(hasAlpha ? rgbaPriority : rgbPriority); }; const dimensionsValid = (width, height, format)=>{ switch(format){ case BASIS_FORMAT.cTFETC1: case BASIS_FORMAT.cTFETC2: return true; case BASIS_FORMAT.cTFBC1: case BASIS_FORMAT.cTFBC3: return (width & 0x3) === 0 && (height & 0x3) === 0; case BASIS_FORMAT.cTFPVRTC1_4_RGB: case BASIS_FORMAT.cTFPVRTC1_4_RGBA: return isPOT(width, height); case BASIS_FORMAT.cTFASTC_4x4: return true; case BASIS_FORMAT.cTFATC_RGB: case BASIS_FORMAT.cTFATC_RGBA_INTERPOLATED_ALPHA: return true; } return false; }; const transcodeKTX2 = (url, data, options)=>{ if (!basis.KTX2File) { throw new Error('Basis transcoder module does not include support for KTX2.'); } const funcStart = performanceNow(); const basisFile = new basis.KTX2File(new Uint8Array(data)); const width = basisFile.getWidth(); const height = basisFile.getHeight(); const levels = basisFile.getLevels(); const hasAlpha = !!basisFile.getHasAlpha(); const isUASTC = basisFile.isUASTC && basisFile.isUASTC(); if (!width || !height || !levels) { basisFile.close(); basisFile.delete(); throw new Error(`Invalid image dimensions url=${url} width=${width} height=${height} levels=${levels}`); } const format = chooseTargetFormat(options.deviceDetails, hasAlpha, isUASTC); const unswizzle = !!options.isGGGR && format === 'pvr'; let basisFormat; if (unswizzle) { basisFormat = BASIS_FORMAT.cTFRGBA32; } else { basisFormat = hasAlpha ? alphaMapping[format] : opaqueMapping[format]; if (!dimensionsValid(width, height, basisFormat)) { basisFormat = hasAlpha ? BASIS_FORMAT.cTFRGBA32 : BASIS_FORMAT.cTFRGB565; } } if (!basisFile.startTranscoding()) { basisFile.close(); basisFile.delete(); throw new Error(`Failed to start transcoding url=${url}`); } let i; const levelData = []; for(let mip = 0; mip < levels; ++mip){ const dstSize = basisFile.getImageTranscodedSizeInBytes(mip, 0, 0, basisFormat); const dst = new Uint8Array(dstSize); if (!basisFile.transcodeImage(dst, mip, 0, 0, basisFormat, 0, -1, -1)) { basisFile.close(); basisFile.delete(); throw new Error(`Failed to transcode image url=${url}`); } const is16BitFormat = basisFormat === BASIS_FORMAT.cTFRGB565 || basisFormat === BASIS_FORMAT.cTFRGBA4444; levelData.push(is16BitFormat ? new Uint16Array(dst.buffer) : dst); } basisFile.close(); basisFile.delete(); if (unswizzle) { basisFormat = BASIS_FORMAT.cTFRGB565; for(i = 0; i < levelData.length; ++i){ levelData[i] = pack565(unswizzleGGGR(levelData[i])); } } return { format: basisToEngineMapping(basisFormat, options.deviceDetails), width: width, height: height, levels: levelData, cubemap: false, transcodeTime: performanceNow() - funcStart, url: url, unswizzledGGGR: unswizzle }; }; const transcodeBasis = (url, data, options)=>{ const funcStart = performanceNow(); const basisFile = new basis.BasisFile(new Uint8Array(data)); const width = basisFile.getImageWidth(0, 0); const height = basisFile.getImageHeight(0, 0); const images = basisFile.getNumImages(); const levels = basisFile.getNumLevels(0); const hasAlpha = !!basisFile.getHasAlpha(); const isUASTC = basisFile.isUASTC && basisFile.isUASTC(); if (!width || !height || !images || !levels) { basisFile.close(); basisFile.delete(); throw new Error(`Invalid image dimensions url=${url} width=${width} height=${height} images=${images} levels=${levels}`); } const format = chooseTargetFormat(options.deviceDetails, hasAlpha, isUASTC); const unswizzle = !!options.isGGGR && format === 'pvr'; let basisFormat; if (unswizzle) { basisFormat = BASIS_FORMAT.cTFRGBA32; } else { basisFormat = hasAlpha ? alphaMapping[format] : opaqueMapping[format]; if (!dimensionsValid(width, height, basisFormat)) { basisFormat = hasAlpha ? BASIS_FORMAT.cTFRGBA32 : BASIS_FORMAT.cTFRGB565; } } if (!basisFile.startTranscoding()) { basisFile.close(); basisFile.delete(); throw new Error(`Failed to start transcoding url=${url}`); } let i; const levelData = []; for(let mip = 0; mip < levels; ++mip){ const dstSize = basisFile.getImageTranscodedSizeInBytes(0, mip, basisFormat); const dst = new Uint8Array(dstSize); if (!basisFile.transcodeImage(dst, 0, mip, basisFormat, 0, 0)) { if (mip === levels - 1 && dstSize === levelData[mip - 1].buffer.byteLength) { dst.set(new Uint8Array(levelData[mip - 1].buffer)); console.warn(`Failed to transcode last mipmap level, using previous level instead url=${url}`); } else { basisFile.close(); basisFile.delete(); throw new Error(`Failed to transcode image url=${url}`); } } const is16BitFormat = basisFormat === BASIS_FORMAT.cTFRGB565 || basisFormat === BASIS_FORMAT.cTFRGBA4444; levelData.push(is16BitFormat ? new Uint16Array(dst.buffer) : dst); } basisFile.close(); basisFile.delete(); if (unswizzle) { basisFormat = BASIS_FORMAT.cTFRGB565; for(i = 0; i < levelData.length; ++i){ levelData[i] = pack565(unswizzleGGGR(levelData[i])); } } return { format: basisToEngineMapping(basisFormat, options.deviceDetails), width: width, height: height, levels: levelData, cubemap: false, transcodeTime: performanceNow() - funcStart, url: url, unswizzledGGGR: unswizzle }; }; const transcode = (url, data, options)=>{ return options.isKTX2 ? transcodeKTX2(url, data, options) : transcodeBasis(url, data, options); }; const workerTranscode = (url, data, options)=>{ try { const result = transcode(url, data, options); result.levels = result.levels.map((v)=>v.buffer); self.postMessage({ url: url, data: result }, result.levels); } catch (err) { self.postMessage({ url: url, err: err }, null); } }; const workerInit = (config, callback)=>{ const instantiateWasmFunc = (imports, successCallback)=>{ WebAssembly.instantiate(config.module, imports).then((result)=>{ successCallback(result); }).catch((reason)=>{ console.error(`instantiate failed + ${reason}`); }); return {}; }; self.BASIS(config.module ? { instantiateWasm: instantiateWasmFunc } : null).then((instance)=>{ instance.initializeBasis(); basis = instance; rgbPriority = config.rgbPriority; rgbaPriority = config.rgbaPriority; callback(null); }); }; const queue = []; self.onmessage = (message)=>{ const data = message.data; switch(data.type){ case 'init': workerInit(data.config, ()=>{ for(let i = 0; i < queue.length; ++i){ workerTranscode(queue[i].url, queue[i].data, queue[i].options); } queue.length = 0; }); break; case 'transcode': if (basis) { workerTranscode(data.url, data.data, data.options); } else { queue.push(data); } break; } }; } const getCompressionFormats = (device)=>{ return { astc: !!device.extCompressedTextureASTC, atc: !!device.extCompressedTextureATC, dxt: !!device.extCompressedTextureS3TC, etc1: !!device.extCompressedTextureETC1, etc2: !!device.extCompressedTextureETC, pvr: !!device.extCompressedTexturePVRTC }; }; const prepareWorkerModules = (config, callback)=>{ const getWorkerBlob = (basisCode)=>{ const code = [ '/* basis */', basisCode, '', `(${BasisWorker.toString()})()\n\n` ].join('\n'); return new Blob([ code ], { type: "application/javascript" }); }; const wasmSupported = ()=>{ try { if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') { const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); if (module instanceof WebAssembly.Module) { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } } catch (e) {} return false; }; const sendResponse = (basisCode, module)=>{ callback(null, { workerUrl: URL.createObjectURL(getWorkerBlob(basisCode)), module: module, rgbPriority: config.rgbPriority, rgbaPriority: config.rgbaPriority }); }; const options = { cache: true, responseType: 'text', retry: config.maxRetries > 0, maxRetries: config.maxRetries }; if (config.glueUrl && config.wasmUrl && wasmSupported()) { let basisCode = null; let module = null; http.get(config.glueUrl, options, (err, response)=>{ if (err) { callback(err); } else { if (module) { sendResponse(response, module); } else { basisCode = response; } } }); const fetchPromise = fetch(config.wasmUrl); const compileManual = ()=>{ fetchPromise.then((result)=>result.arrayBuffer()).then((buffer)=>WebAssembly.compile(buffer)).then((module_)=>{ if (basisCode) { sendResponse(basisCode, module_); } else { module = module_; } }).catch((err)=>{ callback(err, null); }); }; if (WebAssembly.compileStreaming) { WebAssembly.compileStreaming(fetchPromise).then((module_)=>{ if (basisCode) { sendResponse(basisCode, module_); } else { module = module_; } }).catch((err)=>{ compileManual(); }); } else { compileManual(); } } else { http.get(config.fallbackUrl, options, (err, response)=>{ if (err) { callback(err, null); } else { sendResponse(response, null); } }); } }; class BasisQueue { enqueueJob(url, data, callback, options) { if (this.callbacks.hasOwnProperty(url)) { this.callbacks[url].push(callback); } else { this.callbacks[url] = [ callback ]; const job = { url: url, data: data, options: options }; if (this.clients.length > 0) { this.clients.shift().run(job); } else { this.queue.push(job); } } } enqueueClient(client) { if (this.queue.length > 0) { client.run(this.queue.shift()); } else { this.clients.push(client); } } handleResponse(url, err, data) { const callback = this.callbacks[url]; if (err) { for(let i = 0; i < callback.length; ++i){ callback[i](err); } } else { if (data.format === PIXELFORMAT_RGB565 || data.format === PIXELFORMAT_RGBA4) { data.levels = data.levels.map((v)=>{ return new Uint16Array(v); }); } else { data.levels = data.levels.map((v)=>{ return new Uint8Array(v); }); } for(let i = 0; i < callback.length; ++i){ callback[i](null, data); } } delete this.callbacks[url]; } constructor(){ this.callbacks = {}; this.queue = []; this.clients = []; } } class BasisClient { run(job) { const transfer = []; if (job.data instanceof ArrayBuffer) { transfer.push(job.data); } this.worker.postMessage({ type: 'transcode', url: job.url, format: job.format, data: job.data, options: job.options }, transfer); if (this.eager) { this.queue.enqueueClient(this); } } constructor(queue, config, eager){ this.queue = queue; this.worker = new Worker(config.workerUrl); this.worker.addEventListener('message', (message)=>{ const data = message.data; this.queue.handleResponse(data.url, data.err, data.data); if (!this.eager) { this.queue.enqueueClient(this); } }); this.worker.postMessage({ type: 'init', config: config }); this.eager = eager; } } const defaultNumWorkers = 1; const defaultRgbPriority = [ 'etc2', 'etc1', 'astc', 'dxt', 'pvr', 'atc' ]; const defaultRgbaPriority = [ 'astc', 'dxt', 'etc2', 'pvr', 'atc' ]; const defaultMaxRetries = 5; const queue = new BasisQueue(); let lazyConfig = null; let initializing = false; function basisInitialize(config) { if (initializing) { return; } if (!config) { config = lazyConfig || {}; } else if (config.lazyInit) { lazyConfig = config; return; } if (!config.glueUrl || !config.wasmUrl || !config.fallbackUrl) { const moduleConfig = WasmModule.getConfig('BASIS'); if (moduleConfig) { config = { glueUrl: moduleConfig.glueUrl, wasmUrl: moduleConfig.wasmUrl, fallbackUrl: moduleConfig.fallbackUrl, numWorkers: moduleConfig.numWorkers }; } } if (config.glueUrl || config.wasmUrl || config.fallbackUrl) { initializing = true; const numWorkers = Math.max(1, Math.min(16, config.numWorkers || defaultNumWorkers)); const eagerWorkers = config.numWorkers === 1 || (config.hasOwnProperty('eagerWorkers') ? config.eagerWorkers : true); config.rgbPriority = config.rgbPriority || defaultRgbPriority; config.rgbaPriority = config.rgbaPriority || defaultRgbaPriority; config.maxRetries = config.hasOwnProperty('maxRetries') ? config.maxRetries : defaultMaxRetries; prepareWorkerModules(config, (err, clientConfig)=>{ if (err) { console.error(`failed to initialize basis worker: ${err}`); } else { for(let i = 0; i < numWorkers; ++i){ queue.enqueueClient(new BasisClient(queue, clientConfig, eagerWorkers)); } } }); } } let deviceDetails = null; function basisTranscode(device, url, data, callback, options) { basisInitialize(); if (!deviceDetails) { deviceDetails = { formats: getCompressionFormats(device) }; } queue.enqueueJob(url, data, callback, { deviceDetails: deviceDetails, isGGGR: !!options?.isGGGR, isKTX2: !!options?.isKTX2 }); return initializing; } class TextureParser { load(url, callback, asset) { throw new Error('not implemented'); } open(url, data, device) { throw new Error('not implemented'); } } class BasisParser extends TextureParser { load(url, callback, asset) { const device = this.device; const transcode = (data)=>{ const basisModuleFound = basisTranscode(device, url.load, data, callback, { isGGGR: (asset?.file?.variants?.basis?.opt & 8) !== 0 }); if (!basisModuleFound) { callback(`Basis module not found. Asset [${asset.name}](${asset.getFileUrl()}) basis texture variant will not be loaded.`); } }; Asset.fetchArrayBuffer(url.load, (err, result)=>{ if (err) { callback(err); } else { transcode(result); } }, asset, this.maxRetries); } open(url, data, device, textureOptions = {}) { const format = textureOptions.srgb ? pixelFormatLinearToGamma(data.format) : data.format; const texture = new Texture(device, { name: url, addressU: data.cubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, addressV: data.cubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, width: data.width, height: data.height, format: format, cubemap: data.cubemap, levels: data.levels, ...textureOptions }); texture.upload(); return texture; } constructor(registry, device){ super(); this.device = device; this.maxRetries = 0; } } class ImgParser extends TextureParser { load(url, callback, asset) { const hasContents = !!asset?.file?.contents; if (hasContents) { if (this.device.supportsImageBitmap) { this._loadImageBitmapFromBlob(new Blob([ asset.file.contents ]), callback); return; } url = { load: URL.createObjectURL(new Blob([ asset.file.contents ])), original: url.original }; } const handler = (err, result)=>{ if (hasContents) { URL.revokeObjectURL(url.load); } callback(err, result); }; let crossOrigin; if (asset && asset.options && asset.options.hasOwnProperty('crossOrigin')) { crossOrigin = asset.options.crossOrigin; } else if (ABSOLUTE_URL.test(url.load)) { crossOrigin = this.crossOrigin; } if (this.device.supportsImageBitmap) { this._loadImageBitmap(url.load, url.original, crossOrigin, handler, asset); } else { this._loadImage(url.load, url.original, crossOrigin, handler, asset); } } open(url, data, device, textureOptions = {}) { const texture = new Texture(device, { name: url, width: data.width, height: data.height, format: textureOptions.srgb ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8, ...textureOptions }); texture.setSource(data); return texture; } _loadImage(url, originalUrl, crossOrigin, callback, asset) { const image = new Image(); if (crossOrigin) { image.crossOrigin = crossOrigin; } let retries = 0; const maxRetries = this.maxRetries; let retryTimeout; const dummySize = 1024 * 1024; asset?.fire('progress', 0, dummySize); image.onload = function() { asset?.fire('progress', dummySize, dummySize); callback(null, image); }; image.onerror = function() { if (retryTimeout) return; if (maxRetries > 0 && ++retries <= maxRetries) { const retryDelay = Math.pow(2, retries) * 100; console.log(`Error loading Texture from: '${originalUrl}' - Retrying in ${retryDelay}ms...`); const idx = url.indexOf('?'); const separator = idx >= 0 ? '&' : '?'; retryTimeout = setTimeout(()=>{ image.src = `${url + separator}retry=${Date.now()}`; retryTimeout = null; }, retryDelay); } else { callback(`Error loading Texture from: '${originalUrl}'`); } }; image.src = url; } _loadImageBitmap(url, originalUrl, crossOrigin, callback, asset) { const options = { cache: true, responseType: 'blob', retry: this.maxRetries > 0, maxRetries: this.maxRetries, progress: asset }; http.get(url, options, (err, blob)=>{ if (err) { callback(err); } else { this._loadImageBitmapFromBlob(blob, callback); } }); } _loadImageBitmapFromBlob(blob, callback) { createImageBitmap(blob, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' }).then((imageBitmap)=>callback(null, imageBitmap)).catch((e)=>callback(e)); } constructor(registry, device){ super(); this.crossOrigin = registry.prefix ? 'anonymous' : null; this.maxRetries = 0; this.device = device; } } const IDENTIFIER = [ 0x58544BAB, 0xBB313120, 0x0A1A0A0D ]; const KNOWN_FORMATS = { 0x83F0: PIXELFORMAT_DXT1, 0x83F2: PIXELFORMAT_DXT3, 0x83F3: PIXELFORMAT_DXT5, 0x8D64: PIXELFORMAT_ETC1, 0x9274: PIXELFORMAT_ETC2_RGB, 0x9278: PIXELFORMAT_ETC2_RGBA, 0x8C00: PIXELFORMAT_PVRTC_4BPP_RGB_1, 0x8C01: PIXELFORMAT_PVRTC_2BPP_RGB_1, 0x8C02: PIXELFORMAT_PVRTC_4BPP_RGBA_1, 0x8C03: PIXELFORMAT_PVRTC_2BPP_RGBA_1, 0x8051: PIXELFORMAT_RGB8, 0x8058: PIXELFORMAT_RGBA8, 0x8C41: PIXELFORMAT_SRGB8, 0x8C43: PIXELFORMAT_SRGBA8, 0x8C3A: PIXELFORMAT_111110F, 0x881B: PIXELFORMAT_RGB16F, 0x881A: PIXELFORMAT_RGBA16F }; function createContainer(pixelFormat, buffer, byteOffset, byteSize) { return pixelFormat === PIXELFORMAT_111110F ? new Uint32Array(buffer, byteOffset, byteSize / 4) : new Uint8Array(buffer, byteOffset, byteSize); } class KtxParser extends TextureParser { load(url, callback, asset) { Asset.fetchArrayBuffer(url.load, callback, asset, this.maxRetries); } open(url, data, device, textureOptions = {}) { const textureData = this.parse(data); if (!textureData) { return null; } const format = textureOptions.srgb ? pixelFormatLinearToGamma(textureData.format) : textureData.format; const texture = new Texture(device, { name: url, addressU: textureData.cubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, addressV: textureData.cubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, width: textureData.width, height: textureData.height, format: format, cubemap: textureData.cubemap, levels: textureData.levels, ...textureOptions }); texture.upload(); return texture; } parse(data) { const dataU32 = new Uint32Array(data); if (IDENTIFIER[0] !== dataU32[0] || IDENTIFIER[1] !== dataU32[1] || IDENTIFIER[2] !== dataU32[2]) { return null; } const header = { endianness: dataU32[3], glType: dataU32[4], glTypeSize: dataU32[5], glFormat: dataU32[6], glInternalFormat: dataU32[7], glBaseInternalFormat: dataU32[8], pixelWidth: dataU32[9], pixelHeight: dataU32[10], pixelDepth: dataU32[11], numberOfArrayElements: dataU32[12], numberOfFaces: dataU32[13], numberOfMipmapLevels: dataU32[14], bytesOfKeyValueData: dataU32[15] }; if (header.pixelDepth > 1) { return null; } if (header.numberOfArrayElements !== 0) { return null; } const format = KNOWN_FORMATS[header.glInternalFormat]; if (format === undefined) { return null; } let offset = 16 + header.bytesOfKeyValueData / 4; const isCubemap = header.numberOfFaces > 1; const levels = []; for(let mipmapLevel = 0; mipmapLevel < (header.numberOfMipmapLevels || 1); mipmapLevel++){ const imageSizeInBytes = dataU32[offset++]; if (isCubemap) { levels.push([]); } const target = isCubemap ? levels[mipmapLevel] : levels; for(let face = 0; face < (isCubemap ? 6 : 1); ++face){ target.push(createContainer(format, data, offset * 4, imageSizeInBytes)); offset += imageSizeInBytes + 3 >> 2; } } return { format: format, width: header.pixelWidth, height: header.pixelHeight, levels: levels, cubemap: isCubemap }; } constructor(registry){ super(); this.maxRetries = 0; } } const KHRConstants = { KHR_DF_MODEL_UASTC: 166 }; class Ktx2Parser extends TextureParser { load(url, callback, asset) { Asset.fetchArrayBuffer(url.load, (err, result)=>{ if (err) { callback(err, result); } else { this.parse(result, url, callback, asset); } }, asset, this.maxRetries); } open(url, data, device, textureOptions = {}) { const format = textureOptions.srgb ? pixelFormatLinearToGamma(data.format) : data.format; const texture = new Texture(device, { name: url, addressU: data.cubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, addressV: data.cubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, width: data.width, height: data.height, format: format, cubemap: data.cubemap, levels: data.levels, ...textureOptions }); texture.upload(); return texture; } parse(arraybuffer, url, callback, asset) { const rs = new ReadStream(arraybuffer); const magic = [ rs.readU32be(), rs.readU32be(), rs.readU32be() ]; if (magic[0] !== 0xAB4B5458 || magic[1] !== 0x203230BB || magic[2] !== 0x0D0A1A0A) { return null; } const header = { vkFormat: rs.readU32(), typeSize: rs.readU32(), pixelWidth: rs.readU32(), pixelHeight: rs.readU32(), pixelDepth: rs.readU32(), layerCount: rs.readU32(), faceCount: rs.readU32(), levelCount: rs.readU32(), supercompressionScheme: rs.readU32() }; const index = { dfdByteOffset: rs.readU32(), dfdByteLength: rs.readU32(), kvdByteOffset: rs.readU32(), kvdByteLength: rs.readU32(), sgdByteOffset: rs.readU64(), sgdByteLength: rs.readU64() }; const levels = []; for(let i = 0; i < Math.max(1, header.levelCount); ++i){ levels.push({ byteOffset: rs.readU64(), byteLength: rs.readU64(), uncompressedByteLength: rs.readU64() }); } const dfdTotalSize = rs.readU32(); if (dfdTotalSize !== index.kvdByteOffset - index.dfdByteOffset) { return null; } rs.skip(8); const colorModel = rs.readU8(); rs.skip(index.dfdByteLength - 9); rs.skip(index.kvdByteLength); if (header.supercompressionScheme === 1 || colorModel === KHRConstants.KHR_DF_MODEL_UASTC) { const basisModuleFound = basisTranscode(this.device, url.load, arraybuffer, callback, { isGGGR: (asset?.file?.variants?.basis?.opt & 8) !== 0, isKTX2: true }); if (!basisModuleFound) { callback(`Basis module not found. Asset [${asset.name}](${asset.getFileUrl()}) basis texture variant will not be loaded.`); } } else { callback('unsupported KTX2 pixel format'); } } constructor(registry, device){ super(); this.maxRetries = 0; this.device = device; } } class DdsParser extends TextureParser { load(url, callback, asset) { Asset.fetchArrayBuffer(url.load, callback, asset, this.maxRetries); } open(url, data, device, textureOptions = {}) { const header = new Uint32Array(data, 0, 128 / 4); const width = header[4]; const height = header[3]; const mips = Math.max(header[7], 1); const isFourCc = header[20] === 4; const fcc = header[21]; const bpp = header[22]; const isCubemap = header[28] === 65024; const FCC_DXT1 = 827611204; const FCC_DXT5 = 894720068; const FCC_FP16 = 113; const FCC_FP32 = 116; const FCC_ETC1 = 826496069; const FCC_PVRTC_2BPP_RGB_1 = 825438800; const FCC_PVRTC_2BPP_RGBA_1 = 825504336; const FCC_PVRTC_4BPP_RGB_1 = 825439312; const FCC_PVRTC_4BPP_RGBA_1 = 825504848; let compressed = false; let etc1 = false; let pvrtc2 = false; let pvrtc4 = false; let format = null; let componentSize = 1; let texture; if (isFourCc) { if (fcc === FCC_DXT1) { format = PIXELFORMAT_DXT1; compressed = true; } else if (fcc === FCC_DXT5) { format = PIXELFORMAT_DXT5; compressed = true; } else if (fcc === FCC_FP16) { format = PIXELFORMAT_RGBA16F; componentSize = 2; } else if (fcc === FCC_FP32) { format = PIXELFORMAT_RGBA32F; componentSize = 4; } else if (fcc === FCC_ETC1) { format = PIXELFORMAT_ETC1; compressed = true; etc1 = true; } else if (fcc === FCC_PVRTC_2BPP_RGB_1 || fcc === FCC_PVRTC_2BPP_RGBA_1) { format = fcc === FCC_PVRTC_2BPP_RGB_1 ? PIXELFORMAT_PVRTC_2BPP_RGB_1 : PIXELFORMAT_PVRTC_2BPP_RGBA_1; compressed = true; pvrtc2 = true; } else if (fcc === FCC_PVRTC_4BPP_RGB_1 || fcc === FCC_PVRTC_4BPP_RGBA_1) { format = fcc === FCC_PVRTC_4BPP_RGB_1 ? PIXELFORMAT_PVRTC_4BPP_RGB_1 : PIXELFORMAT_PVRTC_4BPP_RGBA_1; compressed = true; pvrtc4 = true; } } else { if (bpp === 32) { format = PIXELFORMAT_RGBA8; } } if (!format) { texture = new Texture(device, { width: 4, height: 4, format: PIXELFORMAT_RGB8, name: 'dds-legacy-empty' }); return texture; } texture = new Texture(device, { name: url, addressU: isCubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, addressV: isCubemap ? ADDRESS_CLAMP_TO_EDGE : ADDRESS_REPEAT, width: width, height: height, format: format, cubemap: isCubemap, mipmaps: mips > 1, ...textureOptions }); let offset = 128; const faces = isCubemap ? 6 : 1; let mipSize; const DXT_BLOCK_WIDTH = 4; const DXT_BLOCK_HEIGHT = 4; const blockSize = fcc === FCC_DXT1 ? 8 : 16; let numBlocksAcross, numBlocksDown, numBlocks; for(let face = 0; face < faces; face++){ let mipWidth = width; let mipHeight = height; for(let i = 0; i < mips; i++){ if (compressed) { if (etc1) { mipSize = Math.floor((mipWidth + 3) / 4) * Math.floor((mipHeight + 3) / 4) * 8; } else if (pvrtc2) { mipSize = Math.max(mipWidth, 16) * Math.max(mipHeight, 8) / 4; } else if (pvrtc4) { mipSize = Math.max(mipWidth, 8) * Math.max(mipHeight, 8) / 2; } else { numBlocksAcross = Math.floor((mipWidth + DXT_BLOCK_WIDTH - 1) / DXT_BLOCK_WIDTH); numBlocksDown = Math.floor((mipHeight + DXT_BLOCK_HEIGHT - 1) / DXT_BLOCK_HEIGHT); numBlocks = numBlocksAcross * numBlocksDown; mipSize = numBlocks * blockSize; } } else { mipSize = mipWidth * mipHeight * 4; } const mipBuff = format === PIXELFORMAT_RGBA32F ? new Float32Array(data, offset, mipSize) : format === PIXELFORMAT_RGBA16F ? new Uint16Array(data, offset, mipSize) : new Uint8Array(data, offset, mipSize); if (!isCubemap) { texture._levels[i] = mipBuff; } else { if (!texture._levels[i]) texture._levels[i] = []; texture._levels[i][face] = mipBuff; } offset += mipSize * componentSize; mipWidth = Math.max(mipWidth * 0.5, 1); mipHeight = Math.max(mipHeight * 0.5, 1); } } texture.upload(); return texture; } constructor(registry){ super(); this.maxRetries = 0; } } class HdrParser extends TextureParser { load(url, callback, asset) { Asset.fetchArrayBuffer(url.load, callback, asset, this.maxRetries); if (asset.data && !asset.data.type) { asset.data.type = TEXTURETYPE_RGBE; } } open(url, data, device, textureOptions = {}) { const textureData = this.parse(data); if (!textureData) { return null; } const texture = new Texture(device, { name: url, addressU: ADDRESS_REPEAT, addressV: ADDRESS_CLAMP_TO_EDGE, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, width: textureData.width, height: textureData.height, levels: textureData.levels, format: PIXELFORMAT_RGBA8, type: TEXTURETYPE_RGBE, mipmaps: false, ...textureOptions }); texture.upload(); return texture; } parse(data) { const readStream = new ReadStream(data); const magic = readStream.readLine(); if (!magic.startsWith('#?RADIANCE')) { return null; } const variables = {}; while(true){ const line = readStream.readLine(); if (line.length === 0) { break; } else { const parts = line.split('='); if (parts.length === 2) { variables[parts[0]] = parts[1]; } } } if (!variables.hasOwnProperty('FORMAT')) { return null; } const resolution = readStream.readLine().split(' '); if (resolution.length !== 4) { return null; } const height = parseInt(resolution[1], 10); const width = parseInt(resolution[3], 10); const pixels = this._readPixels(readStream, width, height, resolution[0] === '-Y'); if (!pixels) { return null; } return { width: width, height: height, levels: [ pixels ] }; } _readPixels(readStream, width, height, flipY) { if (width < 8 || width > 0x7fff) { return this._readPixelsFlat(readStream, width, height); } const rgbe = [ 0, 0, 0, 0 ]; readStream.readArray(rgbe); if (rgbe[0] !== 2 || rgbe[1] !== 2 || (rgbe[2] & 0x80) !== 0) { readStream.skip(-4); return this._readPixelsFlat(readStream, width, height); } const buffer = new ArrayBuffer(width * height * 4); const view = new Uint8Array(buffer); let scanstart = flipY ? 0 : width * 4 * (height - 1); let x, y, i, channel, count, value; for(y = 0; y < height; ++y){ if (y) { readStream.readArray(rgbe); } if ((rgbe[2] << 8) + rgbe[3] !== width) { return null; } for(channel = 0; channel < 4; ++channel){ x = 0; while(x < width){ count = readStream.readU8(); if (count > 128) { count -= 128; if (x + count > width) { return null; } value = readStream.readU8(); for(i = 0; i < count; ++i){ view[scanstart + channel + 4 * x++] = value; } } else { if (count === 0 || x + count > width) { return null; } for(i = 0; i < count; ++i){ view[scanstart + channel + 4 * x++] = readStream.readU8(); } } } } scanstart += width * 4 * (flipY ? 1 : -1); } return view; } _readPixelsFlat(readStream, width, height) { return readStream.remainingBytes === width * height * 4 ? new Uint8Array(readStream.arraybuffer, readStream.offset) : null; } constructor(registry){ super(); this.maxRetries = 0; } } const JSON_ADDRESS_MODE = { 'repeat': ADDRESS_REPEAT, 'clamp': ADDRESS_CLAMP_TO_EDGE, 'mirror': ADDRESS_MIRRORED_REPEAT }; const JSON_FILTER_MODE = { 'nearest': FILTER_NEAREST, 'linear': FILTER_LINEAR, 'nearest_mip_nearest': FILTER_NEAREST_MIPMAP_NEAREST, 'linear_mip_nearest': FILTER_LINEAR_MIPMAP_NEAREST, 'nearest_mip_linear': FILTER_NEAREST_MIPMAP_LINEAR, 'linear_mip_linear': FILTER_LINEAR_MIPMAP_LINEAR }; const JSON_TEXTURE_TYPE = { 'default': TEXTURETYPE_DEFAULT, 'rgbm': TEXTURETYPE_RGBM, 'rgbe': TEXTURETYPE_RGBE, 'rgbp': TEXTURETYPE_RGBP, 'swizzleGGGR': TEXTURETYPE_SWIZZLEGGGR }; const _completePartialMipmapChain = function(texture) { const requiredMipLevels = TextureUtils.calcMipLevelsCount(texture._width, texture._height); const isHtmlElement = function(object) { return object instanceof HTMLCanvasElement || object instanceof HTMLImageElement || object instanceof HTMLVideoElement; }; if (!(texture._format === PIXELFORMAT_RGBA8 || texture._format === PIXELFORMAT_RGBA32F) || texture._volume || texture._compressed || texture._levels.length === 1 || texture._levels.length === requiredMipLevels || isHtmlElement(texture._cubemap ? texture._levels[0][0] : texture._levels[0])) { return; } const downsample = function(width, height, data) { const sampledWidth = Math.max(1, width >> 1); const sampledHeight = Math.max(1, height >> 1); const sampledData = new data.constructor(sampledWidth * sampledHeight * 4); const xs = Math.floor(width / sampledWidth); const ys = Math.floor(height / sampledHeight); const xsys = xs * ys; for(let y = 0; y < sampledHeight; ++y){ for(let x = 0; x < sampledWidth; ++x){ for(let e = 0; e < 4; ++e){ let sum = 0; for(let sy = 0; sy < ys; ++sy){ for(let sx = 0; sx < xs; ++sx){ sum += data[(x * xs + sx + (y * ys + sy) * width) * 4 + e]; } } sampledData[(x + y * sampledWidth) * 4 + e] = sum / xsys; } } } return sampledData; }; for(let level = texture._levels.length; level < requiredMipLevels; ++level){ const width = Math.max(1, texture._width >> level - 1); const height = Math.max(1, texture._height >> level - 1); if (texture._cubemap) { const mips = []; for(let face = 0; face < 6; ++face){ mips.push(downsample(width, height, texture._levels[level - 1][face])); } texture._levels.push(mips); } else { texture._levels.push(downsample(width, height, texture._levels[level - 1])); } } texture._levelsUpdated = texture._cubemap ? [ [ true, true, true, true, true, true ] ] : [ true ]; }; class TextureHandler extends ResourceHandler { set crossOrigin(value) { this.imgParser.crossOrigin = value; } get crossOrigin() { return this.imgParser.crossOrigin; } set maxRetries(value) { this.imgParser.maxRetries = value; for(const parser in this.parsers){ if (this.parsers.hasOwnProperty(parser)) { this.parsers[parser].maxRetries = value; } } } get maxRetries() { return this.imgParser.maxRetries; } _getUrlWithoutParams(url) { return url.indexOf('?') >= 0 ? url.split('?')[0] : url; } _getParser(url) { const ext = path.getExtension(this._getUrlWithoutParams(url)).toLowerCase().replace('.', ''); return this.parsers[ext] || this.imgParser; } _getTextureOptions(asset) { const options = {}; if (asset) { if (asset.name?.length > 0) { options.name = asset.name; } const assetData = asset.data; if (assetData.hasOwnProperty('minfilter')) { options.minFilter = JSON_FILTER_MODE[assetData.minfilter]; } if (assetData.hasOwnProperty('magfilter')) { options.magFilter = JSON_FILTER_MODE[assetData.magfilter]; } if (assetData.hasOwnProperty('addressu')) { options.addressU = JSON_ADDRESS_MODE[assetData.addressu]; } if (assetData.hasOwnProperty('addressv')) { options.addressV = JSON_ADDRESS_MODE[assetData.addressv]; } if (assetData.hasOwnProperty('mipmaps')) { options.mipmaps = assetData.mipmaps; } if (assetData.hasOwnProperty('anisotropy')) { options.anisotropy = assetData.anisotropy; } if (assetData.hasOwnProperty('flipY')) { options.flipY = !!assetData.flipY; } if (assetData.hasOwnProperty('srgb')) { options.srgb = !!assetData.srgb; } options.type = TEXTURETYPE_DEFAULT; if (assetData.hasOwnProperty('type')) { options.type = JSON_TEXTURE_TYPE[assetData.type]; } else if (assetData.hasOwnProperty('rgbm') && assetData.rgbm) { options.type = TEXTURETYPE_RGBM; } else if (asset.file && (asset.file.opt & 8) !== 0) { options.type = TEXTURETYPE_SWIZZLEGGGR; } } return options; } load(url, callback, asset) { if (typeof url === 'string') { url = { load: url, original: url }; } this._getParser(url.original).load(url, callback, asset); } open(url, data, asset) { if (!url) { return undefined; } const textureOptions = this._getTextureOptions(asset); let texture = this._getParser(url).open(url, data, this._device, textureOptions); if (texture === null) { texture = new Texture(this._device, { width: 4, height: 4, format: PIXELFORMAT_RGB8 }); } else { _completePartialMipmapChain(texture); if (data.unswizzledGGGR) { asset.file.variants.basis.opt &= -9; } } return texture; } patch(asset, assets) { const texture = asset.resource; if (!texture) { return; } const options = this._getTextureOptions(asset); for (const key of Object.keys(options)){ texture[key] = options[key]; } } constructor(app){ super(app, 'texture'); const assets = app.assets; const device = app.graphicsDevice; this._device = device; this._assets = assets; this.imgParser = new ImgParser(assets, device); this.parsers = { dds: new DdsParser(assets), ktx: new KtxParser(assets), ktx2: new Ktx2Parser(assets, device), basis: new BasisParser(assets, device), hdr: new HdrParser(assets) }; } } const XRTYPE_INLINE = 'inline'; const XRTYPE_VR = 'immersive-vr'; const XRTYPE_AR = 'immersive-ar'; const XRSPACE_VIEWER = 'viewer'; const XRHAND_LEFT = 'left'; const XRDEPTHSENSINGUSAGE_CPU = 'cpu-optimized'; const XRDEPTHSENSINGUSAGE_GPU = 'gpu-optimized'; const XRDEPTHSENSINGFORMAT_L8A8 = 'luminance-alpha'; const XRDEPTHSENSINGFORMAT_R16U = 'unsigned-short'; const XRDEPTHSENSINGFORMAT_F32 = 'float32'; class XrDomOverlay { get supported() { return this._supported; } get available() { return this._supported && this._manager.active && this._manager._session.domOverlayState !== null; } get state() { if (!this._supported || !this._manager.active || !this._manager._session.domOverlayState) { return null; } return this._manager._session.domOverlayState.type; } set root(value) { if (!this._supported || this._manager.active) { return; } this._root = value; } get root() { return this._root; } constructor(manager){ this._supported = platform.browser && !!window.XRDOMOverlayState; this._root = null; this._manager = manager; } } const poolVec3 = []; const poolQuat = []; class XrHitTestSource extends EventHandler { remove() { if (!this._xrHitTestSource) { return; } const sources = this.manager.hitTest.sources; const ind = sources.indexOf(this); if (ind !== -1) sources.splice(ind, 1); this.onStop(); } onStop() { this._xrHitTestSource.cancel(); this._xrHitTestSource = null; this.fire('remove'); this.manager.hitTest.fire('remove', this); } update(frame) { if (this._transient) { const transientResults = frame.getHitTestResultsForTransientInput(this._xrHitTestSource); for(let i = 0; i < transientResults.length; i++){ const transientResult = transientResults[i]; if (!transientResult.results.length) { continue; } let inputSource; if (transientResult.inputSource) { inputSource = this.manager.input._getByInputSource(transientResult.inputSource); } this.updateHitResults(transientResult.results, inputSource); } } else { const results = frame.getHitTestResults(this._xrHitTestSource); if (!results.length) { return; } this.updateHitResults(results); } } updateHitResults(results, inputSource) { if (this._inputSource && this._inputSource !== inputSource) { return; } const origin = poolVec3.pop() ?? new Vec3(); if (inputSource) { origin.copy(inputSource.getOrigin()); } else { origin.copy(this.manager.camera.getPosition()); } let candidateDistance = Infinity; let candidateHitTestResult = null; const position = poolVec3.pop() ?? new Vec3(); const rotation = poolQuat.pop() ?? new Quat(); for(let i = 0; i < results.length; i++){ const pose = results[i].getPose(this.manager._referenceSpace); const distance = origin.distance(pose.transform.position); if (distance >= candidateDistance) { continue; } candidateDistance = distance; candidateHitTestResult = results[i]; position.copy(pose.transform.position); rotation.copy(pose.transform.orientation); } this.fire('result', position, rotation, inputSource || this._inputSource, candidateHitTestResult); this.manager.hitTest.fire('result', this, position, rotation, inputSource || this._inputSource, candidateHitTestResult); poolVec3.push(origin); poolVec3.push(position); poolQuat.push(rotation); } constructor(manager, xrHitTestSource, transient, inputSource = null){ super(); this.manager = manager; this._xrHitTestSource = xrHitTestSource; this._transient = transient; this._inputSource = inputSource; } } XrHitTestSource.EVENT_REMOVE = 'remove'; XrHitTestSource.EVENT_RESULT = 'result'; class XrHitTest extends EventHandler { _onSessionStart() { if (this.manager.session.enabledFeatures) { const available = this.manager.session.enabledFeatures.indexOf('hit-test') !== -1; if (!available) return; this._available = available; this.fire('available'); } else if (!this._checkingAvailability) { this._checkingAvailability = true; this.manager.session.requestReferenceSpace(XRSPACE_VIEWER).then((referenceSpace)=>{ this.manager.session.requestHitTestSource({ space: referenceSpace }).then((hitTestSource)=>{ hitTestSource.cancel(); if (this.manager.active) { this._available = true; this.fire('available'); } }).catch(()=>{}); }).catch(()=>{}); } } _onSessionEnd() { if (!this._available) return; this._available = false; for(let i = 0; i < this.sources.length; i++){ this.sources[i].onStop(); } this.sources = []; this.fire('unavailable'); } start(options = {}) { if (!this._supported) { options.callback?.(new Error('XR HitTest is not supported'), null); return; } if (!this._available) { options.callback?.(new Error('XR HitTest is not available'), null); return; } if (!options.profile && !options.spaceType) { options.spaceType = XRSPACE_VIEWER; } let xrRay; const offsetRay = options.offsetRay; if (offsetRay) { const origin = new DOMPoint(offsetRay.origin.x, offsetRay.origin.y, offsetRay.origin.z, 1.0); const direction = new DOMPoint(offsetRay.direction.x, offsetRay.direction.y, offsetRay.direction.z, 0.0); xrRay = new XRRay(origin, direction); } const callback = options.callback; if (options.spaceType) { this.manager.session.requestReferenceSpace(options.spaceType).then((referenceSpace)=>{ if (!this.manager.session) { const err = new Error('XR Session is not started (2)'); if (callback) callback(err); this.fire('error', err); return; } this.manager.session.requestHitTestSource({ space: referenceSpace, entityTypes: options.entityTypes || undefined, offsetRay: xrRay }).then((xrHitTestSource)=>{ this._onHitTestSource(xrHitTestSource, false, options.inputSource, callback); }).catch((ex)=>{ if (callback) callback(ex); this.fire('error', ex); }); }).catch((ex)=>{ if (callback) callback(ex); this.fire('error', ex); }); } else { this.manager.session.requestHitTestSourceForTransientInput({ profile: options.profile, entityTypes: options.entityTypes || undefined, offsetRay: xrRay }).then((xrHitTestSource)=>{ this._onHitTestSource(xrHitTestSource, true, options.inputSource, callback); }).catch((ex)=>{ if (callback) callback(ex); this.fire('error', ex); }); } } _onHitTestSource(xrHitTestSource, transient, inputSource, callback) { if (!this.manager.session) { xrHitTestSource.cancel(); const err = new Error('XR Session is not started (3)'); if (callback) callback(err); this.fire('error', err); return; } const hitTestSource = new XrHitTestSource(this.manager, xrHitTestSource, transient, inputSource ?? null); this.sources.push(hitTestSource); if (callback) callback(null, hitTestSource); this.fire('add', hitTestSource); } update(frame) { if (!this._available) { return; } for(let i = 0; i < this.sources.length; i++){ this.sources[i].update(frame); } } get supported() { return this._supported; } get available() { return this._available; } constructor(manager){ super(), this._supported = platform.browser && !!(window.XRSession && window.XRSession.prototype.requestHitTestSource), this._available = false, this._checkingAvailability = false, this.sources = []; this.manager = manager; if (this._supported) { this.manager.on('start', this._onSessionStart, this); this.manager.on('end', this._onSessionEnd, this); } } } XrHitTest.EVENT_AVAILABLE = 'available'; XrHitTest.EVENT_UNAVAILABLE = 'unavailable'; XrHitTest.EVENT_ADD = 'add'; XrHitTest.EVENT_REMOVE = 'remove'; XrHitTest.EVENT_RESULT = 'result'; XrHitTest.EVENT_ERROR = 'error'; class XrTrackedImage extends EventHandler { get image() { return this._image; } set width(value) { this._width = value; } get width() { return this._width; } get trackable() { return this._trackable; } get tracking() { return this._tracking; } get emulated() { return this._emulated; } prepare() { if (this._bitmap) { return { image: this._bitmap, widthInMeters: this._width }; } return createImageBitmap(this._image).then((bitmap)=>{ this._bitmap = bitmap; return { image: this._bitmap, widthInMeters: this._width }; }); } destroy() { this._image = null; this._pose = null; if (this._bitmap) { this._bitmap.close(); this._bitmap = null; } } getPosition() { if (this._pose) this._position.copy(this._pose.transform.position); return this._position; } getRotation() { if (this._pose) this._rotation.copy(this._pose.transform.orientation); return this._rotation; } constructor(image, width){ super(), this._bitmap = null, this._measuredWidth = 0, this._trackable = false, this._tracking = false, this._emulated = false, this._pose = null, this._position = new Vec3(), this._rotation = new Quat(); this._image = image; this._width = width; } } XrTrackedImage.EVENT_TRACKED = 'tracked'; XrTrackedImage.EVENT_UNTRACKED = 'untracked'; class XrImageTracking extends EventHandler { add(image, width) { if (!this._supported || this._manager.active) return null; const trackedImage = new XrTrackedImage(image, width); this._images.push(trackedImage); return trackedImage; } remove(trackedImage) { if (this._manager.active) return; const ind = this._images.indexOf(trackedImage); if (ind !== -1) { trackedImage.destroy(); this._images.splice(ind, 1); } } _onSessionStart() { this._manager.session.getTrackedImageScores().then((images)=>{ this._available = true; for(let i = 0; i < images.length; i++){ this._images[i]._trackable = images[i] === 'trackable'; } }).catch((err)=>{ this._available = false; this.fire('error', err); }); } _onSessionEnd() { this._available = false; for(let i = 0; i < this._images.length; i++){ const image = this._images[i]; image._pose = null; image._measuredWidth = 0; if (image._tracking) { image._tracking = false; image.fire('untracked'); } } } prepareImages(callback) { if (this._images.length) { Promise.all(this._images.map((trackedImage)=>{ return trackedImage.prepare(); })).then((bitmaps)=>{ callback(null, bitmaps); }).catch((err)=>{ callback(err, null); }); } else { callback(null, null); } } update(frame) { if (!this._available) return; const results = frame.getImageTrackingResults(); const index = {}; for(let i = 0; i < results.length; i++){ index[results[i].index] = results[i]; const trackedImage = this._images[results[i].index]; trackedImage._emulated = results[i].trackingState === 'emulated'; trackedImage._measuredWidth = results[i].measuredWidthInMeters; trackedImage._pose = frame.getPose(results[i].imageSpace, this._manager._referenceSpace); } for(let i = 0; i < this._images.length; i++){ if (this._images[i]._tracking && !index[i]) { this._images[i]._tracking = false; this._images[i].fire('untracked'); } else if (!this._images[i]._tracking && index[i]) { this._images[i]._tracking = true; this._images[i].fire('tracked'); } } } get supported() { return this._supported; } get available() { return this._available; } get images() { return this._images; } constructor(manager){ super(), this._supported = platform.browser && !!window.XRImageTrackingResult, this._available = false, this._images = []; this._manager = manager; if (this._supported) { this._manager.on('start', this._onSessionStart, this); this._manager.on('end', this._onSessionEnd, this); } } } XrImageTracking.EVENT_ERROR = 'error'; class XrFinger { get index() { return this._index; } get hand() { return this._hand; } get joints() { return this._joints; } get tip() { return this._tip; } constructor(index, hand){ this._joints = []; this._tip = null; this._index = index; this._hand = hand; this._hand._fingers.push(this); } } const tipJointIds = platform.browser && window.XRHand ? [ 'thumb-tip', 'index-finger-tip', 'middle-finger-tip', 'ring-finger-tip', 'pinky-finger-tip' ] : []; const tipJointIdsIndex = {}; for(let i = 0; i < tipJointIds.length; i++){ tipJointIdsIndex[tipJointIds[i]] = true; } class XrJoint { update(pose) { this._dirtyLocal = true; this._radius = pose.radius; this._localPosition.copy(pose.transform.position); this._localRotation.copy(pose.transform.orientation); } _updateTransforms() { if (this._dirtyLocal) { this._dirtyLocal = false; this._localTransform.setTRS(this._localPosition, this._localRotation, Vec3.ONE); } const manager = this._hand._manager; const parent = manager.camera.parent; if (parent) { this._worldTransform.mul2(parent.getWorldTransform(), this._localTransform); } else { this._worldTransform.copy(this._localTransform); } } getPosition() { this._updateTransforms(); this._worldTransform.getTranslation(this._position); return this._position; } getRotation() { this._updateTransforms(); this._rotation.setFromMat4(this._worldTransform); return this._rotation; } get id() { return this._id; } get index() { return this._index; } get hand() { return this._hand; } get finger() { return this._finger; } get wrist() { return this._wrist; } get tip() { return this._tip; } get radius() { return this._radius || 0.005; } constructor(index, id, hand, finger = null){ this._radius = null; this._localTransform = new Mat4(); this._worldTransform = new Mat4(); this._localPosition = new Vec3(); this._localRotation = new Quat(); this._position = new Vec3(); this._rotation = new Quat(); this._dirtyLocal = true; this._index = index; this._id = id; this._hand = hand; this._finger = finger; this._wrist = id === 'wrist'; this._tip = this._finger && !!tipJointIdsIndex[id]; } } let fingerJointIds = []; const vecA = new Vec3(); const vecB = new Vec3(); const vecC = new Vec3(); if (platform.browser && window.XRHand) { fingerJointIds = [ [ 'thumb-metacarpal', 'thumb-phalanx-proximal', 'thumb-phalanx-distal', 'thumb-tip' ], [ 'index-finger-metacarpal', 'index-finger-phalanx-proximal', 'index-finger-phalanx-intermediate', 'index-finger-phalanx-distal', 'index-finger-tip' ], [ 'middle-finger-metacarpal', 'middle-finger-phalanx-proximal', 'middle-finger-phalanx-intermediate', 'middle-finger-phalanx-distal', 'middle-finger-tip' ], [ 'ring-finger-metacarpal', 'ring-finger-phalanx-proximal', 'ring-finger-phalanx-intermediate', 'ring-finger-phalanx-distal', 'ring-finger-tip' ], [ 'pinky-finger-metacarpal', 'pinky-finger-phalanx-proximal', 'pinky-finger-phalanx-intermediate', 'pinky-finger-phalanx-distal', 'pinky-finger-tip' ] ]; } class XrHand extends EventHandler { update(frame) { const xrInputSource = this._inputSource._xrInputSource; for(let j = 0; j < this._joints.length; j++){ const joint = this._joints[j]; const jointSpace = xrInputSource.hand.get(joint._id); if (jointSpace) { let pose; if (frame.session.visibilityState !== 'hidden') { pose = frame.getJointPose(jointSpace, this._manager._referenceSpace); } if (pose) { joint.update(pose); if (joint.wrist && !this._tracking) { this._tracking = true; this.fire('tracking'); } } else if (joint.wrist) { if (this._tracking) { this._tracking = false; this.fire('trackinglost'); } break; } } } const j1 = this._jointsById['thumb-metacarpal']; const j4 = this._jointsById['thumb-tip']; const j6 = this._jointsById['index-finger-phalanx-proximal']; const j9 = this._jointsById['index-finger-tip']; const j16 = this._jointsById['ring-finger-phalanx-proximal']; const j21 = this._jointsById['pinky-finger-phalanx-proximal']; if (j1 && j4 && j6 && j9 && j16 && j21) { this._inputSource._dirtyRay = true; this._inputSource._rayLocal.origin.lerp(j4._localPosition, j9._localPosition, 0.5); let jointL = j1; let jointR = j21; if (this._inputSource.handedness === XRHAND_LEFT) { const t = jointL; jointL = jointR; jointR = t; } vecA.sub2(jointL._localPosition, this._wrist._localPosition); vecB.sub2(jointR._localPosition, this._wrist._localPosition); vecC.cross(vecA, vecB).normalize(); vecA.lerp(j6._localPosition, j16._localPosition, 0.5); vecA.sub(this._wrist._localPosition).normalize(); this._inputSource._rayLocal.direction.lerp(vecC, vecA, 0.5).normalize(); } const squeezing = this._fingerIsClosed(1) && this._fingerIsClosed(2) && this._fingerIsClosed(3) && this._fingerIsClosed(4); if (squeezing) { if (!this._inputSource._squeezing) { this._inputSource._squeezing = true; this._inputSource.fire('squeezestart'); this._manager.input.fire('squeezestart', this._inputSource); } } else { if (this._inputSource._squeezing) { this._inputSource._squeezing = false; this._inputSource.fire('squeeze'); this._manager.input.fire('squeeze', this._inputSource); this._inputSource.fire('squeezeend'); this._manager.input.fire('squeezeend', this._inputSource); } } } _fingerIsClosed(index) { const finger = this._fingers[index]; vecA.sub2(finger.joints[0]._localPosition, finger.joints[1]._localPosition).normalize(); vecB.sub2(finger.joints[2]._localPosition, finger.joints[3]._localPosition).normalize(); return vecA.dot(vecB) < -0.8; } getJointById(id) { return this._jointsById[id] || null; } get fingers() { return this._fingers; } get joints() { return this._joints; } get tips() { return this._tips; } get wrist() { return this._wrist; } get tracking() { return this._tracking; } constructor(inputSource){ super(), this._tracking = false, this._fingers = [], this._joints = [], this._jointsById = {}, this._tips = [], this._wrist = null; const xrHand = inputSource._xrInputSource.hand; this._manager = inputSource._manager; this._inputSource = inputSource; if (xrHand.get('wrist')) { const joint = new XrJoint(0, 'wrist', this, null); this._wrist = joint; this._joints.push(joint); this._jointsById.wrist = joint; } for(let f = 0; f < fingerJointIds.length; f++){ const finger = new XrFinger(f, this); for(let j = 0; j < fingerJointIds[f].length; j++){ const jointId = fingerJointIds[f][j]; if (!xrHand.get(jointId)) continue; const joint = new XrJoint(j, jointId, this, finger); this._joints.push(joint); this._jointsById[jointId] = joint; if (joint.tip) { this._tips.push(joint); finger._tip = joint; } finger._joints.push(joint); } } } } XrHand.EVENT_TRACKING = 'tracking'; XrHand.EVENT_TRACKINGLOST = 'trackinglost'; const vec3A$1 = new Vec3(); const quat = new Quat(); let ids$1 = 0; class XrInputSource extends EventHandler { get id() { return this._id; } get inputSource() { return this._xrInputSource; } get targetRayMode() { return this._xrInputSource.targetRayMode; } get handedness() { return this._xrInputSource.handedness; } get profiles() { return this._xrInputSource.profiles; } get grip() { return this._grip; } get hand() { return this._hand; } get gamepad() { return this._xrInputSource.gamepad || null; } get selecting() { return this._selecting; } get squeezing() { return this._squeezing; } set elementInput(value) { if (this._elementInput === value) { return; } this._elementInput = value; if (!this._elementInput) { this._elementEntity = null; } } get elementInput() { return this._elementInput; } get elementEntity() { return this._elementEntity; } get hitTestSources() { return this._hitTestSources; } update(frame) { if (this._hand) { this._hand.update(frame); } else { const gripSpace = this._xrInputSource.gripSpace; if (gripSpace) { const gripPose = frame.getPose(gripSpace, this._manager._referenceSpace); if (gripPose) { if (!this._grip) { this._grip = true; this._localTransform = new Mat4(); this._worldTransform = new Mat4(); this._localPositionLast = new Vec3(); this._localPosition = new Vec3(); this._localRotation = new Quat(); this._linearVelocity = new Vec3(); } const timestamp = now(); const dt = (timestamp - this._velocitiesTimestamp) / 1000; this._velocitiesTimestamp = timestamp; this._dirtyLocal = true; this._localPositionLast.copy(this._localPosition); this._localPosition.copy(gripPose.transform.position); this._localRotation.copy(gripPose.transform.orientation); this._velocitiesAvailable = true; if (this._manager.input.velocitiesSupported && gripPose.linearVelocity) { this._linearVelocity.copy(gripPose.linearVelocity); } else if (dt > 0) { vec3A$1.sub2(this._localPosition, this._localPositionLast).divScalar(dt); this._linearVelocity.lerp(this._linearVelocity, vec3A$1, 0.15); } } else { this._velocitiesAvailable = false; } } const targetRayPose = frame.getPose(this._xrInputSource.targetRaySpace, this._manager._referenceSpace); if (targetRayPose) { this._dirtyRay = true; this._rayLocal.origin.copy(targetRayPose.transform.position); this._rayLocal.direction.set(0, 0, -1); quat.copy(targetRayPose.transform.orientation); quat.transformVector(this._rayLocal.direction, this._rayLocal.direction); } } } _updateTransforms() { if (this._dirtyLocal) { this._dirtyLocal = false; this._localTransform.setTRS(this._localPosition, this._localRotation, Vec3.ONE); } const parent = this._manager.camera.parent; if (parent) { this._worldTransform.mul2(parent.getWorldTransform(), this._localTransform); } else { this._worldTransform.copy(this._localTransform); } } _updateRayTransforms() { const dirty = this._dirtyRay; this._dirtyRay = false; const parent = this._manager.camera.parent; if (parent) { const parentTransform = this._manager.camera.parent.getWorldTransform(); parentTransform.getTranslation(this._position); this._rotation.setFromMat4(parentTransform); this._rotation.transformVector(this._rayLocal.origin, this._ray.origin); this._ray.origin.add(this._position); this._rotation.transformVector(this._rayLocal.direction, this._ray.direction); } else if (dirty) { this._ray.origin.copy(this._rayLocal.origin); this._ray.direction.copy(this._rayLocal.direction); } } getPosition() { if (!this._position) return null; this._updateTransforms(); this._worldTransform.getTranslation(this._position); return this._position; } getLocalPosition() { return this._localPosition; } getRotation() { if (!this._rotation) return null; this._updateTransforms(); this._rotation.setFromMat4(this._worldTransform); return this._rotation; } getLocalRotation() { return this._localRotation; } getLinearVelocity() { if (!this._velocitiesAvailable) { return null; } return this._linearVelocity; } getOrigin() { this._updateRayTransforms(); return this._ray.origin; } getDirection() { this._updateRayTransforms(); return this._ray.direction; } hitTestStart(options = {}) { options.inputSource = this; options.profile = this._xrInputSource.profiles[0]; const callback = options.callback; options.callback = (err, hitTestSource)=>{ if (hitTestSource) this.onHitTestSourceAdd(hitTestSource); if (callback) callback(err, hitTestSource); }; this._manager.hitTest.start(options); } onHitTestSourceAdd(hitTestSource) { this._hitTestSources.push(hitTestSource); this.fire('hittest:add', hitTestSource); hitTestSource.on('result', (position, rotation, inputSource, hitTestResult)=>{ if (inputSource !== this) return; this.fire('hittest:result', hitTestSource, position, rotation, hitTestResult); }); hitTestSource.once('remove', ()=>{ this.onHitTestSourceRemove(hitTestSource); this.fire('hittest:remove', hitTestSource); }); } onHitTestSourceRemove(hitTestSource) { const ind = this._hitTestSources.indexOf(hitTestSource); if (ind !== -1) this._hitTestSources.splice(ind, 1); } constructor(manager, xrInputSource){ super(), this._ray = new Ray(), this._rayLocal = new Ray(), this._grip = false, this._hand = null, this._velocitiesAvailable = false, this._velocitiesTimestamp = now(), this._localTransform = null, this._worldTransform = null, this._position = new Vec3(), this._rotation = new Quat(), this._localPosition = null, this._localPositionLast = null, this._localRotation = null, this._linearVelocity = null, this._dirtyLocal = true, this._dirtyRay = false, this._selecting = false, this._squeezing = false, this._elementInput = true, this._elementEntity = null, this._hitTestSources = []; this._id = ++ids$1; this._manager = manager; this._xrInputSource = xrInputSource; if (xrInputSource.hand) { this._hand = new XrHand(this); } } } XrInputSource.EVENT_REMOVE = 'remove'; XrInputSource.EVENT_SELECT = 'select'; XrInputSource.EVENT_SELECTSTART = 'selectstart'; XrInputSource.EVENT_SELECTEND = 'selectend'; XrInputSource.EVENT_SQUEEZE = 'squeeze'; XrInputSource.EVENT_SQUEEZESTART = 'squeezestart'; XrInputSource.EVENT_SQUEEZEEND = 'squeezeend'; XrInputSource.EVENT_HITTESTADD = 'hittest:add'; XrInputSource.EVENT_HITTESTREMOVE = 'hittest:remove'; XrInputSource.EVENT_HITTESTRESULT = 'hittest:result'; class XrInput extends EventHandler { _onSessionStart() { const session = this.manager.session; session.addEventListener('inputsourceschange', this._onInputSourcesChangeEvt); session.addEventListener('select', (evt)=>{ const inputSource = this._getByInputSource(evt.inputSource); inputSource.update(evt.frame); inputSource.fire('select', evt); this.fire('select', inputSource, evt); }); session.addEventListener('selectstart', (evt)=>{ const inputSource = this._getByInputSource(evt.inputSource); inputSource.update(evt.frame); inputSource._selecting = true; inputSource.fire('selectstart', evt); this.fire('selectstart', inputSource, evt); }); session.addEventListener('selectend', (evt)=>{ const inputSource = this._getByInputSource(evt.inputSource); inputSource.update(evt.frame); inputSource._selecting = false; inputSource.fire('selectend', evt); this.fire('selectend', inputSource, evt); }); session.addEventListener('squeeze', (evt)=>{ const inputSource = this._getByInputSource(evt.inputSource); inputSource.update(evt.frame); inputSource.fire('squeeze', evt); this.fire('squeeze', inputSource, evt); }); session.addEventListener('squeezestart', (evt)=>{ const inputSource = this._getByInputSource(evt.inputSource); inputSource.update(evt.frame); inputSource._squeezing = true; inputSource.fire('squeezestart', evt); this.fire('squeezestart', inputSource, evt); }); session.addEventListener('squeezeend', (evt)=>{ const inputSource = this._getByInputSource(evt.inputSource); inputSource.update(evt.frame); inputSource._squeezing = false; inputSource.fire('squeezeend', evt); this.fire('squeezeend', inputSource, evt); }); const inputSources = session.inputSources; for(let i = 0; i < inputSources.length; i++){ this._addInputSource(inputSources[i]); } } _onSessionEnd() { let i = this._inputSources.length; while(i--){ const inputSource = this._inputSources[i]; this._inputSources.splice(i, 1); inputSource.fire('remove'); this.fire('remove', inputSource); } const session = this.manager.session; session.removeEventListener('inputsourceschange', this._onInputSourcesChangeEvt); } _onInputSourcesChange(evt) { for(let i = 0; i < evt.removed.length; i++){ this._removeInputSource(evt.removed[i]); } for(let i = 0; i < evt.added.length; i++){ this._addInputSource(evt.added[i]); } } _getByInputSource(xrInputSource) { for(let i = 0; i < this._inputSources.length; i++){ if (this._inputSources[i].inputSource === xrInputSource) { return this._inputSources[i]; } } return null; } _addInputSource(xrInputSource) { if (this._getByInputSource(xrInputSource)) { return; } const inputSource = new XrInputSource(this.manager, xrInputSource); this._inputSources.push(inputSource); this.fire('add', inputSource); } _removeInputSource(xrInputSource) { for(let i = 0; i < this._inputSources.length; i++){ if (this._inputSources[i].inputSource !== xrInputSource) { continue; } const inputSource = this._inputSources[i]; this._inputSources.splice(i, 1); let h = inputSource.hitTestSources.length; while(h--){ inputSource.hitTestSources[h].remove(); } inputSource.fire('remove'); this.fire('remove', inputSource); return; } } update(frame) { for(let i = 0; i < this._inputSources.length; i++){ this._inputSources[i].update(frame); } } get inputSources() { return this._inputSources; } constructor(manager){ super(), this._inputSources = [], this.velocitiesSupported = false; this.manager = manager; this.velocitiesSupported = !!(platform.browser && window.XRPose?.prototype?.hasOwnProperty('linearVelocity')); this._onInputSourcesChangeEvt = (evt)=>{ this._onInputSourcesChange(evt); }; this.manager.on('start', this._onSessionStart, this); this.manager.on('end', this._onSessionEnd, this); } } XrInput.EVENT_ADD = 'add'; XrInput.EVENT_REMOVE = 'remove'; XrInput.EVENT_SELECT = 'select'; XrInput.EVENT_SELECTSTART = 'selectstart'; XrInput.EVENT_SELECTEND = 'selectend'; XrInput.EVENT_SQUEEZE = 'squeeze'; XrInput.EVENT_SQUEEZESTART = 'squeezestart'; XrInput.EVENT_SQUEEZEEND = 'squeezeend'; const vec3A = new Vec3(); const vec3B = new Vec3(); const mat4A = new Mat4(); const mat4B = new Mat4(); class XrLightEstimation extends EventHandler { _onSessionStart() { const supported = !!this._manager.session.requestLightProbe; if (!supported) return; this._supported = true; } _onSessionEnd() { this._supported = false; this._available = false; this._lightProbeRequested = false; this._lightProbe = null; } start() { let err; if (!this._manager.session) { err = new Error('XR session is not running'); } if (!err && this._manager.type !== XRTYPE_AR) { err = new Error('XR session type is not AR'); } if (!err && !this._supported) { err = new Error('light-estimation is not supported'); } if (!err && this._lightProbe || this._lightProbeRequested) { err = new Error('light estimation is already requested'); } if (err) { this.fire('error', err); return; } this._lightProbeRequested = true; this._manager.session.requestLightProbe().then((lightProbe)=>{ const wasRequested = this._lightProbeRequested; this._lightProbeRequested = false; if (this._manager.active) { if (wasRequested) { this._lightProbe = lightProbe; } } else { this.fire('error', new Error('XR session is not active')); } }).catch((ex)=>{ this._lightProbeRequested = false; this.fire('error', ex); }); } end() { this._lightProbeRequested = false; this._lightProbe = null; this._available = false; } update(frame) { if (!this._lightProbe) return; const lightEstimate = frame.getLightEstimate(this._lightProbe); if (!lightEstimate) return; if (!this._available) { this._available = true; this.fire('available'); } const pli = lightEstimate.primaryLightIntensity; this._intensity = Math.max(1.0, Math.max(pli.x, Math.max(pli.y, pli.z))); vec3A.copy(pli).mulScalar(1 / this._intensity); this._color.set(vec3A.x, vec3A.y, vec3A.z); vec3A.set(0, 0, 0); vec3B.copy(lightEstimate.primaryLightDirection); mat4A.setLookAt(vec3B, vec3A, Vec3.UP); mat4B.setFromAxisAngle(Vec3.RIGHT, 90); mat4A.mul(mat4B); this._rotation.setFromMat4(mat4A); this._sphericalHarmonics.set(lightEstimate.sphericalHarmonicsCoefficients); } get supported() { return this._supported; } get available() { return this._available; } get intensity() { return this._available ? this._intensity : null; } get color() { return this._available ? this._color : null; } get rotation() { return this._available ? this._rotation : null; } get sphericalHarmonics() { return this._available ? this._sphericalHarmonics : null; } constructor(manager){ super(), this._supported = false, this._available = false, this._lightProbeRequested = false, this._lightProbe = null, this._intensity = 0, this._rotation = new Quat(), this._color = new Color(), this._sphericalHarmonics = new Float32Array(27); this._manager = manager; this._manager.on('start', this._onSessionStart, this); this._manager.on('end', this._onSessionEnd, this); } } XrLightEstimation.EVENT_AVAILABLE = 'available'; XrLightEstimation.EVENT_ERROR = 'error'; let ids = 0; class XrPlane extends EventHandler { destroy() { if (!this._xrPlane) return; this._xrPlane = null; this.fire('remove'); } update(frame) { const manager = this._planeDetection._manager; const pose = frame.getPose(this._xrPlane.planeSpace, manager._referenceSpace); if (pose) { this._position.copy(pose.transform.position); this._rotation.copy(pose.transform.orientation); } if (this._lastChangedTime !== this._xrPlane.lastChangedTime) { this._lastChangedTime = this._xrPlane.lastChangedTime; this.fire('change'); } } getPosition() { return this._position; } getRotation() { return this._rotation; } get id() { return this._id; } get orientation() { return this._orientation; } get points() { return this._xrPlane.polygon; } get label() { return this._xrPlane.semanticLabel || ''; } constructor(planeDetection, xrPlane){ super(), this._position = new Vec3(), this._rotation = new Quat(); this._id = ++ids; this._planeDetection = planeDetection; this._xrPlane = xrPlane; this._lastChangedTime = xrPlane.lastChangedTime; this._orientation = xrPlane.orientation; } } XrPlane.EVENT_REMOVE = 'remove'; XrPlane.EVENT_CHANGE = 'change'; class XrPlaneDetection extends EventHandler { _onSessionStart() { if (this._manager.session.enabledFeatures) { const available = this._manager.session.enabledFeatures.indexOf('plane-detection') !== -1; if (available) { this._available = true; this.fire('available'); } } } _onSessionEnd() { for(let i = 0; i < this._planes.length; i++){ this._planes[i].destroy(); this.fire('remove', this._planes[i]); } this._planesIndex.clear(); this._planes.length = 0; if (this._available) { this._available = false; this.fire('unavailable'); } } update(frame) { if (!this._available) { if (!this._manager.session.enabledFeatures && frame.detectedPlanes.size) { this._available = true; this.fire('available'); } else { return; } } const detectedPlanes = frame.detectedPlanes; for (const [xrPlane, plane] of this._planesIndex){ if (detectedPlanes.has(xrPlane)) { continue; } this._planesIndex.delete(xrPlane); this._planes.splice(this._planes.indexOf(plane), 1); plane.destroy(); this.fire('remove', plane); } for (const xrPlane of detectedPlanes){ let plane = this._planesIndex.get(xrPlane); if (!plane) { plane = new XrPlane(this, xrPlane); this._planesIndex.set(xrPlane, plane); this._planes.push(plane); plane.update(frame); this.fire('add', plane); } else { plane.update(frame); } } } get supported() { return this._supported; } get available() { return this._available; } get planes() { return this._planes; } constructor(manager){ super(), this._supported = platform.browser && !!window.XRPlane, this._available = false, this._planesIndex = new Map(), this._planes = []; this._manager = manager; if (this._supported) { this._manager.on('start', this._onSessionStart, this); this._manager.on('end', this._onSessionEnd, this); } } } XrPlaneDetection.EVENT_AVAILABLE = 'available'; XrPlaneDetection.EVENT_UNAVAILABLE = 'unavailable'; XrPlaneDetection.EVENT_ADD = 'add'; XrPlaneDetection.EVENT_REMOVE = 'remove'; class XrAnchor extends EventHandler { destroy() { if (!this._xrAnchor) return; const xrAnchor = this._xrAnchor; this._xrAnchor.delete(); this._xrAnchor = null; this.fire('destroy', xrAnchor, this); } update(frame) { if (!this._xrAnchor) { return; } const pose = frame.getPose(this._xrAnchor.anchorSpace, this._anchors.manager._referenceSpace); if (pose) { if (this._position.equals(pose.transform.position) && this._rotation.equals(pose.transform.orientation)) { return; } this._position.copy(pose.transform.position); this._rotation.copy(pose.transform.orientation); this.fire('change'); } } getPosition() { return this._position; } getRotation() { return this._rotation; } persist(callback) { if (!this._anchors.persistence) { callback?.(new Error('Persistent Anchors are not supported'), null); return; } if (this._uuid) { callback?.(null, this._uuid); return; } if (this._uuidRequests) { if (callback) this._uuidRequests.push(callback); return; } this._uuidRequests = []; this._xrAnchor.requestPersistentHandle().then((uuid)=>{ this._uuid = uuid; this._anchors._indexByUuid.set(this._uuid, this); callback?.(null, uuid); for (const uuidRequest of this._uuidRequests){ uuidRequest(null, uuid); } this._uuidRequests = null; this.fire('persist', uuid); }).catch((ex)=>{ callback?.(ex, null); for (const uuidRequest of this._uuidRequests){ uuidRequest(ex, null); } this._uuidRequests = null; }); } forget(callback) { if (!this._uuid) { callback?.(new Error('Anchor is not persistent')); return; } this._anchors.forget(this._uuid, (ex)=>{ this._uuid = null; callback?.(ex); this.fire('forget'); }); } get uuid() { return this._uuid; } get persistent() { return !!this._uuid; } constructor(anchors, xrAnchor, uuid = null){ super(), this._position = new Vec3(), this._rotation = new Quat(), this._uuid = null, this._uuidRequests = null; this._anchors = anchors; this._xrAnchor = xrAnchor; this._uuid = uuid; } } XrAnchor.EVENT_DESTROY = 'destroy'; XrAnchor.EVENT_CHANGE = 'change'; XrAnchor.EVENT_PERSIST = 'persist'; XrAnchor.EVENT_FORGET = 'forget'; class XrAnchors extends EventHandler { _onSessionStart() { const available = this.manager.session.enabledFeatures?.indexOf('anchors') >= 0; if (!available) return; this._available = available; this.fire('available'); } _onSessionEnd() { if (!this._available) return; this._available = false; for(let i = 0; i < this._creationQueue.length; i++){ if (!this._creationQueue[i].callback) { continue; } this._creationQueue[i].callback(new Error('session ended'), null); } this._creationQueue.length = 0; this._index.clear(); this._indexByUuid.clear(); let i = this._list.length; while(i--){ this._list[i].destroy(); } this._list.length = 0; this.fire('unavailable'); } _createAnchor(xrAnchor, uuid = null) { const anchor = new XrAnchor(this, xrAnchor, uuid); this._index.set(xrAnchor, anchor); if (uuid) this._indexByUuid.set(uuid, anchor); this._list.push(anchor); anchor.once('destroy', this._onAnchorDestroy, this); return anchor; } _onAnchorDestroy(xrAnchor, anchor) { this._index.delete(xrAnchor); if (anchor.uuid) this._indexByUuid.delete(anchor.uuid); const ind = this._list.indexOf(anchor); if (ind !== -1) this._list.splice(ind, 1); this.fire('destroy', anchor); } create(position, rotation, callback) { if (!this._available) { callback?.(new Error('Anchors API is not available'), null); return; } if (window.XRHitTestResult && position instanceof XRHitTestResult) { const hitResult = position; callback = rotation; if (!this._supported) { callback?.(new Error('Anchors API is not supported'), null); return; } if (!hitResult.createAnchor) { callback?.(new Error('Creating Anchor from Hit Test is not supported'), null); return; } hitResult.createAnchor().then((xrAnchor)=>{ const anchor = this._createAnchor(xrAnchor); callback?.(null, anchor); this.fire('add', anchor); }).catch((ex)=>{ callback?.(ex, null); this.fire('error', ex); }); } else { this._creationQueue.push({ transform: new XRRigidTransform(position, rotation), callback: callback }); } } restore(uuid, callback) { if (!this._available) { callback?.(new Error('Anchors API is not available'), null); return; } if (!this._persistence) { callback?.(new Error('Anchor Persistence is not supported'), null); return; } if (!this.manager.active) { callback?.(new Error('WebXR session is not active'), null); return; } this.manager.session.restorePersistentAnchor(uuid).then((xrAnchor)=>{ const anchor = this._createAnchor(xrAnchor, uuid); callback?.(null, anchor); this.fire('add', anchor); }).catch((ex)=>{ callback?.(ex, null); this.fire('error', ex); }); } forget(uuid, callback) { if (!this._available) { callback?.(new Error('Anchors API is not available')); return; } if (!this._persistence) { callback?.(new Error('Anchor Persistence is not supported')); return; } if (!this.manager.active) { callback?.(new Error('WebXR session is not active')); return; } this.manager.session.deletePersistentAnchor(uuid).then(()=>{ callback?.(null); }).catch((ex)=>{ callback?.(ex); this.fire('error', ex); }); } update(frame) { if (!this._available) { if (!this.manager.session.enabledFeatures && !this._checkingAvailability) { this._checkingAvailability = true; frame.createAnchor(new XRRigidTransform(), this.manager._referenceSpace).then((xrAnchor)=>{ xrAnchor.delete(); if (this.manager.active) { this._available = true; this.fire('available'); } }).catch(()=>{}); } return; } if (this._creationQueue.length) { for(let i = 0; i < this._creationQueue.length; i++){ const request = this._creationQueue[i]; frame.createAnchor(request.transform, this.manager._referenceSpace).then((xrAnchor)=>{ if (request.callback) { this._callbacksAnchors.set(xrAnchor, request.callback); } }).catch((ex)=>{ if (request.callback) { request.callback(ex, null); } this.fire('error', ex); }); } this._creationQueue.length = 0; } for (const [xrAnchor, anchor] of this._index){ if (frame.trackedAnchors.has(xrAnchor)) { continue; } this._index.delete(xrAnchor); anchor.destroy(); } for(let i = 0; i < this._list.length; i++){ this._list[i].update(frame); } for (const xrAnchor of frame.trackedAnchors){ if (this._index.has(xrAnchor)) { continue; } try { const tmp = xrAnchor.anchorSpace; } catch (ex) { continue; } const anchor = this._createAnchor(xrAnchor); anchor.update(frame); const callback = this._callbacksAnchors.get(xrAnchor); if (callback) { this._callbacksAnchors.delete(xrAnchor); callback(null, anchor); } this.fire('add', anchor); } } get supported() { return this._supported; } get available() { return this._available; } get persistence() { return this._persistence; } get uuids() { if (!this._available) { return null; } if (!this._persistence) { return null; } if (!this.manager.active) { return null; } return this.manager.session.persistentAnchors; } get list() { return this._list; } constructor(manager){ super(), this._supported = platform.browser && !!window.XRAnchor, this._available = false, this._checkingAvailability = false, this._persistence = platform.browser && !!window?.XRSession?.prototype.restorePersistentAnchor, this._creationQueue = [], this._index = new Map(), this._indexByUuid = new Map(), this._list = [], this._callbacksAnchors = new Map(); this.manager = manager; if (this._supported) { this.manager.on('start', this._onSessionStart, this); this.manager.on('end', this._onSessionEnd, this); } } } XrAnchors.EVENT_AVAILABLE = 'available'; XrAnchors.EVENT_UNAVAILABLE = 'unavailable'; XrAnchors.EVENT_ERROR = 'error'; XrAnchors.EVENT_ADD = 'add'; XrAnchors.EVENT_DESTROY = 'destroy'; class XrMesh extends EventHandler { get xrMesh() { return this._xrMesh; } get label() { return this._xrMesh.semanticLabel || ''; } get vertices() { return this._xrMesh.vertices; } get indices() { return this._xrMesh.indices; } destroy() { if (!this._xrMesh) return; this._xrMesh = null; this.fire('remove'); } update(frame) { const manager = this._meshDetection._manager; const pose = frame.getPose(this._xrMesh.meshSpace, manager._referenceSpace); if (pose) { this._position.copy(pose.transform.position); this._rotation.copy(pose.transform.orientation); } if (this._lastChanged !== this._xrMesh.lastChangedTime) { this._lastChanged = this._xrMesh.lastChangedTime; this.fire('change'); } } getPosition() { return this._position; } getRotation() { return this._rotation; } constructor(meshDetection, xrMesh){ super(), this._lastChanged = 0, this._position = new Vec3(), this._rotation = new Quat(); this._meshDetection = meshDetection; this._xrMesh = xrMesh; this._lastChanged = this._xrMesh.lastChangedTime; } } XrMesh.EVENT_REMOVE = 'remove'; XrMesh.EVENT_CHANGE = 'change'; class XrMeshDetection extends EventHandler { update(frame) { if (!this._available) { if (!this._manager.session.enabledFeatures && frame.detectedMeshes.size) { this._available = true; this.fire('available'); } else { return; } } for (const xrMesh of frame.detectedMeshes){ let mesh = this._index.get(xrMesh); if (!mesh) { mesh = new XrMesh(this, xrMesh); this._index.set(xrMesh, mesh); this._list.push(mesh); mesh.update(frame); this.fire('add', mesh); } else { mesh.update(frame); } } for (const mesh of this._index.values()){ if (frame.detectedMeshes.has(mesh.xrMesh)) { continue; } this._removeMesh(mesh); } } _removeMesh(mesh) { this._index.delete(mesh.xrMesh); this._list.splice(this._list.indexOf(mesh), 1); mesh.destroy(); this.fire('remove', mesh); } _onSessionStart() { if (this._manager.session.enabledFeatures) { const available = this._manager.session.enabledFeatures.indexOf('mesh-detection') !== -1; if (!available) return; this._available = available; this.fire('available'); } } _onSessionEnd() { if (!this._available) return; this._available = false; for (const mesh of this._index.values()){ this._removeMesh(mesh); } this.fire('unavailable'); } get supported() { return this._supported; } get available() { return this._available; } get meshes() { return this._list; } constructor(manager){ super(), this._supported = platform.browser && !!window.XRMesh, this._available = false, this._index = new Map(), this._list = []; this._manager = manager; if (this._supported) { this._manager.on('start', this._onSessionStart, this); this._manager.on('end', this._onSessionEnd, this); } } } XrMeshDetection.EVENT_AVAILABLE = 'available'; XrMeshDetection.EVENT_UNAVAILABLE = 'unavailable'; XrMeshDetection.EVENT_ADD = 'add'; XrMeshDetection.EVENT_REMOVE = 'remove'; class XrView extends EventHandler { get textureColor() { return this._textureColor; } get textureDepth() { return this._textureDepth; } get depthUvMatrix() { return this._depthMatrix; } get depthValueToMeters() { return this._depthInfo?.rawValueToMeters || 0; } get eye() { return this._xrView.eye; } get viewport() { return this._viewport; } get projMat() { return this._projMat; } get projViewOffMat() { return this._projViewOffMat; } get viewOffMat() { return this._viewOffMat; } get viewInvOffMat() { return this._viewInvOffMat; } get viewMat3() { return this._viewMat3; } get positionData() { return this._positionData; } update(frame, xrView) { this._xrView = xrView; if (this._manager.views.availableColor) { this._xrCamera = this._xrView.camera; } const layer = frame.session.renderState.baseLayer; const viewport = layer.getViewport(this._xrView); this._viewport.x = viewport.x; this._viewport.y = viewport.y; this._viewport.z = viewport.width; this._viewport.w = viewport.height; this._projMat.set(this._xrView.projectionMatrix); this._viewMat.set(this._xrView.transform.inverse.matrix); this._viewInvMat.set(this._xrView.transform.matrix); this._updateTextureColor(); this._updateDepth(frame); } _updateTextureColor() { if (!this._manager.views.availableColor || !this._xrCamera || !this._textureColor) { return; } const binding = this._manager.webglBinding; if (!binding) { return; } const texture = binding.getCameraImage(this._xrCamera); if (!texture) { return; } const device = this._manager.app.graphicsDevice; const gl = device.gl; if (!this._frameBufferSource) { this._frameBufferSource = gl.createFramebuffer(); this._frameBuffer = gl.createFramebuffer(); } else { const attachmentBaseConstant = gl.COLOR_ATTACHMENT0; const width = this._xrCamera.width; const height = this._xrCamera.height; device.setFramebuffer(this._frameBufferSource); gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentBaseConstant, gl.TEXTURE_2D, texture, 0); device.setFramebuffer(this._frameBuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentBaseConstant, gl.TEXTURE_2D, this._textureColor.impl._glTexture, 0); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._frameBufferSource); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._frameBuffer); gl.blitFramebuffer(0, height, width, 0, 0, 0, width, height, gl.COLOR_BUFFER_BIT, gl.NEAREST); } } _updateDepth(frame) { if (!this._manager.views.availableDepth || !this._textureDepth) { return; } const gpu = this._manager.views.depthGpuOptimized; const infoSource = gpu ? this._manager.webglBinding : frame; if (!infoSource) { this._depthInfo = null; return; } const depthInfo = infoSource.getDepthInformation(this._xrView); if (!depthInfo) { this._depthInfo = null; return; } let matrixDirty = !this._depthInfo !== !depthInfo; this._depthInfo = depthInfo; const width = this._depthInfo?.width || 4; const height = this._depthInfo?.height || 4; let resized = false; if (this._textureDepth.width !== width || this._textureDepth.height !== height) { this._textureDepth._width = width; this._textureDepth._height = height; matrixDirty = true; resized = true; } if (matrixDirty) { if (this._depthInfo) { this._depthMatrix.data.set(this._depthInfo.normDepthBufferFromNormView.matrix); } else { this._depthMatrix.setIdentity(); } } if (this._depthInfo) { if (gpu) { if (this._depthInfo.texture) { const gl = this._manager.app.graphicsDevice.gl; this._textureDepth.impl._glTexture = this._depthInfo.texture; if (this._depthInfo.textureType === 'texture-array') { this._textureDepth.impl._glTarget = gl.TEXTURE_2D_ARRAY; } else { this._textureDepth.impl._glTarget = gl.TEXTURE_2D; } switch(this._manager.views.depthPixelFormat){ case PIXELFORMAT_R32F: this._textureDepth.impl._glInternalFormat = gl.R32F; this._textureDepth.impl._glPixelType = gl.FLOAT; this._textureDepth.impl._glFormat = gl.RED; break; case PIXELFORMAT_DEPTH: this._textureDepth.impl._glInternalFormat = gl.DEPTH_COMPONENT16; this._textureDepth.impl._glPixelType = gl.UNSIGNED_SHORT; this._textureDepth.impl._glFormat = gl.DEPTH_COMPONENT; break; } this._textureDepth.impl._glCreated = true; } } else { this._textureDepth._levels[0] = new Uint8Array(this._depthInfo.data); this._textureDepth.upload(); } } else { this._textureDepth._levels[0] = this._emptyDepthBuffer; this._textureDepth.upload(); } if (resized) this.fire('depth:resize', width, height); } updateTransforms(transform) { if (transform) { this._viewInvOffMat.mul2(transform, this._viewInvMat); this.viewOffMat.copy(this._viewInvOffMat).invert(); } else { this._viewInvOffMat.copy(this._viewInvMat); this.viewOffMat.copy(this._viewMat); } this._viewMat3.setFromMat4(this._viewOffMat); this._projViewOffMat.mul2(this._projMat, this._viewOffMat); this._positionData[0] = this._viewInvOffMat.data[12]; this._positionData[1] = this._viewInvOffMat.data[13]; this._positionData[2] = this._viewInvOffMat.data[14]; } _onDeviceLost() { this._frameBufferSource = null; this._frameBuffer = null; this._depthInfo = null; } getDepth(u, v) { if (this._manager.views.depthGpuOptimized) { return null; } return this._depthInfo?.getDepthInMeters(u, v) ?? null; } destroy() { this._depthInfo = null; if (this._textureColor) { this._textureColor.destroy(); this._textureColor = null; } if (this._textureDepth) { this._textureDepth.destroy(); this._textureDepth = null; } if (this._frameBufferSource) { const gl = this._manager.app.graphicsDevice.gl; gl.deleteFramebuffer(this._frameBufferSource); this._frameBufferSource = null; gl.deleteFramebuffer(this._frameBuffer); this._frameBuffer = null; } } constructor(manager, xrView, viewsCount){ super(), this._positionData = new Float32Array(3), this._viewport = new Vec4(), this._projMat = new Mat4(), this._projViewOffMat = new Mat4(), this._viewMat = new Mat4(), this._viewOffMat = new Mat4(), this._viewMat3 = new Mat3(), this._viewInvMat = new Mat4(), this._viewInvOffMat = new Mat4(), this._xrCamera = null, this._textureColor = null, this._textureDepth = null, this._depthInfo = null, this._emptyDepthBuffer = new Uint8Array(32), this._depthMatrix = new Mat4(); this._manager = manager; this._xrView = xrView; const device = this._manager.app.graphicsDevice; if (this._manager.views.supportedColor) { this._xrCamera = this._xrView.camera; if (this._manager.views.availableColor && this._xrCamera) { this._textureColor = new Texture(device, { format: PIXELFORMAT_RGB8, mipmaps: false, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, minFilter: FILTER_LINEAR, magFilter: FILTER_LINEAR, width: this._xrCamera.width, height: this._xrCamera.height, name: `XrView-${this._xrView.eye}-Color` }); } } if (this._manager.views.supportedDepth && this._manager.views.availableDepth) { const filtering = this._manager.views.depthGpuOptimized ? FILTER_NEAREST : FILTER_LINEAR; this._textureDepth = new Texture(device, { format: this._manager.views.depthPixelFormat, arrayLength: viewsCount === 1 ? 0 : viewsCount, mipmaps: false, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, minFilter: filtering, magFilter: filtering, width: 4, height: 4, name: `XrView-${this._xrView.eye}-Depth` }); for(let i = 0; i < this._textureDepth._levels.length; i++){ this._textureDepth._levels[i] = this._emptyDepthBuffer; } this._textureDepth.upload(); } if (this._textureColor || this._textureDepth) { device.on('devicelost', this._onDeviceLost, this); } } } XrView.EVENT_DEPTHRESIZE = 'depth:resize'; class XrViews extends EventHandler { get list() { return this._list; } get supportedColor() { return this._supportedColor; } get supportedDepth() { return this._supportedDepth; } get availableColor() { return this._availableColor; } get availableDepth() { return this._availableDepth; } get depthUsage() { return this._depthUsage; } get depthGpuOptimized() { return this._depthUsage === XRDEPTHSENSINGUSAGE_GPU; } get depthFormat() { return this._depthFormat; } get depthPixelFormat() { return this._depthFormats[this._depthFormat] ?? null; } update(frame, xrViews) { for(let i = 0; i < xrViews.length; i++){ this._indexTmp.set(xrViews[i].eye, xrViews[i]); } for (const [eye, xrView] of this._indexTmp){ let view = this._index.get(eye); if (!view) { view = new XrView(this._manager, xrView, xrViews.length); this._index.set(eye, view); this._list.push(view); view.update(frame, xrView); this.fire('add', view); } else { view.update(frame, xrView); } } for (const [eye, view] of this._index){ if (this._indexTmp.has(eye)) { continue; } view.destroy(); this._index.delete(eye); const ind = this._list.indexOf(view); if (ind !== -1) this._list.splice(ind, 1); this.fire('remove', view); } this._indexTmp.clear(); } get(eye) { return this._index.get(eye) || null; } _onSessionStart() { if (this._manager.type !== XRTYPE_AR) { return; } if (!this._manager.session.enabledFeatures) { return; } this._availableColor = this._manager.session.enabledFeatures.indexOf('camera-access') !== -1; this._availableDepth = this._manager.session.enabledFeatures.indexOf('depth-sensing') !== -1; if (this._availableDepth) { const session = this._manager.session; this._depthUsage = session.depthUsage; this._depthFormat = session.depthDataFormat; } } _onSessionEnd() { for (const view of this._index.values()){ view.destroy(); } this._index.clear(); this._availableColor = false; this._availableDepth = false; this._depthUsage = ''; this._depthFormat = ''; this._list.length = 0; } constructor(manager){ super(), this._index = new Map(), this._indexTmp = new Map(), this._list = [], this._supportedColor = platform.browser && !!window.XRCamera && !!window.XRWebGLBinding, this._supportedDepth = platform.browser && !!window.XRDepthInformation, this._availableColor = false, this._availableDepth = false, this._depthUsage = '', this._depthFormat = '', this._depthFormats = { [XRDEPTHSENSINGFORMAT_L8A8]: PIXELFORMAT_LA8, [XRDEPTHSENSINGFORMAT_R16U]: PIXELFORMAT_DEPTH, [XRDEPTHSENSINGFORMAT_F32]: PIXELFORMAT_R32F }; this._manager = manager; this._manager.on('start', this._onSessionStart, this); this._manager.on('end', this._onSessionEnd, this); } } XrViews.EVENT_ADD = 'add'; XrViews.EVENT_REMOVE = 'remove'; class XrManager extends EventHandler { destroy() {} start(camera, type, spaceType, options) { let callback = options; if (typeof options === 'object') { callback = options.callback; } if (!this._available[type]) { if (callback) callback(new Error('XR is not available')); return; } if (this._session) { if (callback) callback(new Error('XR session is already started')); return; } this._camera = camera; this._camera.camera.xr = this; this._type = type; this._spaceType = spaceType; this._framebufferScaleFactor = options?.framebufferScaleFactor ?? 1.0; this._setClipPlanes(camera.nearClip, camera.farClip); const opts = { requiredFeatures: [ spaceType ], optionalFeatures: [] }; const device = this.app.graphicsDevice; if (device?.isWebGPU) { opts.requiredFeatures.push('webgpu'); } const webgl = device?.isWebGL2; if (type === XRTYPE_AR) { opts.optionalFeatures.push('light-estimation'); opts.optionalFeatures.push('hit-test'); if (options) { if (options.imageTracking && this.imageTracking.supported) { opts.optionalFeatures.push('image-tracking'); } if (options.planeDetection) { opts.optionalFeatures.push('plane-detection'); } if (options.meshDetection) { opts.optionalFeatures.push('mesh-detection'); } } if (this.domOverlay.supported && this.domOverlay.root) { opts.optionalFeatures.push('dom-overlay'); opts.domOverlay = { root: this.domOverlay.root }; } if (options && options.anchors && this.anchors.supported) { opts.optionalFeatures.push('anchors'); } if (options && options.depthSensing && this.views.supportedDepth) { opts.optionalFeatures.push('depth-sensing'); const usagePreference = []; const dataFormatPreference = []; usagePreference.push(XRDEPTHSENSINGUSAGE_GPU, XRDEPTHSENSINGUSAGE_CPU); dataFormatPreference.push(XRDEPTHSENSINGFORMAT_F32, XRDEPTHSENSINGFORMAT_L8A8, XRDEPTHSENSINGFORMAT_R16U); if (options.depthSensing.usagePreference) { const ind = usagePreference.indexOf(options.depthSensing.usagePreference); if (ind !== -1) usagePreference.splice(ind, 1); usagePreference.unshift(options.depthSensing.usagePreference); } if (options.depthSensing.dataFormatPreference) { const ind = dataFormatPreference.indexOf(options.depthSensing.dataFormatPreference); if (ind !== -1) dataFormatPreference.splice(ind, 1); dataFormatPreference.unshift(options.depthSensing.dataFormatPreference); } opts.depthSensing = { usagePreference: usagePreference, dataFormatPreference: dataFormatPreference }; } if (webgl && options && options.cameraColor && this.views.supportedColor) { opts.optionalFeatures.push('camera-access'); } } opts.optionalFeatures.push('hand-tracking'); if (options && options.optionalFeatures) { opts.optionalFeatures = opts.optionalFeatures.concat(options.optionalFeatures); } if (this.imageTracking.supported && this.imageTracking.images.length) { this.imageTracking.prepareImages((err, trackedImages)=>{ if (err) { if (callback) callback(err); this.fire('error', err); return; } if (trackedImages !== null) { opts.trackedImages = trackedImages; } this._onStartOptionsReady(type, spaceType, opts, callback); }); } else { this._onStartOptionsReady(type, spaceType, opts, callback); } } _onStartOptionsReady(type, spaceType, options, callback) { navigator.xr.requestSession(type, options).then((session)=>{ this._onSessionStart(session, spaceType, callback); }).catch((ex)=>{ this._camera.camera.xr = null; this._camera = null; this._type = null; this._spaceType = null; if (callback) callback(ex); this.fire('error', ex); }); } end(callback) { if (!this._session) { if (callback) callback(new Error('XR Session is not initialized')); return; } this.webglBinding = null; if (callback) this.once('end', callback); this._session.end(); } isAvailable(type) { return this._available[type]; } _deviceAvailabilityCheck() { for(const key in this._available){ this._sessionSupportCheck(key); } } initiateRoomCapture(callback) { if (!this._session) { callback(new Error('Session is not active')); return; } if (!this._session.initiateRoomCapture) { callback(new Error('Session does not support manual room capture')); return; } this._session.initiateRoomCapture().then(()=>{ if (callback) callback(null); }).catch((err)=>{ if (callback) callback(err); }); } updateTargetFrameRate(frameRate, callback) { if (!this._session?.updateTargetFrameRate) { callback?.(new Error('unable to update frameRate')); return; } this._session.updateTargetFrameRate(frameRate).then(()=>{ callback?.(); }).catch((err)=>{ callback?.(err); }); } _sessionSupportCheck(type) { navigator.xr.isSessionSupported(type).then((available)=>{ if (this._available[type] === available) { return; } this._available[type] = available; this.fire('available', type, available); this.fire(`available:${type}`, available); }).catch((ex)=>{ this.fire('error', ex); }); } _onSessionStart(session, spaceType, callback) { let failed = false; this._session = session; const onVisibilityChange = ()=>{ this.fire('visibility:change', session.visibilityState); }; const onClipPlanesChange = ()=>{ this._setClipPlanes(this._camera.nearClip, this._camera.farClip); }; const onEnd = ()=>{ if (this._camera) { this._camera.off('set_nearClip', onClipPlanesChange); this._camera.off('set_farClip', onClipPlanesChange); this._camera.camera.xr = null; this._camera = null; } session.removeEventListener('end', onEnd); session.removeEventListener('visibilitychange', onVisibilityChange); if (!failed) this.fire('end'); this._session = null; this._referenceSpace = null; this._width = 0; this._height = 0; this._type = null; this._spaceType = null; if (this.app.systems) { this.app.requestAnimationFrame(); } }; session.addEventListener('end', onEnd); session.addEventListener('visibilitychange', onVisibilityChange); this._camera.on('set_nearClip', onClipPlanesChange); this._camera.on('set_farClip', onClipPlanesChange); this._createBaseLayer(); if (this.session.supportedFrameRates) { this._supportedFrameRates = Array.from(this.session.supportedFrameRates); } else { this._supportedFrameRates = null; } this._session.addEventListener('frameratechange', ()=>{ this.fire('frameratechange', this._session?.frameRate); }); session.requestReferenceSpace(spaceType).then((referenceSpace)=>{ this._referenceSpace = referenceSpace; this.app.requestAnimationFrame(); if (callback) callback(null); this.fire('start'); }).catch((ex)=>{ failed = true; session.end(); if (callback) callback(ex); this.fire('error', ex); }); } _setClipPlanes(near, far) { if (this._depthNear === near && this._depthFar === far) { return; } this._depthNear = near; this._depthFar = far; if (!this._session) { return; } this._session.updateRenderState({ depthNear: this._depthNear, depthFar: this._depthFar }); } _createBaseLayer() { const device = this.app.graphicsDevice; const framebufferScaleFactor = device.maxPixelRatio / window.devicePixelRatio * this._framebufferScaleFactor; this._baseLayer = new XRWebGLLayer(this._session, device.gl, { alpha: true, depth: true, stencil: true, framebufferScaleFactor: framebufferScaleFactor, antialias: false }); if (device?.isWebGL2 && window.XRWebGLBinding) { try { this.webglBinding = new XRWebGLBinding(this._session, device.gl); } catch (ex) { this.fire('error', ex); } } this._session.updateRenderState({ baseLayer: this._baseLayer, depthNear: this._depthNear, depthFar: this._depthFar }); } _onDeviceLost() { if (!this._session) { return; } if (this.webglBinding) { this.webglBinding = null; } this._baseLayer = null; this._session.updateRenderState({ baseLayer: this._baseLayer, depthNear: this._depthNear, depthFar: this._depthFar }); } _onDeviceRestored() { if (!this._session) { return; } setTimeout(()=>{ this.app.graphicsDevice.gl.makeXRCompatible().then(()=>{ this._createBaseLayer(); }).catch((ex)=>{ this.fire('error', ex); }); }, 0); } update(frame) { if (!this._session) return false; const width = frame.session.renderState.baseLayer.framebufferWidth; const height = frame.session.renderState.baseLayer.framebufferHeight; if (this._width !== width || this._height !== height) { this._width = width; this._height = height; this.app.graphicsDevice.setResolution(width, height); } const pose = frame.getViewerPose(this._referenceSpace); if (!pose) return false; const lengthOld = this.views.list.length; this.views.update(frame, pose.views); const posePosition = pose.transform.position; const poseOrientation = pose.transform.orientation; this._localPosition.set(posePosition.x, posePosition.y, posePosition.z); this._localRotation.set(poseOrientation.x, poseOrientation.y, poseOrientation.z, poseOrientation.w); if (lengthOld === 0 && this.views.list.length > 0) { const viewProjMat = new Mat4(); const view = this.views.list[0]; viewProjMat.copy(view.projMat); const data = viewProjMat.data; const fov = 2.0 * Math.atan(1.0 / data[5]) * 180.0 / Math.PI; const aspectRatio = data[5] / data[0]; const farClip = data[14] / (data[10] + 1); const nearClip = data[14] / (data[10] - 1); const horizontalFov = false; const camera = this._camera.camera; camera.setXrProperties({ aspectRatio, farClip, fov, horizontalFov, nearClip }); } this._camera.camera._node.setLocalPosition(this._localPosition); this._camera.camera._node.setLocalRotation(this._localRotation); this.input.update(frame); if (this._type === XRTYPE_AR) { if (this.hitTest.supported) { this.hitTest.update(frame); } if (this.lightEstimation.supported) { this.lightEstimation.update(frame); } if (this.imageTracking.supported) { this.imageTracking.update(frame); } if (this.anchors.supported) { this.anchors.update(frame); } if (this.planeDetection.supported) { this.planeDetection.update(frame); } if (this.meshDetection.supported) { this.meshDetection.update(frame); } } this.fire('update', frame); return true; } get supported() { return this._supported; } get active() { return !!this._session; } get type() { return this._type; } get spaceType() { return this._spaceType; } get session() { return this._session; } get frameRate() { return this._session?.frameRate ?? null; } get supportedFrameRates() { return this._supportedFrameRates; } get framebufferScaleFactor() { return this._framebufferScaleFactor; } set fixedFoveation(value) { if ((this._baseLayer?.fixedFoveation ?? null) !== null) { if (this.app.graphicsDevice.samples > 1) ; this._baseLayer.fixedFoveation = value; } } get fixedFoveation() { return this._baseLayer?.fixedFoveation ?? null; } get camera() { return this._camera ? this._camera.entity : null; } get visibilityState() { if (!this._session) { return null; } return this._session.visibilityState; } constructor(app){ super(), this._supported = platform.browser && !!navigator.xr, this._available = {}, this._type = null, this._spaceType = null, this._session = null, this._baseLayer = null, this.webglBinding = null, this._referenceSpace = null, this._camera = null, this._localPosition = new Vec3(), this._localRotation = new Quat(), this._depthNear = 0.1, this._depthFar = 1000, this._supportedFrameRates = null, this._width = 0, this._height = 0, this._framebufferScaleFactor = 1.0; this.app = app; this._available[XRTYPE_INLINE] = false; this._available[XRTYPE_VR] = false; this._available[XRTYPE_AR] = false; this.views = new XrViews(this); this.domOverlay = new XrDomOverlay(this); this.hitTest = new XrHitTest(this); this.imageTracking = new XrImageTracking(this); this.planeDetection = new XrPlaneDetection(this); this.meshDetection = new XrMeshDetection(this); this.input = new XrInput(this); this.lightEstimation = new XrLightEstimation(this); this.anchors = new XrAnchors(this); this.views = new XrViews(this); if (this._supported) { navigator.xr.addEventListener('devicechange', ()=>{ this._deviceAvailabilityCheck(); }); this._deviceAvailabilityCheck(); this.app.graphicsDevice.on('devicelost', this._onDeviceLost, this); this.app.graphicsDevice.on('devicerestored', this._onDeviceRestored, this); } } } XrManager.EVENT_AVAILABLE = 'available'; XrManager.EVENT_START = 'start'; XrManager.EVENT_END = 'end'; XrManager.EVENT_UPDATE = 'update'; XrManager.EVENT_ERROR = 'error'; const tempMeshInstances$1 = []; const lights = [ [], [], [] ]; class RenderPassPicker extends RenderPass { destroy() { this.viewBindGroups.forEach((bg)=>{ bg.defaultUniformBuffer.destroy(); bg.destroy(); }); this.viewBindGroups.length = 0; } update(camera, scene, layers, mapping, depth) { this.camera = camera; this.scene = scene; this.layers = layers; this.mapping = mapping; this.depth = depth; if (scene.clusteredLightingEnabled) { this.emptyWorldClusters = this.renderer.worldClustersAllocator.empty; } } execute() { const device = this.device; const { renderer, camera, scene, layers, mapping, renderTarget } = this; const srcLayers = scene.layers.layerList; const subLayerEnabled = scene.layers.subLayerEnabled; const isTransparent = scene.layers.subLayerList; for(let i = 0; i < srcLayers.length; i++){ const srcLayer = srcLayers[i]; if (layers && layers.indexOf(srcLayer) < 0) { continue; } if (srcLayer.enabled && subLayerEnabled[i]) { if (srcLayer.camerasSet.has(camera.camera)) { const transparent = isTransparent[i]; if (srcLayer._clearDepthBuffer) { renderer.clear(camera.camera, false, true, false); } const meshInstances = srcLayer.meshInstances; for(let j = 0; j < meshInstances.length; j++){ const meshInstance = meshInstances[j]; if (meshInstance.pick && meshInstance.transparent === transparent) { tempMeshInstances$1.push(meshInstance); mapping.set(meshInstance.id, meshInstance); } } if (tempMeshInstances$1.length > 0) { const clusteredLightingEnabled = scene.clusteredLightingEnabled; if (clusteredLightingEnabled) { const lightClusters = this.emptyWorldClusters; lightClusters.activate(); } renderer.setCameraUniforms(camera.camera, renderTarget); if (device.supportsUniformBuffers) { renderer.initViewBindGroupFormat(clusteredLightingEnabled); renderer.setupViewUniformBuffers(this.viewBindGroups, renderer.viewUniformFormat, renderer.viewBindGroupFormat, null); } const shaderPass = this.depth ? SHADER_DEPTH_PICK : SHADER_PICK; renderer.renderForward(camera.camera, renderTarget, tempMeshInstances$1, lights, shaderPass, (meshInstance)=>{ device.setBlendState(this.blendState); }); tempMeshInstances$1.length = 0; } } } } } constructor(device, renderer){ super(device), this.viewBindGroups = [], this.blendState = BlendState.NOBLEND; this.renderer = renderer; } } const tempSet = new Set(); const _rect = new Vec4(); const _floatView = new Float32Array(1); const _int32View = new Int32Array(_floatView.buffer); let Picker$1 = class Picker { destroy() { this.releaseRenderTarget(); this.renderPass?.destroy(); } getSelection(x, y, width = 1, height = 1) { const device = this.device; if (device.isWebGPU) { return []; } y = this.renderTarget.height - (y + height); const rect = this.sanitizeRect(x, y, width, height); device.setRenderTarget(this.renderTarget); device.updateBegin(); const pixels = new Uint8Array(4 * rect.z * rect.w); device.readPixels(rect.x, rect.y, rect.z, rect.w, pixels); device.updateEnd(); return this.decodePixels(pixels, this.mapping); } getSelectionAsync(x, y, width = 1, height = 1) { if (!this.renderTarget || !this.renderTarget.colorBuffer) { return Promise.resolve([]); } return this._readTexture(this.renderTarget.colorBuffer, x, y, width, height, this.renderTarget).then((pixels)=>{ return this.decodePixels(pixels, this.mapping); }); } _readTexture(texture, x, y, width, height, renderTarget) { if (this.device?.isWebGL2) { y = renderTarget.height - (y + height); } const rect = this.sanitizeRect(x, y, width, height); return texture.read(rect.x, rect.y, rect.z, rect.w, { immediate: true, renderTarget: renderTarget }); } async getWorldPointAsync(x, y) { const camera = this.renderPass.camera; if (!camera) { return null; } const viewProjMat = new Mat4().mul2(camera.camera.projectionMatrix, camera.camera.viewMatrix); const invViewProj = viewProjMat.invert(); const depth = await this.getPointDepthAsync(x, y); if (depth === null) { return null; } const deviceCoord = new Vec4(x / this.width * 2 - 1, (1 - y / this.height) * 2 - 1, depth * 2 - 1, 1.0); invViewProj.transformVec4(deviceCoord, deviceCoord); deviceCoord.mulScalar(1.0 / deviceCoord.w); return new Vec3(deviceCoord.x, deviceCoord.y, deviceCoord.z); } async getPointDepthAsync(x, y) { if (!this.depthBuffer) { return null; } const pixels = await this._readTexture(this.depthBuffer, x, y, 1, 1, this.renderTargetDepth); const intBits = pixels[0] << 24 | pixels[1] << 16 | pixels[2] << 8 | pixels[3]; if (intBits === 0xFFFFFFFF) { return null; } _int32View[0] = intBits; return _floatView[0]; } sanitizeRect(x, y, width, height) { const maxWidth = this.renderTarget.width; const maxHeight = this.renderTarget.height; x = math.clamp(Math.floor(x), 0, maxWidth - 1); y = math.clamp(Math.floor(y), 0, maxHeight - 1); width = Math.floor(Math.max(width, 1)); width = Math.min(width, maxWidth - x); height = Math.floor(Math.max(height, 1)); height = Math.min(height, maxHeight - y); return _rect.set(x, y, width, height); } decodePixels(pixels, mapping) { const selection = []; if (this.deviceValid) { const count = pixels.length; for(let i = 0; i < count; i += 4){ const r = pixels[i + 0]; const g = pixels[i + 1]; const b = pixels[i + 2]; const a = pixels[i + 3]; const index = (a << 24 | r << 16 | g << 8 | b) >>> 0; if (index !== 0xFFFFFFFF) { tempSet.add(mapping.get(index)); } } tempSet.forEach((meshInstance)=>{ if (meshInstance) { selection.push(meshInstance); } }); tempSet.clear(); } return selection; } createTexture(name) { return new Texture(this.device, { format: PIXELFORMAT_RGBA8, width: this.width, height: this.height, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: name }); } allocateRenderTarget() { this.colorBuffer = this.createTexture('pick'); const colorBuffers = [ this.colorBuffer ]; if (this.depth) { this.depthBuffer = this.createTexture('pick-depth'); colorBuffers.push(this.depthBuffer); this.renderTargetDepth = new RenderTarget({ colorBuffer: this.depthBuffer, depth: false }); } this.renderTarget = new RenderTarget({ colorBuffers: colorBuffers, depth: true }); } releaseRenderTarget() { this.renderTarget?.destroyTextureBuffers(); this.renderTarget?.destroy(); this.renderTarget = null; this.renderTargetDepth?.destroy(); this.renderTargetDepth = null; this.colorBuffer = null; this.depthBuffer = null; } prepare(camera, scene, layers) { if (layers instanceof Layer) { layers = [ layers ]; } this.renderTarget?.resize(this.width, this.height); this.renderTargetDepth?.resize(this.width, this.height); this.mapping.clear(); const renderPass = this.renderPass; renderPass.init(this.renderTarget); renderPass.setClearColor(Color.WHITE); renderPass.depthStencilOps.clearDepth = true; renderPass.update(camera, scene, layers, this.mapping, this.depth); renderPass.render(); } resize(width, height) { this.width = Math.floor(width); this.height = Math.floor(height); } constructor(app, width, height, depth = false){ this.renderTarget = null; this.colorBuffer = null; this.depthBuffer = null; this.renderTargetDepth = null; this.mapping = new Map(); this.deviceValid = true; this.device = app.graphicsDevice; this.renderPass = new RenderPassPicker(this.device, app.renderer); this.depth = depth; this.width = 0; this.height = 0; this.resize(width, height); this.allocateRenderTarget(); this.device.on('destroy', ()=>{ this.deviceValid = false; }); } }; class CpuTimer { begin(name) { if (!this.enabled) { return; } if (this._frameIndex < this._frameTimings.length) { this._frameTimings.splice(this._frameIndex); } const tmp = this._prevTimings; this._prevTimings = this._timings; this._timings = this._frameTimings; this._frameTimings = tmp; this._frameIndex = 0; this.mark(name); } mark(name) { if (!this.enabled) { return; } const timestamp = now(); if (this._frameIndex > 0) { const prev = this._frameTimings[this._frameIndex - 1]; prev[1] = timestamp - prev[1]; } else if (this._timings.length > 0) { const prev = this._timings[this._timings.length - 1]; prev[1] = timestamp - prev[1]; } if (this._frameIndex >= this._frameTimings.length) { this._frameTimings.push([ name, timestamp ]); } else { const timing = this._frameTimings[this._frameIndex]; timing[0] = name; timing[1] = timestamp; } this._frameIndex++; } get timings() { return this._timings.slice(0, -1).map((v)=>v[1]); } constructor(app){ this._frameIndex = 0; this._frameTimings = []; this._timings = []; this._prevTimings = []; this.unitsName = 'ms'; this.decimalPlaces = 1; this.enabled = true; app.on('frameupdate', this.begin.bind(this, 'update')); app.on('framerender', this.mark.bind(this, 'render')); app.on('frameend', this.mark.bind(this, 'other')); } } class GpuTimer { get timings() { this._timings[0] = this.device.gpuProfiler?._frameTime ?? 0; return this._timings; } constructor(device){ this.device = device; if (device.gpuProfiler) { device.gpuProfiler.enabled = true; } this.enabled = true; this.unitsName = 'ms'; this.decimalPlaces = 1; this._timings = []; } } class StatsTimer { get timings() { return this.values; } constructor(app, statNames, decimalPlaces, unitsName, multiplier){ this.app = app; this.values = []; this.statNames = statNames; if (this.statNames.length > 3) { this.statNames.length = 3; } this.unitsName = unitsName; this.decimalPlaces = decimalPlaces; this.multiplier = multiplier || 1; const resolve = (path, obj)=>{ return path.split('.').reduce((prev, curr)=>{ if (!prev) return null; if (prev instanceof Map) { return prev.get(curr); } return prev[curr]; }, obj || this); }; app.on('frameupdate', (ms)=>{ for(let i = 0; i < this.statNames.length; i++){ const value = resolve(this.statNames[i], this.app.stats); this.values[i] = (value ?? 0) * this.multiplier; } }); } } class Graph { destroy() { this.app.off('frameupdate', this.update, this); } loseContext() { if (this.timer && typeof this.timer.loseContext === 'function') { this.timer.loseContext(); } } update(ms) { const timings = this.timer.timings; const total = timings.reduce((a, v)=>a + v, 0); this.avgTotal += total; this.avgTimer += ms; this.avgCount++; this.maxValue = Math.max(this.maxValue, total); if (this.avgTimer > this.textRefreshRate) { this.timingText = (this.avgTotal / this.avgCount).toFixed(this.timer.decimalPlaces); this.maxText = this.maxValue.toFixed(this.timer.decimalPlaces); this.avgTimer = 0; this.avgTotal = 0; this.avgCount = 0; this.maxValue = 0; } if (this.enabled) { let value = 0; const range = 1.5 * this.watermark; for(let i = 0; i < timings.length; ++i){ value += Math.floor(timings[i] / range * 255); this.sample[i] = value; } this.sample[3] = this.watermark / range * 255; if (this.yOffset >= this.texture.height) { return; } const data = this.texture.lock(); if (this.needsClear) { const rowOffset = this.yOffset * this.texture.width * 4; data.fill(0, rowOffset, rowOffset + this.texture.width * 4); this.needsClear = false; } data.set(this.sample, (this.cursor + this.yOffset * this.texture.width) * 4); this.texture.unlock(); this.cursor++; if (this.cursor === this.texture.width) { this.cursor = 0; } } } render(render2d, x, y, w, h) { render2d.quad(x + w, y, -w, h, this.enabled ? this.cursor : 0, this.enabled ? 0.5 + this.yOffset : this.texture.height - 1, -w, 0, this.texture, this.graphType); } constructor(name, app, watermark, textRefreshRate, timer){ this.app = app; this.name = name; this.device = app.graphicsDevice; this.timer = timer; this.watermark = watermark; this.enabled = false; this.textRefreshRate = textRefreshRate; this.avgTotal = 0; this.avgTimer = 0; this.avgCount = 0; this.maxValue = 0; this.timingText = ''; this.maxText = ''; this.texture = null; this.yOffset = 0; this.graphType = 0.0; this.cursor = 0; this.sample = new Uint8ClampedArray(4); this.sample.set([ 0, 0, 0, 255 ]); this.needsClear = false; this.counter = 0; this.app.on('frameupdate', this.update, this); } } class WordAtlas { destroy() { this.texture.destroy(); this.texture = null; } render(render2d, word, x, y) { const p = this.placements.get(word); if (p) { const padding = 1; render2d.quad(x + p.l - padding, y - p.d + padding, p.w + padding * 2, p.h + padding * 2, p.x - padding, this.texture.height - p.y - p.h - padding, undefined, undefined, this.texture, 1); return p.w; } let totalWidth = 0; for(let i = 0; i < word.length; i++){ const char = word[i]; if (char === ' ') { totalWidth += 5; continue; } const charPlacement = this.placements.get(char); if (charPlacement) { const padding = 1; render2d.quad(x + totalWidth + charPlacement.l - padding, y - charPlacement.d + padding, charPlacement.w + padding * 2, charPlacement.h + padding * 2, charPlacement.x - padding, this.texture.height - charPlacement.y - charPlacement.h - padding, undefined, undefined, this.texture, 1); totalWidth += charPlacement.w; } } return totalWidth; } constructor(device, words){ const initContext = (context)=>{ context.font = '10px "Lucida Console", Monaco, monospace'; context.textAlign = 'left'; context.textBaseline = 'alphabetic'; }; const isNumber = (word)=>{ return word === '.' || word.length === 1 && word.charCodeAt(0) >= 48 && word.charCodeAt(0) <= 57; }; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d', { alpha: true }); initContext(context); const placements = new Map(); const padding = 5; const width = 512; let x = padding; let y = padding; words.forEach((word)=>{ const measurement = context.measureText(word); const l = Math.ceil(-measurement.actualBoundingBoxLeft); const r = Math.ceil(measurement.actualBoundingBoxRight); const a = Math.ceil(measurement.actualBoundingBoxAscent); const d = Math.ceil(measurement.actualBoundingBoxDescent); const w = l + r; const h = a + d; if (x + w + padding >= width) { x = padding; y += 16; } placements.set(word, { l, r, a, d, w, h, x: x, y: y }); x += w + padding; }); canvas.width = 512; canvas.height = math.nextPowerOfTwo(y + 16 + padding); initContext(context); context.fillStyle = 'rgb(0, 0, 0)'; context.fillRect(0, 0, canvas.width, canvas.height); placements.forEach((m, word)=>{ context.fillStyle = isNumber(word) ? 'rgb(255, 240, 100)' : 'rgb(150, 220, 230)'; context.fillText(word, m.x - m.l, m.y + m.a); }); this.placements = placements; const data = context.getImageData(0, 0, canvas.width, canvas.height).data; for(let i = 0; i < data.length; i += 4){ const maxChannel = Math.max(data[i + 0], data[i + 1], data[i + 2]); data[i + 3] = Math.min(maxChannel * 2, 255); } this.texture = new Texture(device, { name: 'mini-stats-word-atlas', width: canvas.width, height: canvas.height, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, levels: [ data ] }); } } const graphColorRed = '1.0, 0.412, 0.380'; const graphColorGreen = '0.467, 0.867, 0.467'; const graphColorBlue = '0.424, 0.627, 0.863'; const mainBackgroundColor = '0.0, 0.0, 0.0'; const gpuBackgroundColor = '0.15, 0.15, 0.0'; const cpuBackgroundColor = '0.15, 0.0, 0.1'; const vertexShaderGLSL = ` attribute vec3 vertex_position; attribute vec4 vertex_texCoord0; varying vec4 uv0; varying float wordFlag; void main(void) { gl_Position = vec4(vertex_position.xy * 2.0 - 1.0, 0.5, 1.0); uv0 = vertex_texCoord0; wordFlag = vertex_position.z; } `; const vertexShaderWGSL = ` attribute vertex_position: vec3f; attribute vertex_texCoord0: vec4f; varying uv0: vec4f; varying wordFlag: f32; @vertex fn vertexMain(input : VertexInput) -> VertexOutput { var output : VertexOutput; output.position = vec4(input.vertex_position.xy * 2.0 - 1.0, 0.5, 1.0); output.uv0 = input.vertex_texCoord0; output.wordFlag = input.vertex_position.z; return output; } `; const fragmentShaderGLSL = ` varying vec4 uv0; varying float wordFlag; uniform vec4 clr; uniform sampler2D graphTex; uniform sampler2D wordsTex; void main (void) { vec4 graphSample = texture2D(graphTex, uv0.xy); vec4 graph; if (uv0.w < graphSample.r) graph = vec4(${graphColorRed}, 1.0); else if (uv0.w < graphSample.g) graph = vec4(${graphColorGreen}, 1.0); else if (uv0.w < graphSample.b) graph = vec4(${graphColorBlue}, 1.0); else { vec3 bgColor = vec3(${mainBackgroundColor}); if (wordFlag > 0.5) { bgColor = vec3(${cpuBackgroundColor}); } else if (wordFlag > 0.2) { bgColor = vec3(${gpuBackgroundColor}); } graph = vec4(bgColor, 1.0); } vec4 words = texture2D(wordsTex, vec2(uv0.x, 1.0 - uv0.y)); if (wordFlag > 0.99) { gl_FragColor = words * clr; } else { gl_FragColor = graph * clr; } } `; const fragmentShaderWGSL = ` varying uv0: vec4f; varying wordFlag: f32; uniform clr: vec4f; var graphTex : texture_2d; var graphTex_sampler : sampler; var wordsTex : texture_2d; var wordsTex_sampler : sampler; @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var uv0: vec4f = input.uv0; var graphSample: vec4f = textureSample(graphTex, graphTex_sampler, uv0.xy); var graph: vec4f; if (uv0.w < graphSample.r) { graph = vec4f(${graphColorRed}, 1.0); } else if (uv0.w < graphSample.g) { graph = vec4f(${graphColorGreen}, 1.0); } else if (uv0.w < graphSample.b) { graph = vec4f(${graphColorBlue}, 1.0); } else { var bgColor: vec3f = vec3f(${mainBackgroundColor}); if (input.wordFlag > 0.5) { bgColor = vec3f(${cpuBackgroundColor}); } else if (input.wordFlag > 0.2) { bgColor = vec3f(${gpuBackgroundColor}); } graph = vec4f(bgColor, 1.0); } var words: vec4f = textureSample(wordsTex, wordsTex_sampler, vec2f(uv0.x, 1.0 - uv0.y)); var output: FragmentOutput; if (input.wordFlag > 0.99) { output.color = words * uniform.clr; } else { output.color = graph * uniform.clr; } return output; } `; class Render2d { quad(x, y, w, h, u, v, uw, uh, texture, wordFlag = 0) { if (this.quads >= this.maxQuads) { return; } const rw = this.targetSize.width; const rh = this.targetSize.height; const x0 = x / rw; const y0 = y / rh; const x1 = (x + w) / rw; const y1 = (y + h) / rh; const tw = texture.width; const th = texture.height; const u0 = u / tw; const v0 = v / th; const u1 = (u + (uw ?? w)) / tw; const v1 = (v + (uh ?? h)) / th; this.data.set([ x0, y0, wordFlag, u0, v0, 0, 0, x1, y0, wordFlag, u1, v0, 1, 0, x1, y1, wordFlag, u1, v1, 1, 1, x0, y1, wordFlag, u0, v1, 0, 1 ], 4 * 7 * this.quads); this.quads++; this.prim.count += 6; } startFrame() { this.quads = 0; this.prim.count = 0; this.targetSize.width = this.device.canvas.scrollWidth; this.targetSize.height = this.device.canvas.scrollHeight; } render(app, layer, graphTexture, wordsTexture, clr, height) { this.buffer.setData(this.data.buffer); this.uniforms.clr.set(clr, 0); this.material.setParameter('clr', this.uniforms.clr); this.material.setParameter('graphTex', graphTexture); this.material.setParameter('wordsTex', wordsTexture); app.drawMeshInstance(this.meshInstance, layer); } constructor(device, maxQuads = 2048){ const format = new VertexFormat(device, [ { semantic: SEMANTIC_POSITION, components: 3, type: TYPE_FLOAT32 }, { semantic: SEMANTIC_TEXCOORD0, components: 4, type: TYPE_FLOAT32 } ]); const indices = new Uint16Array(maxQuads * 6); for(let i = 0; i < maxQuads; ++i){ indices[i * 6 + 0] = i * 4; indices[i * 6 + 1] = i * 4 + 1; indices[i * 6 + 2] = i * 4 + 2; indices[i * 6 + 3] = i * 4; indices[i * 6 + 4] = i * 4 + 2; indices[i * 6 + 5] = i * 4 + 3; } this.device = device; this.maxQuads = maxQuads; this.buffer = new VertexBuffer(device, format, maxQuads * 4, { usage: BUFFER_STREAM }); this.data = new Float32Array(this.buffer.numBytes / 4); this.indexBuffer = new IndexBuffer(device, INDEXFORMAT_UINT16, maxQuads * 6, BUFFER_STATIC, indices); this.prim = { type: PRIMITIVE_TRIANGLES, indexed: true, base: 0, baseVertex: 0, count: 0 }; this.quads = 0; this.mesh = new Mesh(device); this.mesh.vertexBuffer = this.buffer; this.mesh.indexBuffer[0] = this.indexBuffer; this.mesh.primitive = [ this.prim ]; const material = new ShaderMaterial({ uniqueName: 'MiniStats', vertexGLSL: vertexShaderGLSL, fragmentGLSL: fragmentShaderGLSL, vertexWGSL: vertexShaderWGSL, fragmentWGSL: fragmentShaderWGSL, attributes: { vertex_position: SEMANTIC_POSITION, vertex_texCoord0: SEMANTIC_TEXCOORD0 } }); this.material = material; material.cull = CULLFACE_NONE; material.depthState = DepthState.NODEPTH; material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE); material.update(); this.meshInstance = new MeshInstance(this.mesh, material, new GraphNode('MiniStatsMesh')); this.uniforms = { clr: new Float32Array(4) }; this.targetSize = { width: device.width, height: device.height }; } } const cpuStatDisplayNames = { animUpdate: 'anim', physicsTime: 'physics', renderTime: 'render', gsplatSort: 'gsplatSort' }; const delayedStartStats = new Set([ 'physicsTime', 'animUpdate', 'gsplatSort' ]); class MiniStats { destroy() { this.device.off('resizecanvas', this.updateDiv, this); this.device.off('losecontext', this.loseContext, this); this.app.off('postrender', this.postRender, this); this.graphs.forEach((graph)=>graph.destroy()); this.gpuPassGraphs.clear(); this.wordAtlas.destroy(); this.texture.destroy(); this.div.remove(); } static getDefaultOptions() { return { sizes: [ { width: 100, height: 16, spacing: 0, graphs: false }, { width: 128, height: 32, spacing: 2, graphs: true }, { width: 256, height: 64, spacing: 2, graphs: true } ], startSizeIndex: 0, textRefreshRate: 500, cpu: { enabled: true, watermark: 33 }, gpu: { enabled: true, watermark: 33 }, stats: [ { name: 'Frame', stats: [ 'frame.ms' ], decimalPlaces: 1, unitsName: 'ms', watermark: 33 }, { name: 'DrawCalls', stats: [ 'drawCalls.total' ], watermark: 1000 } ], gpuTimingMinSize: 1, cpuTimingMinSize: 1 }; } set activeSizeIndex(value) { this._activeSizeIndex = value; this.gspacing = this.sizes[value].spacing; this.resize(this.sizes[value].width, this.sizes[value].height, this.sizes[value].graphs); this.opacity = value > 0 ? 0.85 : 0.7; if (value < this.gpuTimingMinSize && this.gpuPassGraphs) { for (const passData of this.gpuPassGraphs.values()){ const index = this.graphs.indexOf(passData.graph); if (index !== -1) { this.graphs.splice(index, 1); } this.freeRow(passData.graph); passData.graph.destroy(); } this.gpuPassGraphs.clear(); const gpuGraph = this.graphs.find((g)=>g.name === 'GPU'); if (gpuGraph) gpuGraph.graphType = 0.0; } if (value < this.cpuTimingMinSize && this.cpuGraphs) { for (const statData of this.cpuGraphs.values()){ const index = this.graphs.indexOf(statData.graph); if (index !== -1) { this.graphs.splice(index, 1); } this.freeRow(statData.graph); statData.graph.destroy(); } this.cpuGraphs.clear(); const cpuGraph = this.graphs.find((g)=>g.name === 'CPU'); if (cpuGraph) cpuGraph.graphType = 0.0; } } get activeSizeIndex() { return this._activeSizeIndex; } set opacity(value) { this.clr[3] = value; } get opacity() { return this.clr[3]; } get overallHeight() { const graphs = this.graphs; const spacing = this.gspacing; return this.height * graphs.length + spacing * (graphs.length - 1); } set enabled(value) { if (value !== this._enabled) { this._enabled = value; for(let i = 0; i < this.graphs.length; ++i){ this.graphs[i].enabled = value; this.graphs[i].timer.enabled = value; } } } get enabled() { return this._enabled; } initGraphs(app, device, options) { this.graphs = []; if (options.cpu.enabled) { const timer = new CpuTimer(app); const graph = new Graph('CPU', app, options.cpu.watermark, options.textRefreshRate, timer); this.graphs.push(graph); } if (options.gpu.enabled) { const timer = new GpuTimer(device); const graph = new Graph('GPU', app, options.gpu.watermark, options.textRefreshRate, timer); this.graphs.push(graph); } if (options.stats) { options.stats.forEach((entry)=>{ const timer = new StatsTimer(app, entry.stats, entry.decimalPlaces, entry.unitsName, entry.multiplier); const graph = new Graph(entry.name, app, entry.watermark, options.textRefreshRate, timer); this.graphs.push(graph); }); } this.texture = new Texture(device, { name: 'mini-stats-graph-texture', width: 1, height: 1, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_REPEAT, addressV: ADDRESS_REPEAT }); this.graphs.forEach((graph)=>{ graph.texture = this.texture; this.allocateRow(graph); }); } render() { const graphs = this.graphs; const wordAtlas = this.wordAtlas; const render2d = this.render2d; const width = this.width; const height = this.height; const gspacing = this.gspacing; render2d.startFrame(); for(let i = 0; i < graphs.length; ++i){ const graph = graphs[i]; let y = i * (height + gspacing); graph.render(render2d, 0, y, width, height); let x = 1; y += height - 13; x += wordAtlas.render(render2d, graph.name, x, y) + 10; const timingText = graph.timingText; for(let j = 0; j < timingText.length; ++j){ x += wordAtlas.render(render2d, timingText[j], x, y); } if (graph.maxText && this._activeSizeIndex > 0) { x += 5; x += wordAtlas.render(render2d, 'max', x, y); x += 5; const maxText = graph.maxText; for(let j = 0; j < maxText.length; ++j){ x += wordAtlas.render(render2d, maxText[j], x, y); } } if (graph.timer.unitsName) { x += wordAtlas.render(render2d, graph.timer.unitsName, x, y); } } render2d.render(this.app, this.drawLayer, this.texture, this.wordAtlas.texture, this.clr, height); } resize(width, height, showGraphs) { const graphs = this.graphs; for(let i = 0; i < graphs.length; ++i){ graphs[i].enabled = showGraphs; } this.width = width; this.height = height; this.updateDiv(); } updateDiv() { const rect = this.device.canvas.getBoundingClientRect(); this.div.style.left = `${rect.left}px`; this.div.style.bottom = `${window.innerHeight - rect.bottom}px`; this.div.style.width = `${this.width}px`; this.div.style.height = `${this.overallHeight}px`; } loseContext() { this.graphs.forEach((graph)=>graph.loseContext()); } updateSubStats(subGraphs, mainGraphName, stats, statPathPrefix, removeAfterFrames) { const passesToRemove = []; for (const [statName, statData] of subGraphs){ const timing = stats instanceof Map ? stats.get(statName) || 0 : stats[statName] || 0; if (timing > 0) { statData.lastNonZeroFrame = this.frameIndex; } else if (removeAfterFrames > 0) { const shouldAutoHide = statPathPrefix === 'gpu'; if (shouldAutoHide && this.frameIndex - statData.lastNonZeroFrame > removeAfterFrames) { passesToRemove.push(statName); } } } for (const statName of passesToRemove){ const statData = subGraphs.get(statName); if (statData) { const index = this.graphs.indexOf(statData.graph); if (index !== -1) { this.graphs.splice(index, 1); } this.freeRow(statData.graph); statData.graph.destroy(); subGraphs.delete(statName); } } const statsEntries = stats instanceof Map ? stats : Object.entries(stats); for (const [statName, timing] of statsEntries){ if (!subGraphs.has(statName)) { const isDelayedStart = statPathPrefix === 'gpu' || delayedStartStats.has(statName); if (isDelayedStart && timing === 0) { continue; } let displayName = statName; if (statPathPrefix === 'frame') { displayName = cpuStatDisplayNames[statName] || statName; } const graphName = ` ${displayName}`; const watermark = 10.0; const statPath = `${statPathPrefix}.${statName}`; const timer = new StatsTimer(this.app, [ statPath ], 1, 'ms', 1); const graph = new Graph(graphName, this.app, watermark, this.textRefreshRate, timer); if (statPathPrefix === 'gpu') { graph.graphType = 0.33; } else if (statPathPrefix === 'frame') { graph.graphType = 0.66; } graph.texture = this.texture; this.allocateRow(graph); const currentSize = this.sizes[this._activeSizeIndex]; graph.enabled = currentSize.graphs; let mainGraphIndex = this.graphs.findIndex((g)=>g.name === mainGraphName); if (mainGraphIndex === -1) { mainGraphIndex = 0; } let insertIndex = mainGraphIndex; for(let i = mainGraphIndex - 1; i >= 0; i--){ if (this.graphs[i].name.startsWith(' ')) { insertIndex = i; } else { break; } } this.graphs.splice(insertIndex, 0, graph); subGraphs.set(statName, { graph: graph, lastNonZeroFrame: timing > 0 ? this.frameIndex : this.frameIndex - removeAfterFrames - 1 }); } } const mainGraph = this.graphs.find((g)=>g.name === mainGraphName); if (mainGraph) { for (const statData of subGraphs.values()){ statData.graph.watermark = mainGraph.watermark; } if (subGraphs.size > 0) { if (statPathPrefix === 'gpu') { mainGraph.graphType = 0.33; } else if (statPathPrefix === 'frame') { mainGraph.graphType = 0.66; } } else { mainGraph.graphType = 0.0; } } } allocateRow(graph) { let row; if (this.freeRows.length > 0) { row = this.freeRows.pop(); } else { row = this.nextRowIndex++; this.ensureTextureHeight(this.nextRowIndex); } this.graphRows.set(graph, row); graph.yOffset = row; graph.needsClear = true; return row; } freeRow(graph) { const row = this.graphRows.get(graph); if (row !== undefined) { this.freeRows.push(row); this.graphRows.delete(graph); } } ensureTextureHeight(requiredRows) { const maxWidth = this.sizes[this.sizes.length - 1].width; const requiredWidth = math.nextPowerOfTwo(maxWidth); const requiredHeight = math.nextPowerOfTwo(requiredRows); if (requiredHeight > this.texture.height) { this.texture.resize(requiredWidth, requiredHeight); } } postRender() { if (this._enabled) { this.render(); if (this._activeSizeIndex >= this.gpuTimingMinSize) { const gpuStats = this.app.stats.gpu; if (gpuStats) { this.updateSubStats(this.gpuPassGraphs, 'GPU', gpuStats, 'gpu', 240); } } if (this._activeSizeIndex >= this.cpuTimingMinSize) { const cpuStats = { scriptUpdate: this.app.stats.frame.scriptUpdate, scriptPostUpdate: this.app.stats.frame.scriptPostUpdate, animUpdate: this.app.stats.frame.animUpdate, physicsTime: this.app.stats.frame.physicsTime, renderTime: this.app.stats.frame.renderTime, gsplatSort: this.app.stats.frame.gsplatSort }; this.updateSubStats(this.cpuGraphs, 'CPU', cpuStats, 'frame', 240); } } this.frameIndex++; } constructor(app, options = MiniStats.getDefaultOptions()){ const device = app.graphicsDevice; this.graphRows = new Map(); this.freeRows = []; this.nextRowIndex = 0; this.sizes = options.sizes; this.initGraphs(app, device, options); const words = new Set([ '', 'ms', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', ' ' ].concat(this.graphs.map((graph)=>graph.name)).concat(options.stats ? options.stats.map((stat)=>stat.unitsName) : []).filter((item)=>!!item)); for(let i = 97; i <= 122; i++){ words.add(String.fromCharCode(i)); } for(let i = 65; i <= 90; i++){ words.add(String.fromCharCode(i)); } this.wordAtlas = new WordAtlas(device, words); this._activeSizeIndex = options.startSizeIndex; const gpuTimingMinSize = options.gpuTimingMinSize ?? 1; const cpuTimingMinSize = options.cpuTimingMinSize ?? 1; if (gpuTimingMinSize < this.sizes.length || cpuTimingMinSize < this.sizes.length) { const lastWidth = this.sizes[this.sizes.length - 1].width; for(let i = 1; i < this.sizes.length - 1; i++){ this.sizes[i].width = lastWidth; } } const div = document.createElement('div'); div.setAttribute('id', 'mini-stats'); div.style.cssText = 'position:fixed;bottom:0;left:0;background:transparent;'; document.body.appendChild(div); div.addEventListener('mouseenter', (event)=>{ this.opacity = 1.0; }); div.addEventListener('mouseleave', (event)=>{ this.opacity = this._activeSizeIndex > 0 ? 0.85 : 0.7; }); div.addEventListener('click', (event)=>{ event.preventDefault(); if (this._enabled) { this.activeSizeIndex = (this.activeSizeIndex + 1) % this.sizes.length; this.resize(this.sizes[this.activeSizeIndex].width, this.sizes[this.activeSizeIndex].height, this.sizes[this.activeSizeIndex].graphs); } }); device.on('resizecanvas', this.updateDiv, this); device.on('losecontext', this.loseContext, this); app.on('postrender', this.postRender, this); this.app = app; this.drawLayer = app.scene.layers.getLayerById(LAYERID_UI); this.device = device; this.render2d = new Render2d(device); this.div = div; this.width = 0; this.height = 0; this.gspacing = 2; this.clr = [ 1, 1, 1, options.startSizeIndex > 0 ? 0.85 : 0.7 ]; this._enabled = true; this.gpuTimingMinSize = gpuTimingMinSize; this.gpuPassGraphs = new Map(); this.cpuTimingMinSize = cpuTimingMinSize; this.cpuGraphs = new Map(); this.frameIndex = 0; this.textRefreshRate = options.textRefreshRate; this.activeSizeIndex = this._activeSizeIndex; } } const SSAOTYPE_NONE = 'none'; const SSAOTYPE_LIGHTING = 'lighting'; const SSAOTYPE_COMBINE = 'combine'; var glslDownsamplePS = ` uniform sampler2D sourceTexture; uniform vec2 sourceInvResolution; varying vec2 uv0; #ifdef PREMULTIPLY uniform sampler2D premultiplyTexture; #endif void main() { vec3 e = texture2D (sourceTexture, uv0).rgb; #ifdef BOXFILTER vec3 value = e; #ifdef PREMULTIPLY float premultiply = texture2D(premultiplyTexture, uv0).{PREMULTIPLY_SRC_CHANNEL}; value *= vec3(premultiply); #endif #else float x = sourceInvResolution.x; float y = sourceInvResolution.y; vec3 a = texture2D(sourceTexture, vec2 (uv0.x - 2.0 * x, uv0.y + 2.0 * y)).rgb; vec3 b = texture2D(sourceTexture, vec2 (uv0.x, uv0.y + 2.0 * y)).rgb; vec3 c = texture2D(sourceTexture, vec2 (uv0.x + 2.0 * x, uv0.y + 2.0 * y)).rgb; vec3 d = texture2D(sourceTexture, vec2 (uv0.x - 2.0 * x, uv0.y)).rgb; vec3 f = texture2D(sourceTexture, vec2 (uv0.x + 2.0 * x, uv0.y)).rgb; vec3 g = texture2D(sourceTexture, vec2 (uv0.x - 2.0 * x, uv0.y - 2.0 * y)).rgb; vec3 h = texture2D(sourceTexture, vec2 (uv0.x, uv0.y - 2.0 * y)).rgb; vec3 i = texture2D(sourceTexture, vec2 (uv0.x + 2.0 * x, uv0.y - 2.0 * y)).rgb; vec3 j = texture2D(sourceTexture, vec2 (uv0.x - x, uv0.y + y)).rgb; vec3 k = texture2D(sourceTexture, vec2 (uv0.x + x, uv0.y + y)).rgb; vec3 l = texture2D(sourceTexture, vec2 (uv0.x - x, uv0.y - y)).rgb; vec3 m = texture2D(sourceTexture, vec2 (uv0.x + x, uv0.y - y)).rgb; vec3 value = e * 0.125; value += (a + c + g + i) * 0.03125; value += (b + d + f + h) * 0.0625; value += (j + k + l + m) * 0.125; #endif #ifdef REMOVE_INVALID value = max(value, vec3(0.0)); #endif gl_FragColor = vec4(value, 1.0); } `; var wgslDownsamplePS = ` var sourceTexture: texture_2d; var sourceTextureSampler: sampler; uniform sourceInvResolution: vec2f; varying uv0: vec2f; #ifdef PREMULTIPLY var premultiplyTexture: texture_2d; var premultiplyTextureSampler: sampler; #endif @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let e: vec3f = textureSample(sourceTexture, sourceTextureSampler, input.uv0).rgb; #ifdef BOXFILTER var value: vec3f = e; #ifdef PREMULTIPLY let premultiply: f32 = textureSample(premultiplyTexture, premultiplyTextureSampler, input.uv0).{PREMULTIPLY_SRC_CHANNEL}; value = value * vec3f(premultiply); #endif #else let x: f32 = uniform.sourceInvResolution.x; let y: f32 = uniform.sourceInvResolution.y; let a: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - 2.0 * x, input.uv0.y + 2.0 * y)).rgb; let b: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x, input.uv0.y + 2.0 * y)).rgb; let c: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + 2.0 * x, input.uv0.y + 2.0 * y)).rgb; let d: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - 2.0 * x, input.uv0.y)).rgb; let f: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + 2.0 * x, input.uv0.y)).rgb; let g: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - 2.0 * x, input.uv0.y - 2.0 * y)).rgb; let h: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x, input.uv0.y - 2.0 * y)).rgb; let i: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + 2.0 * x, input.uv0.y - 2.0 * y)).rgb; let j: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - x, input.uv0.y + y)).rgb; let k: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + x, input.uv0.y + y)).rgb; let l: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - x, input.uv0.y - y)).rgb; let m: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + x, input.uv0.y - y)).rgb; var value: vec3f = e * 0.125; value = value + (a + c + g + i) * 0.03125; value = value + (b + d + f + h) * 0.0625; value = value + (j + k + l + m) * 0.125; #endif #ifdef REMOVE_INVALID value = max(value, vec3f(0.0)); #endif output.color = vec4f(value, 1.0); return output; } `; class RenderPassDownsample extends RenderPassShaderQuad { setSourceTexture(value) { this._sourceTexture = value; this.options.resizeSource = value; } execute() { this.sourceTextureId.setValue(this.sourceTexture); if (this.premultiplyTexture) { this.premultiplyTextureId.setValue(this.premultiplyTexture); } this.sourceInvResolutionValue[0] = 1.0 / this.sourceTexture.width; this.sourceInvResolutionValue[1] = 1.0 / this.sourceTexture.height; this.sourceInvResolutionId.setValue(this.sourceInvResolutionValue); super.execute(); } constructor(device, sourceTexture, options = {}){ super(device); this.sourceTexture = sourceTexture; this.premultiplyTexture = options.premultiplyTexture; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('downsamplePS', glslDownsamplePS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('downsamplePS', wgslDownsamplePS); const boxFilter = options.boxFilter ?? false; const key = `${boxFilter ? 'Box' : ''}-${options.premultiplyTexture ? 'Premultiply' : ''}-${options.premultiplySrcChannel ?? ''}-${options.removeInvalid ? 'RemoveInvalid' : ''}`; const defines = new Map(); if (boxFilter) defines.set('BOXFILTER', ''); if (options.premultiplyTexture) defines.set('PREMULTIPLY', ''); if (options.removeInvalid) defines.set('REMOVE_INVALID', ''); defines.set('{PREMULTIPLY_SRC_CHANNEL}', options.premultiplySrcChannel ?? 'x'); this.shader = ShaderUtils.createShader(device, { uniqueName: `DownSampleShader:${key}`, attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'downsamplePS', fragmentDefines: defines }); this.sourceTextureId = device.scope.resolve('sourceTexture'); this.premultiplyTextureId = device.scope.resolve('premultiplyTexture'); this.sourceInvResolutionId = device.scope.resolve('sourceInvResolution'); this.sourceInvResolutionValue = new Float32Array(2); } } var glslUpsamplePS = ` uniform sampler2D sourceTexture; uniform vec2 sourceInvResolution; varying vec2 uv0; void main() { float x = sourceInvResolution.x; float y = sourceInvResolution.y; vec3 a = texture2D (sourceTexture, vec2 (uv0.x - x, uv0.y + y)).rgb; vec3 b = texture2D (sourceTexture, vec2 (uv0.x, uv0.y + y)).rgb; vec3 c = texture2D (sourceTexture, vec2 (uv0.x + x, uv0.y + y)).rgb; vec3 d = texture2D (sourceTexture, vec2 (uv0.x - x, uv0.y)).rgb; vec3 e = texture2D (sourceTexture, vec2 (uv0.x, uv0.y)).rgb; vec3 f = texture2D (sourceTexture, vec2 (uv0.x + x, uv0.y)).rgb; vec3 g = texture2D (sourceTexture, vec2 (uv0.x - x, uv0.y - y)).rgb; vec3 h = texture2D (sourceTexture, vec2 (uv0.x, uv0.y - y)).rgb; vec3 i = texture2D (sourceTexture, vec2 (uv0.x + x, uv0.y - y)).rgb; vec3 value = e * 4.0; value += (b + d + f + h) * 2.0; value += (a + c + g + i); value *= 1.0 / 16.0; gl_FragColor = vec4(value, 1.0); } `; var wgslUpsamplePS = ` var sourceTexture: texture_2d; var sourceTextureSampler: sampler; uniform sourceInvResolution: vec2f; varying uv0: vec2f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let x: f32 = uniform.sourceInvResolution.x; let y: f32 = uniform.sourceInvResolution.y; let a: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - x, input.uv0.y + y)).rgb; let b: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x, input.uv0.y + y)).rgb; let c: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + x, input.uv0.y + y)).rgb; let d: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - x, input.uv0.y)).rgb; let e: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x, input.uv0.y)).rgb; let f: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + x, input.uv0.y)).rgb; let g: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x - x, input.uv0.y - y)).rgb; let h: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x, input.uv0.y - y)).rgb; let i: vec3f = textureSample(sourceTexture, sourceTextureSampler, vec2f(input.uv0.x + x, input.uv0.y - y)).rgb; var value: vec3f = e * 4.0; value = value + (b + d + f + h) * 2.0; value = value + (a + c + g + i); value = value * (1.0 / 16.0); output.color = vec4f(value, 1.0); return output; } `; class RenderPassUpsample extends RenderPassShaderQuad { execute() { this.sourceTextureId.setValue(this.sourceTexture); this.sourceInvResolutionValue[0] = 1.0 / this.sourceTexture.width; this.sourceInvResolutionValue[1] = 1.0 / this.sourceTexture.height; this.sourceInvResolutionId.setValue(this.sourceInvResolutionValue); super.execute(); } constructor(device, sourceTexture){ super(device); this.sourceTexture = sourceTexture; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('upsamplePS', glslUpsamplePS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('upsamplePS', wgslUpsamplePS); this.shader = ShaderUtils.createShader(device, { uniqueName: 'UpSampleShader', attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'upsamplePS' }); this.sourceTextureId = device.scope.resolve('sourceTexture'); this.sourceInvResolutionId = device.scope.resolve('sourceInvResolution'); this.sourceInvResolutionValue = new Float32Array(2); } } class RenderPassBloom extends RenderPass { destroy() { this.destroyRenderPasses(); this.destroyRenderTargets(); } destroyRenderTargets(startIndex = 0) { for(let i = startIndex; i < this.renderTargets.length; i++){ const rt = this.renderTargets[i]; rt.destroyTextureBuffers(); rt.destroy(); } this.renderTargets.length = 0; } destroyRenderPasses() { for(let i = 0; i < this.beforePasses.length; i++){ this.beforePasses[i].destroy(); } this.beforePasses.length = 0; } createRenderTarget(index) { return new RenderTarget({ depth: false, colorBuffer: new Texture(this.device, { name: `BloomTexture${index}`, width: 1, height: 1, format: this.textureFormat, mipmaps: false, minFilter: FILTER_LINEAR, magFilter: FILTER_LINEAR, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }) }); } createRenderTargets(count) { for(let i = 0; i < count; i++){ const rt = i === 0 ? this.bloomRenderTarget : this.createRenderTarget(i); this.renderTargets.push(rt); } } calcMipLevels(width, height, minSize) { const min = Math.min(width, height); return Math.floor(Math.log2(min) - Math.log2(minSize)); } createRenderPasses(numPasses) { const device = this.device; let passSourceTexture = this._sourceTexture; for(let i = 0; i < numPasses; i++){ const pass = new RenderPassDownsample(device, passSourceTexture); const rt = this.renderTargets[i]; pass.init(rt, { resizeSource: passSourceTexture, scaleX: 0.5, scaleY: 0.5 }); pass.setClearColor(Color.BLACK); this.beforePasses.push(pass); passSourceTexture = rt.colorBuffer; } passSourceTexture = this.renderTargets[numPasses - 1].colorBuffer; for(let i = numPasses - 2; i >= 0; i--){ const pass = new RenderPassUpsample(device, passSourceTexture); const rt = this.renderTargets[i]; pass.init(rt); pass.blendState = BlendState.ADDBLEND; this.beforePasses.push(pass); passSourceTexture = rt.colorBuffer; } } onDisable() { this.renderTargets[0]?.resize(1, 1); this.destroyRenderPasses(); this.destroyRenderTargets(1); } frameUpdate() { super.frameUpdate(); const maxNumPasses = this.calcMipLevels(this._sourceTexture.width, this._sourceTexture.height, 1); const numPasses = math.clamp(maxNumPasses, 1, this.blurLevel); if (this.renderTargets.length !== numPasses) { this.destroyRenderPasses(); this.destroyRenderTargets(1); this.createRenderTargets(numPasses); this.createRenderPasses(numPasses); } } constructor(device, sourceTexture, format){ super(device), this.blurLevel = 16, this.renderTargets = []; this._sourceTexture = sourceTexture; this.textureFormat = format; this.bloomRenderTarget = this.createRenderTarget(0); this.bloomTexture = this.bloomRenderTarget.colorBuffer; } } var composePS$1 = ` #include "tonemappingPS" #include "gammaPS" varying vec2 uv0; uniform sampler2D sceneTexture; uniform vec2 sceneTextureInvRes; #include "composeBloomPS" #include "composeDofPS" #include "composeSsaoPS" #include "composeGradingPS" #include "composeVignettePS" #include "composeFringingPS" #include "composeCasPS" #include "composeColorLutPS" #include "composeDeclarationsPS" void main() { #include "composeMainStartPS" vec2 uv = uv0; #ifdef TAA #ifdef WEBGPU uv.y = 1.0 - uv.y; #endif #endif vec4 scene = texture2DLod(sceneTexture, uv, 0.0); vec3 result = scene.rgb; #ifdef CAS result = applyCas(result, uv, sharpness); #endif #ifdef DOF result = applyDof(result, uv0); #endif #ifdef SSAO_TEXTURE result = applySsao(result, uv0); #endif #ifdef FRINGING result = applyFringing(result, uv); #endif #ifdef BLOOM result = applyBloom(result, uv0); #endif #ifdef GRADING result = applyGrading(result); #endif result = toneMap(max(vec3(0.0), result)); #ifdef COLOR_LUT result = applyColorLUT(result); #endif #ifdef VIGNETTE result = applyVignette(result, uv); #endif #include "composeMainEndPS" #ifdef DEBUG_COMPOSE #if DEBUG_COMPOSE == scene result = scene.rgb; #elif defined(BLOOM) && DEBUG_COMPOSE == bloom result = dBloom * bloomIntensity; #elif defined(DOF) && DEBUG_COMPOSE == dofcoc result = vec3(dCoc, 0.0); #elif defined(DOF) && DEBUG_COMPOSE == dofblur result = dBlur; #elif defined(SSAO_TEXTURE) && DEBUG_COMPOSE == ssao result = vec3(dSsao); #elif defined(VIGNETTE) && DEBUG_COMPOSE == vignette result = vec3(dVignette); #endif #endif result = gammaCorrectOutput(result); gl_FragColor = vec4(result, scene.a); } `; var composeBloomPS$1 = ` #ifdef BLOOM uniform sampler2D bloomTexture; uniform float bloomIntensity; vec3 dBloom; vec3 applyBloom(vec3 color, vec2 uv) { dBloom = texture2DLod(bloomTexture, uv, 0.0).rgb; return color + dBloom * bloomIntensity; } #endif `; var composeDofPS$1 = ` #ifdef DOF uniform sampler2D cocTexture; uniform sampler2D blurTexture; vec2 dCoc; vec3 dBlur; vec3 getDofBlur(vec2 uv) { dCoc = texture2DLod(cocTexture, uv, 0.0).rg; #if DOF_UPSCALE vec2 blurTexelSize = 1.0 / vec2(textureSize(blurTexture, 0)); vec3 bilinearBlur = vec3(0.0); float totalWeight = 0.0; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { vec2 offset = vec2(i, j) * blurTexelSize; vec2 cocSample = texture2DLod(cocTexture, uv + offset, 0.0).rg; vec3 blurSample = texture2DLod(blurTexture, uv + offset, 0.0).rgb; float cocWeight = clamp(cocSample.r + cocSample.g, 0.0, 1.0); bilinearBlur += blurSample * cocWeight; totalWeight += cocWeight; } } if (totalWeight > 0.0) { bilinearBlur /= totalWeight; } dBlur = bilinearBlur; return bilinearBlur; #else dBlur = texture2DLod(blurTexture, uv, 0.0).rgb; return dBlur; #endif } vec3 applyDof(vec3 color, vec2 uv) { vec3 blur = getDofBlur(uv); return mix(color, blur, dCoc.r + dCoc.g); } #endif `; var composeSsaoPS$1 = ` #ifdef SSAO #define SSAO_TEXTURE #endif #if DEBUG_COMPOSE == ssao #define SSAO_TEXTURE #endif #ifdef SSAO_TEXTURE uniform sampler2D ssaoTexture; float dSsao; vec3 applySsao(vec3 color, vec2 uv) { dSsao = texture2DLod(ssaoTexture, uv, 0.0).r; #ifdef SSAO return color * dSsao; #else return color; #endif } #endif `; var composeGradingPS$1 = ` #ifdef GRADING uniform vec3 brightnessContrastSaturation; uniform vec3 tint; vec3 colorGradingHDR(vec3 color, float brt, float sat, float con) { color *= tint; color = color * brt; float grey = dot(color, vec3(0.3, 0.59, 0.11)); grey = grey / max(1.0, max(color.r, max(color.g, color.b))); color = mix(vec3(grey), color, sat); return mix(vec3(0.5), color, con); } vec3 applyGrading(vec3 color) { return colorGradingHDR(color, brightnessContrastSaturation.x, brightnessContrastSaturation.z, brightnessContrastSaturation.y); } #endif `; var composeVignettePS$1 = ` #ifdef VIGNETTE uniform vec4 vignetterParams; uniform vec3 vignetteColor; float dVignette; float calcVignette(vec2 uv) { float inner = vignetterParams.x; float outer = vignetterParams.y; float curvature = vignetterParams.z; float intensity = vignetterParams.w; vec2 curve = pow(abs(uv * 2.0 -1.0), vec2(1.0 / curvature)); float edge = pow(length(curve), curvature); dVignette = 1.0 - intensity * smoothstep(inner, outer, edge); return dVignette; } vec3 applyVignette(vec3 color, vec2 uv) { return mix(vignetteColor, color, calcVignette(uv)); } #endif `; var composeFringingPS$1 = ` #ifdef FRINGING uniform float fringingIntensity; vec3 applyFringing(vec3 color, vec2 uv) { vec2 centerDistance = uv - 0.5; vec2 offset = fringingIntensity * pow(centerDistance, vec2(2.0, 2.0)); color.r = texture2D(sceneTexture, uv - offset).r; color.b = texture2D(sceneTexture, uv + offset).b; return color; } #endif `; var composeCasPS$1 = ` #ifdef CAS uniform float sharpness; float maxComponent(float x, float y, float z) { return max(x, max(y, z)); } vec3 toSDR(vec3 c) { return c / (1.0 + maxComponent(c.r, c.g, c.b)); } vec3 toHDR(vec3 c) { return c / (1.0 - maxComponent(c.r, c.g, c.b)); } vec3 applyCas(vec3 color, vec2 uv, float sharpness) { float x = sceneTextureInvRes.x; float y = sceneTextureInvRes.y; vec3 a = toSDR(texture2DLod(sceneTexture, uv + vec2(0.0, -y), 0.0).rgb); vec3 b = toSDR(texture2DLod(sceneTexture, uv + vec2(-x, 0.0), 0.0).rgb); vec3 c = toSDR(color.rgb); vec3 d = toSDR(texture2DLod(sceneTexture, uv + vec2(x, 0.0), 0.0).rgb); vec3 e = toSDR(texture2DLod(sceneTexture, uv + vec2(0.0, y), 0.0).rgb); float min_g = min(a.g, min(b.g, min(c.g, min(d.g, e.g)))); float max_g = max(a.g, max(b.g, max(c.g, max(d.g, e.g)))); float sharpening_amount = sqrt(min(1.0 - max_g, min_g) / max_g); float w = sharpening_amount * sharpness; vec3 res = (w * (a + b + d + e) + c) / (4.0 * w + 1.0); res = max(res, 0.0); return toHDR(res); } #endif `; var composeColorLutPS$1 = ` #ifdef COLOR_LUT uniform sampler2D colorLUT; uniform vec4 colorLUTParams; vec3 applyColorLUT(vec3 color) { vec3 c = clamp(color, 0.0, 1.0); float width = colorLUTParams.x; float height = colorLUTParams.y; float maxColor = colorLUTParams.z; float cell = c.b * maxColor; float cell_l = floor(cell); float cell_h = ceil(cell); float half_px_x = 0.5 / width; float half_px_y = 0.5 / height; float r_offset = half_px_x + c.r / height * (maxColor / height); float g_offset = half_px_y + c.g * (maxColor / height); vec2 uv_l = vec2(cell_l / height + r_offset, g_offset); vec2 uv_h = vec2(cell_h / height + r_offset, g_offset); vec3 color_l = texture2DLod(colorLUT, uv_l, 0.0).rgb; vec3 color_h = texture2DLod(colorLUT, uv_h, 0.0).rgb; vec3 lutColor = mix(color_l, color_h, fract(cell)); return mix(color, lutColor, colorLUTParams.w); } #endif `; const composeChunksGLSL = { composePS: composePS$1, composeBloomPS: composeBloomPS$1, composeDofPS: composeDofPS$1, composeSsaoPS: composeSsaoPS$1, composeGradingPS: composeGradingPS$1, composeVignettePS: composeVignettePS$1, composeFringingPS: composeFringingPS$1, composeCasPS: composeCasPS$1, composeColorLutPS: composeColorLutPS$1, composeDeclarationsPS: '', composeMainStartPS: '', composeMainEndPS: '' }; var composePS = ` #include "tonemappingPS" #include "gammaPS" varying uv0: vec2f; var sceneTexture: texture_2d; var sceneTextureSampler: sampler; uniform sceneTextureInvRes: vec2f; #include "composeBloomPS" #include "composeDofPS" #include "composeSsaoPS" #include "composeGradingPS" #include "composeVignettePS" #include "composeFringingPS" #include "composeCasPS" #include "composeColorLutPS" #include "composeDeclarationsPS" @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { #include "composeMainStartPS" var output: FragmentOutput; var uv = uv0; #ifdef TAA uv.y = 1.0 - uv.y; #endif let scene = textureSampleLevel(sceneTexture, sceneTextureSampler, uv, 0.0); var result = scene.rgb; #ifdef CAS result = applyCas(result, uv, uniform.sharpness); #endif #ifdef DOF result = applyDof(result, uv0); #endif #ifdef SSAO_TEXTURE result = applySsao(result, uv0); #endif #ifdef FRINGING result = applyFringing(result, uv); #endif #ifdef BLOOM result = applyBloom(result, uv0); #endif #ifdef GRADING result = applyGrading(result); #endif result = toneMap(max(vec3f(0.0), result)); #ifdef COLOR_LUT result = applyColorLUT(result); #endif #ifdef VIGNETTE result = applyVignette(result, uv); #endif #include "composeMainEndPS" #ifdef DEBUG_COMPOSE #if DEBUG_COMPOSE == scene result = scene.rgb; #elif defined(BLOOM) && DEBUG_COMPOSE == bloom result = dBloom * uniform.bloomIntensity; #elif defined(DOF) && DEBUG_COMPOSE == dofcoc result = vec3f(dCoc, 0.0); #elif defined(DOF) && DEBUG_COMPOSE == dofblur result = dBlur; #elif defined(SSAO_TEXTURE) && DEBUG_COMPOSE == ssao result = vec3f(dSsao); #elif defined(VIGNETTE) && DEBUG_COMPOSE == vignette result = vec3f(dVignette); #endif #endif result = gammaCorrectOutput(result); output.color = vec4f(result, scene.a); return output; } `; var composeBloomPS = ` #ifdef BLOOM var bloomTexture: texture_2d; var bloomTextureSampler: sampler; uniform bloomIntensity: f32; var dBloom: vec3f; fn applyBloom(color: vec3f, uv: vec2f) -> vec3f { dBloom = textureSampleLevel(bloomTexture, bloomTextureSampler, uv, 0.0).rgb; return color + dBloom * uniform.bloomIntensity; } #endif `; var composeDofPS = ` #ifdef DOF var cocTexture: texture_2d; var cocTextureSampler: sampler; var blurTexture: texture_2d; var blurTextureSampler: sampler; var dCoc: vec2f; var dBlur: vec3f; fn getDofBlur(uv: vec2f) -> vec3f { dCoc = textureSampleLevel(cocTexture, cocTextureSampler, uv, 0.0).rg; #if DOF_UPSCALE let blurTexelSize = 1.0 / vec2f(textureDimensions(blurTexture, 0)); var bilinearBlur = vec3f(0.0); var totalWeight = 0.0; for (var i = -1; i <= 1; i++) { for (var j = -1; j <= 1; j++) { let offset = vec2f(f32(i), f32(j)) * blurTexelSize; let cocSample = textureSampleLevel(cocTexture, cocTextureSampler, uv + offset, 0.0).rg; let blurSample = textureSampleLevel(blurTexture, blurTextureSampler, uv + offset, 0.0).rgb; let cocWeight = clamp(cocSample.r + cocSample.g, 0.0, 1.0); bilinearBlur += blurSample * cocWeight; totalWeight += cocWeight; } } if (totalWeight > 0.0) { bilinearBlur /= totalWeight; } dBlur = bilinearBlur; return bilinearBlur; #else dBlur = textureSampleLevel(blurTexture, blurTextureSampler, uv, 0.0).rgb; return dBlur; #endif } fn applyDof(color: vec3f, uv: vec2f) -> vec3f { let blur = getDofBlur(uv); return mix(color, blur, dCoc.r + dCoc.g); } #endif `; var composeSsaoPS = ` #ifdef SSAO #define SSAO_TEXTURE #endif #if DEBUG_COMPOSE == ssao #define SSAO_TEXTURE #endif #ifdef SSAO_TEXTURE var ssaoTexture: texture_2d; var ssaoTextureSampler: sampler; var dSsao: f32; fn applySsao(color: vec3f, uv: vec2f) -> vec3f { dSsao = textureSampleLevel(ssaoTexture, ssaoTextureSampler, uv, 0.0).r; #ifdef SSAO return color * dSsao; #else return color; #endif } #endif `; var composeGradingPS = ` #ifdef GRADING uniform brightnessContrastSaturation: vec3f; uniform tint: vec3f; fn colorGradingHDR(color: vec3f, brt: f32, sat: f32, con: f32) -> vec3f { var colorOut = color * uniform.tint; colorOut = colorOut * brt; let grey = dot(colorOut, vec3f(0.3, 0.59, 0.11)); let normalizedGrey = grey / max(1.0, max(colorOut.r, max(colorOut.g, colorOut.b))); colorOut = mix(vec3f(normalizedGrey), colorOut, sat); return mix(vec3f(0.5), colorOut, con); } fn applyGrading(color: vec3f) -> vec3f { return colorGradingHDR(color, uniform.brightnessContrastSaturation.x, uniform.brightnessContrastSaturation.z, uniform.brightnessContrastSaturation.y); } #endif `; var composeVignettePS = ` #ifdef VIGNETTE uniform vignetterParams: vec4f; uniform vignetteColor: vec3f; var dVignette: f32; fn calcVignette(uv: vec2f) -> f32 { let inner = uniform.vignetterParams.x; let outer = uniform.vignetterParams.y; let curvature = uniform.vignetterParams.z; let intensity = uniform.vignetterParams.w; let curve = pow(abs(uv * 2.0 - 1.0), vec2f(1.0 / curvature)); let edge = pow(length(curve), curvature); dVignette = 1.0 - intensity * smoothstep(inner, outer, edge); return dVignette; } fn applyVignette(color: vec3f, uv: vec2f) -> vec3f { return mix(uniform.vignetteColor, color, calcVignette(uv)); } #endif `; var composeFringingPS = ` #ifdef FRINGING uniform fringingIntensity: f32; fn applyFringing(color: vec3f, uv: vec2f) -> vec3f { let centerDistance = uv - 0.5; let offset = uniform.fringingIntensity * pow(centerDistance, vec2f(2.0)); var colorOut = color; colorOut.r = textureSample(sceneTexture, sceneTextureSampler, uv - offset).r; colorOut.b = textureSample(sceneTexture, sceneTextureSampler, uv + offset).b; return colorOut; } #endif `; var composeCasPS = ` #ifdef CAS uniform sharpness: f32; fn maxComponent(x: f32, y: f32, z: f32) -> f32 { return max(x, max(y, z)); } fn toSDR(c: vec3f) -> vec3f { return c / (1.0 + maxComponent(c.r, c.g, c.b)); } fn toHDR(c: vec3f) -> vec3f { return c / (1.0 - maxComponent(c.r, c.g, c.b)); } fn applyCas(color: vec3f, uv: vec2f, sharpness: f32) -> vec3f { let x = uniform.sceneTextureInvRes.x; let y = uniform.sceneTextureInvRes.y; let a = toSDR(textureSampleLevel(sceneTexture, sceneTextureSampler, uv + vec2f(0.0, -y), 0.0).rgb); let b = toSDR(textureSampleLevel(sceneTexture, sceneTextureSampler, uv + vec2f(-x, 0.0), 0.0).rgb); let c = toSDR(color.rgb); let d = toSDR(textureSampleLevel(sceneTexture, sceneTextureSampler, uv + vec2f(x, 0.0), 0.0).rgb); let e = toSDR(textureSampleLevel(sceneTexture, sceneTextureSampler, uv + vec2f(0.0, y), 0.0).rgb); let min_g = min(a.g, min(b.g, min(c.g, min(d.g, e.g)))); let max_g = max(a.g, max(b.g, max(c.g, max(d.g, e.g)))); let sharpening_amount = sqrt(min(1.0 - max_g, min_g) / max_g); let w = sharpening_amount * uniform.sharpness; var res = (w * (a + b + d + e) + c) / (4.0 * w + 1.0); res = max(res, vec3f(0.0)); return toHDR(res); } #endif `; var composeColorLutPS = ` #ifdef COLOR_LUT var colorLUT: texture_2d; var colorLUTSampler: sampler; uniform colorLUTParams: vec4f; fn applyColorLUT(color: vec3f) -> vec3f { var c: vec3f = clamp(color, vec3f(0.0), vec3f(1.0)); let width: f32 = uniform.colorLUTParams.x; let height: f32 = uniform.colorLUTParams.y; let maxColor: f32 = uniform.colorLUTParams.z; let cell: f32 = c.b * maxColor; let cell_l: f32 = floor(cell); let cell_h: f32 = ceil(cell); let half_px_x: f32 = 0.5 / width; let half_px_y: f32 = 0.5 / height; let r_offset: f32 = half_px_x + c.r / height * (maxColor / height); let g_offset: f32 = half_px_y + c.g * (maxColor / height); let uv_l: vec2f = vec2f(cell_l / height + r_offset, g_offset); let uv_h: vec2f = vec2f(cell_h / height + r_offset, g_offset); let color_l: vec3f = textureSampleLevel(colorLUT, colorLUTSampler, uv_l, 0.0).rgb; let color_h: vec3f = textureSampleLevel(colorLUT, colorLUTSampler, uv_h, 0.0).rgb; let lutColor: vec3f = mix(color_l, color_h, fract(cell)); return mix(color, lutColor, uniform.colorLUTParams.w); } #endif `; const composeChunksWGSL = { composePS, composeBloomPS, composeDofPS, composeSsaoPS, composeGradingPS, composeVignettePS, composeFringingPS, composeCasPS, composeColorLutPS, composeDeclarationsPS: '', composeMainStartPS: '', composeMainEndPS: '' }; class RenderPassCompose extends RenderPassShaderQuad { set debug(value) { if (this._debug !== value) { this._debug = value; this._shaderDirty = true; } } get debug() { return this._debug; } set colorLUT(value) { if (this._colorLUT !== value) { this._colorLUT = value; this._shaderDirty = true; } } get colorLUT() { return this._colorLUT; } set bloomTexture(value) { if (this._bloomTexture !== value) { this._bloomTexture = value; this._shaderDirty = true; } } get bloomTexture() { return this._bloomTexture; } set cocTexture(value) { if (this._cocTexture !== value) { this._cocTexture = value; this._shaderDirty = true; } } get cocTexture() { return this._cocTexture; } set ssaoTexture(value) { if (this._ssaoTexture !== value) { this._ssaoTexture = value; this._shaderDirty = true; } } get ssaoTexture() { return this._ssaoTexture; } set taaEnabled(value) { if (this._taaEnabled !== value) { this._taaEnabled = value; this._shaderDirty = true; } } get taaEnabled() { return this._taaEnabled; } set gradingEnabled(value) { if (this._gradingEnabled !== value) { this._gradingEnabled = value; this._shaderDirty = true; } } get gradingEnabled() { return this._gradingEnabled; } set vignetteEnabled(value) { if (this._vignetteEnabled !== value) { this._vignetteEnabled = value; this._shaderDirty = true; } } get vignetteEnabled() { return this._vignetteEnabled; } set fringingEnabled(value) { if (this._fringingEnabled !== value) { this._fringingEnabled = value; this._shaderDirty = true; } } get fringingEnabled() { return this._fringingEnabled; } set toneMapping(value) { if (this._toneMapping !== value) { this._toneMapping = value; this._shaderDirty = true; } } get toneMapping() { return this._toneMapping; } set sharpness(value) { if (this._sharpness !== value) { this._sharpness = value; this._shaderDirty = true; } } get sharpness() { return this._sharpness; } get isSharpnessEnabled() { return this._sharpness > 0; } postInit() { this.setClearColor(Color.BLACK); this.setClearDepth(1.0); this.setClearStencil(0); } frameUpdate() { const rt = this.renderTarget ?? this.device.backBuffer; const srgb = rt.isColorBufferSrgb(0); const neededGammaCorrection = srgb ? GAMMA_NONE : GAMMA_SRGB; if (this._gammaCorrection !== neededGammaCorrection) { this._gammaCorrection = neededGammaCorrection; this._shaderDirty = true; } const shaderChunks = ShaderChunks.get(this.device, this.device.isWebGPU ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL); for (const [name, prevValue] of this._customComposeChunks.entries()){ const currentValue = shaderChunks.get(name); if (currentValue !== prevValue) { this._customComposeChunks.set(name, currentValue); this._shaderDirty = true; } } if (this._shaderDirty) { this._shaderDirty = false; const gammaCorrectionName = gammaNames[this._gammaCorrection]; const customChunks = this._customComposeChunks; const declHash = hashCode(customChunks.get('composeDeclarationsPS') ?? ''); const startHash = hashCode(customChunks.get('composeMainStartPS') ?? ''); const endHash = hashCode(customChunks.get('composeMainEndPS') ?? ''); const key = `${this.toneMapping}` + `-${gammaCorrectionName}` + `-${this.bloomTexture ? 'bloom' : 'nobloom'}` + `-${this.cocTexture ? 'dof' : 'nodof'}` + `-${this.blurTextureUpscale ? 'dofupscale' : ''}` + `-${this.ssaoTexture ? 'ssao' : 'nossao'}` + `-${this.gradingEnabled ? 'grading' : 'nograding'}` + `-${this.colorLUT ? 'colorlut' : 'nocolorlut'}` + `-${this.vignetteEnabled ? 'vignette' : 'novignette'}` + `-${this.fringingEnabled ? 'fringing' : 'nofringing'}` + `-${this.taaEnabled ? 'taa' : 'notaa'}` + `-${this.isSharpnessEnabled ? 'cas' : 'nocas'}` + `-${this._debug ?? ''}` + `-decl${declHash}-start${startHash}-end${endHash}`; if (this._key !== key) { this._key = key; const defines = new Map(); defines.set('TONEMAP', tonemapNames[this.toneMapping]); defines.set('GAMMA', gammaCorrectionName); if (this.bloomTexture) defines.set('BLOOM', true); if (this.cocTexture) defines.set('DOF', true); if (this.blurTextureUpscale) defines.set('DOF_UPSCALE', true); if (this.ssaoTexture) defines.set('SSAO', true); if (this.gradingEnabled) defines.set('GRADING', true); if (this.colorLUT) defines.set('COLOR_LUT', true); if (this.vignetteEnabled) defines.set('VIGNETTE', true); if (this.fringingEnabled) defines.set('FRINGING', true); if (this.taaEnabled) defines.set('TAA', true); if (this.isSharpnessEnabled) defines.set('CAS', true); if (this._debug) defines.set('DEBUG_COMPOSE', this._debug); const includes = new Map(shaderChunks); this.shader = ShaderUtils.createShader(this.device, { uniqueName: `ComposeShader-${key}`, attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'composePS', fragmentDefines: defines, fragmentIncludes: includes }); } } } execute() { const sceneTex = this.sceneTexture; this.sceneTextureId.setValue(sceneTex); this.sceneTextureInvResValue[0] = 1.0 / sceneTex.width; this.sceneTextureInvResValue[1] = 1.0 / sceneTex.height; this.sceneTextureInvResId.setValue(this.sceneTextureInvResValue); if (this._bloomTexture) { this.bloomTextureId.setValue(this._bloomTexture); this.bloomIntensityId.setValue(this.bloomIntensity); } if (this._cocTexture) { this.cocTextureId.setValue(this._cocTexture); this.blurTextureId.setValue(this.blurTexture); } if (this._ssaoTexture) { this.ssaoTextureId.setValue(this._ssaoTexture); } if (this._gradingEnabled) { this.bcsId.setValue([ this.gradingBrightness, this.gradingContrast, this.gradingSaturation ]); this.tintId.setValue([ this.gradingTint.r, this.gradingTint.g, this.gradingTint.b ]); } const lutTexture = this._colorLUT; if (lutTexture) { this.colorLUTParams[0] = lutTexture.width; this.colorLUTParams[1] = lutTexture.height; this.colorLUTParams[2] = lutTexture.height - 1.0; this.colorLUTParams[3] = this.colorLUTIntensity; this.colorLUTParamsId.setValue(this.colorLUTParams); this.colorLUTId.setValue(lutTexture); } if (this._vignetteEnabled) { this.vignetterParamsId.setValue([ this.vignetteInner, this.vignetteOuter, this.vignetteCurvature, this.vignetteIntensity ]); this.vignetteColorId.setValue([ this.vignetteColor.r, this.vignetteColor.g, this.vignetteColor.b ]); } if (this._fringingEnabled) { this.fringingIntensityId.setValue(this.fringingIntensity / 1024); } if (this.isSharpnessEnabled) { this.sharpnessId.setValue(math.lerp(-0.125, -0.2, this.sharpness)); } super.execute(); } constructor(graphicsDevice){ super(graphicsDevice), this.sceneTexture = null, this.bloomIntensity = 0.01, this._bloomTexture = null, this._cocTexture = null, this.blurTexture = null, this.blurTextureUpscale = false, this._ssaoTexture = null, this._toneMapping = TONEMAP_LINEAR, this._gradingEnabled = false, this.gradingSaturation = 1, this.gradingContrast = 1, this.gradingBrightness = 1, this.gradingTint = new Color(1, 1, 1, 1), this._shaderDirty = true, this._vignetteEnabled = false, this.vignetteInner = 0.5, this.vignetteOuter = 1.0, this.vignetteCurvature = 0.5, this.vignetteIntensity = 0.3, this.vignetteColor = new Color(0, 0, 0), this._fringingEnabled = false, this.fringingIntensity = 10, this._taaEnabled = false, this._sharpness = 0.5, this._gammaCorrection = GAMMA_SRGB, this._colorLUT = null, this.colorLUTIntensity = 1, this._key = '', this._debug = null, this._customComposeChunks = new Map([ [ 'composeDeclarationsPS', '' ], [ 'composeMainStartPS', '' ], [ 'composeMainEndPS', '' ] ]); ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_GLSL).add(composeChunksGLSL, false); ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_WGSL).add(composeChunksWGSL, false); const { scope } = graphicsDevice; this.sceneTextureId = scope.resolve('sceneTexture'); this.bloomTextureId = scope.resolve('bloomTexture'); this.cocTextureId = scope.resolve('cocTexture'); this.ssaoTextureId = scope.resolve('ssaoTexture'); this.blurTextureId = scope.resolve('blurTexture'); this.bloomIntensityId = scope.resolve('bloomIntensity'); this.bcsId = scope.resolve('brightnessContrastSaturation'); this.tintId = scope.resolve('tint'); this.vignetterParamsId = scope.resolve('vignetterParams'); this.vignetteColorId = scope.resolve('vignetteColor'); this.fringingIntensityId = scope.resolve('fringingIntensity'); this.sceneTextureInvResId = scope.resolve('sceneTextureInvRes'); this.sceneTextureInvResValue = new Float32Array(2); this.sharpnessId = scope.resolve('sharpness'); this.colorLUTId = scope.resolve('colorLUT'); this.colorLUTParams = new Float32Array(4); this.colorLUTParamsId = scope.resolve('colorLUTParams'); } } var glslSampleCatmullRomPS = ` vec4 SampleTextureCatmullRom(TEXTURE_ACCEPT(tex), vec2 uv, vec2 texSize) { vec2 samplePos = uv * texSize; vec2 texPos1 = floor(samplePos - 0.5) + 0.5; vec2 f = samplePos - texPos1; vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)); vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f); vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f)); vec2 w3 = f * f * (-0.5 + 0.5 * f); vec2 w12 = w1 + w2; vec2 offset12 = w2 / (w1 + w2); vec2 texPos0 = (texPos1 - 1.0) / texSize; vec2 texPos3 = (texPos1 + 2.0) / texSize; vec2 texPos12 = (texPos1 + offset12) / texSize; vec4 result = vec4(0.0); result += texture2DLod(tex, vec2(texPos0.x, texPos0.y), 0.0) * w0.x * w0.y; result += texture2DLod(tex, vec2(texPos12.x, texPos0.y), 0.0) * w12.x * w0.y; result += texture2DLod(tex, vec2(texPos3.x, texPos0.y), 0.0) * w3.x * w0.y; result += texture2DLod(tex, vec2(texPos0.x, texPos12.y), 0.0) * w0.x * w12.y; result += texture2DLod(tex, vec2(texPos12.x, texPos12.y), 0.0) * w12.x * w12.y; result += texture2DLod(tex, vec2(texPos3.x, texPos12.y), 0.0) * w3.x * w12.y; result += texture2DLod(tex, vec2(texPos0.x, texPos3.y), 0.0) * w0.x * w3.y; result += texture2DLod(tex, vec2(texPos12.x, texPos3.y), 0.0) * w12.x * w3.y; result += texture2DLod(tex, vec2(texPos3.x, texPos3.y), 0.0) * w3.x * w3.y; return result; } `; var wgslSampleCatmullRomPS = ` fn SampleTextureCatmullRom(tex: texture_2d, texSampler: sampler, uv: vec2f, texSize: vec2f) -> vec4f { let samplePos: vec2f = uv * texSize; let texPos1: vec2f = floor(samplePos - 0.5) + 0.5; let f: vec2f = samplePos - texPos1; let w0: vec2f = f * (-0.5 + f * (1.0 - 0.5 * f)); let w1: vec2f = 1.0 + f * f * (-2.5 + 1.5 * f); let w2: vec2f = f * (0.5 + f * (2.0 - 1.5 * f)); let w3: vec2f = f * f * (-0.5 + 0.5 * f); let w12: vec2f = w1 + w2; let offset12: vec2f = w2 / w12; let texPos0: vec2f = (texPos1 - 1.0) / texSize; let texPos3: vec2f = (texPos1 + 2.0) / texSize; let texPos12: vec2f = (texPos1 + offset12) / texSize; var result: vec4f = vec4f(0.0); result = result + textureSampleLevel(tex, texSampler, vec2f(texPos0.x, texPos0.y), 0.0) * w0.x * w0.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos12.x, texPos0.y), 0.0) * w12.x * w0.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos3.x, texPos0.y), 0.0) * w3.x * w0.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos0.x, texPos12.y), 0.0) * w0.x * w12.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos12.x, texPos12.y), 0.0) * w12.x * w12.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos3.x, texPos12.y), 0.0) * w3.x * w12.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos0.x, texPos3.y), 0.0) * w0.x * w3.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos12.x, texPos3.y), 0.0) * w12.x * w3.y; result = result + textureSampleLevel(tex, texSampler, vec2f(texPos3.x, texPos3.y), 0.0) * w3.x * w3.y; return result; } `; var glsltaaResolvePS = ` #include "sampleCatmullRomPS" #include "screenDepthPS" uniform sampler2D sourceTexture; uniform sampler2D historyTexture; uniform mat4 matrix_viewProjectionPrevious; uniform mat4 matrix_viewProjectionInverse; uniform vec4 jitters; uniform vec2 textureSize; varying vec2 uv0; vec2 reproject(vec2 uv, float depth) { #ifndef WEBGPU depth = depth * 2.0 - 1.0; #endif vec4 ndc = vec4(uv * 2.0 - 1.0, depth, 1.0); ndc.xy -= jitters.xy; vec4 worldPosition = matrix_viewProjectionInverse * ndc; worldPosition /= worldPosition.w; vec4 screenPrevious = matrix_viewProjectionPrevious * worldPosition; return (screenPrevious.xy / screenPrevious.w) * 0.5 + 0.5; } vec4 colorClamp(vec2 uv, vec4 historyColor) { vec3 minColor = vec3(9999.0); vec3 maxColor = vec3(-9999.0); for(float x = -1.0; x <= 1.0; ++x) { for(float y = -1.0; y <= 1.0; ++y) { vec3 color = texture2D(sourceTexture, uv + vec2(x, y) / textureSize).rgb; minColor = min(minColor, color); maxColor = max(maxColor, color); } } vec3 clamped = clamp(historyColor.rgb, minColor, maxColor); return vec4(clamped, historyColor.a); } void main() { vec2 uv = uv0; #ifdef WEBGPU uv.y = 1.0 - uv.y; #endif vec4 srcColor = texture2D(sourceTexture, uv); float linearDepth = getLinearScreenDepth(uv0); float depth = delinearizeDepth(linearDepth); vec2 historyUv = reproject(uv0, depth); #ifdef QUALITY_HIGH vec4 historyColor = SampleTextureCatmullRom(TEXTURE_PASS(historyTexture), historyUv, textureSize); #else vec4 historyColor = texture2D(historyTexture, historyUv); #endif vec4 historyColorClamped = colorClamp(uv, historyColor); float mixFactor = (historyUv.x < 0.0 || historyUv.x > 1.0 || historyUv.y < 0.0 || historyUv.y > 1.0) ? 1.0 : 0.05; gl_FragColor = mix(historyColorClamped, srcColor, mixFactor); } `; var wgsltaaResolvePS = ` #include "sampleCatmullRomPS" #include "screenDepthPS" var sourceTexture: texture_2d; var sourceTextureSampler: sampler; var historyTexture: texture_2d; var historyTextureSampler: sampler; uniform matrix_viewProjectionPrevious: mat4x4f; uniform matrix_viewProjectionInverse: mat4x4f; uniform jitters: vec4f; uniform textureSize: vec2f; varying uv0: vec2f; fn reproject(uv: vec2f, depth: f32) -> vec2f { var ndc = vec4f(uv * 2.0 - 1.0, depth, 1.0); ndc = vec4f(ndc.xy - uniform.jitters.xy, ndc.zw); var worldPosition = uniform.matrix_viewProjectionInverse * ndc; worldPosition = worldPosition / worldPosition.w; let screenPrevious = uniform.matrix_viewProjectionPrevious * worldPosition; return (screenPrevious.xy / screenPrevious.w) * 0.5 + 0.5; } fn colorClamp(uv: vec2f, historyColor: vec4f) -> vec4f { var minColor = vec3f(9999.0); var maxColor = vec3f(-9999.0); for (var ix: i32 = -1; ix <= 1; ix = ix + 1) { for (var iy: i32 = -1; iy <= 1; iy = iy + 1) { let color_sample = textureSample(sourceTexture, sourceTextureSampler, uv + vec2f(f32(ix), f32(iy)) / uniform.textureSize).rgb; minColor = min(minColor, color_sample); maxColor = max(maxColor, color_sample); } } let clamped = clamp(historyColor.rgb, minColor, maxColor); return vec4f(clamped, historyColor.a); } @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; var uv = input.uv0; uv.y = 1.0 - uv.y; let srcColor = textureSample(sourceTexture, sourceTextureSampler, uv); let linearDepth = getLinearScreenDepth(uv0); let depth = delinearizeDepth(linearDepth); let historyUv = reproject(uv0, depth); #ifdef QUALITY_HIGH var historyColor: vec4f = SampleTextureCatmullRom(historyTexture, historyTextureSampler, historyUv, uniform.textureSize); #else var historyColor: vec4f = textureSample(historyTexture, historyTextureSampler, historyUv); #endif let historyColorClamped = colorClamp(uv, historyColor); let mixFactor_condition = historyUv.x < 0.0 || historyUv.x > 1.0 || historyUv.y < 0.0 || historyUv.y > 1.0; let mixFactor = select(0.05, 1.0, mixFactor_condition); output.color = mix(historyColorClamped, srcColor, mixFactor); return output; } `; class RenderPassTAA extends RenderPassShaderQuad { destroy() { if (this.renderTarget) { this.renderTarget.destroyTextureBuffers(); this.renderTarget.destroy(); this.renderTarget = null; } } setup() { for(let i = 0; i < 2; ++i){ this.historyTextures[i] = new Texture(this.device, { name: `TAA-History-${i}`, width: 4, height: 4, format: this.sourceTexture.format, mipmaps: false, minFilter: FILTER_LINEAR, magFilter: FILTER_LINEAR, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); this.historyRenderTargets[i] = new RenderTarget({ colorBuffer: this.historyTextures[i], depth: false }); } this.historyTexture = this.historyTextures[0]; this.init(this.historyRenderTargets[0], { resizeSource: this.sourceTexture }); } before() { this.sourceTextureId.setValue(this.sourceTexture); this.historyTextureId.setValue(this.historyTextures[1 - this.historyIndex]); this.textureSize[0] = this.sourceTexture.width; this.textureSize[1] = this.sourceTexture.height; this.textureSizeId.setValue(this.textureSize); const camera = this.cameraComponent.camera; this.viewProjPrevId.setValue(camera._viewProjPrevious.data); this.viewProjInvId.setValue(camera._viewProjInverse.data); this.jittersId.setValue(camera._jitters); this.cameraParamsId.setValue(camera.fillShaderParams(this.cameraParams)); } update() { this.historyIndex = 1 - this.historyIndex; this.historyTexture = this.historyTextures[this.historyIndex]; this.renderTarget = this.historyRenderTargets[this.historyIndex]; return this.historyTexture; } constructor(device, sourceTexture, cameraComponent){ super(device), this.historyIndex = 0, this.historyTexture = null, this.historyTextures = [], this.historyRenderTargets = []; this.sourceTexture = sourceTexture; this.cameraComponent = cameraComponent; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('sampleCatmullRomPS', glslSampleCatmullRomPS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('sampleCatmullRomPS', wgslSampleCatmullRomPS); ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('taaResolvePS', glsltaaResolvePS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('taaResolvePS', wgsltaaResolvePS); const defines = new Map(); defines.set('QUALITY_HIGH', true); ShaderUtils.addScreenDepthChunkDefines(device, cameraComponent.shaderParams, defines); this.shader = ShaderUtils.createShader(device, { uniqueName: 'TaaResolveShader', attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'taaResolvePS', fragmentDefines: defines }); const { scope } = device; this.sourceTextureId = scope.resolve('sourceTexture'); this.textureSizeId = scope.resolve('textureSize'); this.textureSize = new Float32Array(2); this.historyTextureId = scope.resolve('historyTexture'); this.viewProjPrevId = scope.resolve('matrix_viewProjectionPrevious'); this.viewProjInvId = scope.resolve('matrix_viewProjectionInverse'); this.jittersId = scope.resolve('jitters'); this.cameraParams = new Float32Array(4); this.cameraParamsId = scope.resolve('camera_params'); this.setup(); } } var glslCocPS = ` #include "screenDepthPS" varying vec2 uv0; uniform vec3 params; void main() { float depth = getLinearScreenDepth(uv0); float focusDistance = params.x; float focusRange = params.y; float invRange = params.z; float farRange = focusDistance + focusRange * 0.5; float cocFar = min((depth - farRange) * invRange, 1.0); #ifdef NEAR_BLUR float nearRange = focusDistance - focusRange * 0.5; float cocNear = min((nearRange - depth) * invRange, 1.0); #else float cocNear = 0.0; #endif gl_FragColor = vec4(cocFar, cocNear, 0.0, 0.0); } `; var wgslCocPS = ` #include "screenDepthPS" varying uv0: vec2f; uniform params: vec3f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let depth: f32 = getLinearScreenDepth(uv0); let focusDistance: f32 = uniform.params.x; let focusRange: f32 = uniform.params.y; let invRange: f32 = uniform.params.z; let farRange: f32 = focusDistance + focusRange * 0.5; let cocFar: f32 = min((depth - farRange) * invRange, 1.0); #ifdef NEAR_BLUR let nearRange: f32 = focusDistance - focusRange * 0.5; var cocNear: f32 = min((nearRange - depth) * invRange, 1.0); #else var cocNear: f32 = 0.0; #endif output.color = vec4f(cocFar, cocNear, 0.0, 0.0); return output; } `; class RenderPassCoC extends RenderPassShaderQuad { execute() { const { paramsValue, focusRange } = this; paramsValue[0] = this.focusDistance + 0.001; paramsValue[1] = focusRange; paramsValue[2] = 1 / focusRange; this.paramsId.setValue(paramsValue); const camera = this.cameraComponent.camera; this.cameraParamsId.setValue(camera.fillShaderParams(this.cameraParams)); super.execute(); } constructor(device, cameraComponent, nearBlur){ super(device); this.cameraComponent = cameraComponent; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('cocPS', glslCocPS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('cocPS', wgslCocPS); const defines = new Map(); if (nearBlur) defines.set('NEAR_BLUR', ''); ShaderUtils.addScreenDepthChunkDefines(device, cameraComponent.shaderParams, defines); this.shader = ShaderUtils.createShader(device, { uniqueName: `CocShader-${nearBlur}`, attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'cocPS', fragmentDefines: defines }); this.paramsId = device.scope.resolve('params'); this.paramsValue = new Float32Array(3); this.cameraParams = new Float32Array(4); this.cameraParamsId = device.scope.resolve('camera_params'); } } var glsldofBlurPS = ` #if defined(NEAR_BLUR) uniform sampler2D nearTexture; #endif uniform sampler2D farTexture; uniform sampler2D cocTexture; uniform vec2 kernel[{KERNEL_COUNT}]; uniform float blurRadiusNear; uniform float blurRadiusFar; varying vec2 uv0; void main() { vec2 coc = texture2D(cocTexture, uv0).rg; float cocFar = coc.r; vec3 sum = vec3(0.0, 0.0, 0.0); #if defined(NEAR_BLUR) float cocNear = coc.g; if (cocNear > 0.0001) { ivec2 nearTextureSize = textureSize(nearTexture, 0); vec2 step = cocNear * blurRadiusNear / vec2(nearTextureSize); for (int i = 0; i < {KERNEL_COUNT}; i++) { vec2 uv = uv0 + step * kernel[i]; vec3 tap = texture2DLod(nearTexture, uv, 0.0).rgb; sum += tap.rgb; } sum *= float({INV_KERNEL_COUNT}); } else #endif if (cocFar > 0.0001) { ivec2 farTextureSize = textureSize(farTexture, 0); vec2 step = cocFar * blurRadiusFar / vec2(farTextureSize); float sumCoC = 0.0; for (int i = 0; i < {KERNEL_COUNT}; i++) { vec2 uv = uv0 + step * kernel[i]; vec3 tap = texture2DLod(farTexture, uv, 0.0).rgb; float cocThis = texture2DLod(cocTexture, uv, 0.0).r; tap *= cocThis; sumCoC += cocThis; sum += tap; } if (sumCoC > 0.0) sum /= sumCoC; sum /= cocFar; } pcFragColor0 = vec4(sum, 1.0); } `; var wgsldofBlurPS = ` #if defined(NEAR_BLUR) var nearTexture: texture_2d; var nearTextureSampler: sampler; #endif var farTexture: texture_2d; var farTextureSampler: sampler; var cocTexture: texture_2d; var cocTextureSampler: sampler; uniform kernel: array; uniform blurRadiusNear: f32; uniform blurRadiusFar: f32; varying uv0: vec2f; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let coc: vec2f = textureSample(cocTexture, cocTextureSampler, input.uv0).rg; let cocFar: f32 = coc.r; var sum: vec3f = vec3f(0.0, 0.0, 0.0); #if defined(NEAR_BLUR) let cocNear: f32 = coc.g; if (cocNear > 0.0001) { let nearTextureSize: vec2f = vec2f(textureDimensions(nearTexture, 0)); let step: vec2f = cocNear * uniform.blurRadiusNear / nearTextureSize; for (var i: i32 = 0; i < {KERNEL_COUNT}; i = i + 1) { let uv: vec2f = uv0 + step * uniform.kernel[i].element; let tap: vec3f = textureSampleLevel(nearTexture, nearTextureSampler, uv, 0.0).rgb; sum = sum + tap; } sum = sum * f32({INV_KERNEL_COUNT}); } else #endif if (cocFar > 0.0001) { let farTextureSize: vec2f = vec2f(textureDimensions(farTexture, 0)); let step: vec2f = cocFar * uniform.blurRadiusFar / farTextureSize; var sumCoC: f32 = 0.0; for (var i: i32 = 0; i < {KERNEL_COUNT}; i = i + 1) { let uv: vec2f = uv0 + step * uniform.kernel[i].element; var tap: vec3f = textureSampleLevel(farTexture, farTextureSampler, uv, 0.0).rgb; let cocThis: f32 = textureSampleLevel(cocTexture, cocTextureSampler, uv, 0.0).r; tap = tap * cocThis; sumCoC = sumCoC + cocThis; sum = sum + tap; } if (sumCoC > 0.0) { sum = sum / sumCoC; } sum = sum / cocFar; } output.color = vec4f(sum, 1.0); return output; } `; class RenderPassDofBlur extends RenderPassShaderQuad { set blurRings(value) { if (this._blurRings !== value) { this._blurRings = value; this.shader = null; } } get blurRings() { return this._blurRings; } set blurRingPoints(value) { if (this._blurRingPoints !== value) { this._blurRingPoints = value; this.shader = null; } } get blurRingPoints() { return this._blurRingPoints; } createShader() { this.kernel = new Float32Array(Kernel.concentric(this.blurRings, this.blurRingPoints)); const kernelCount = this.kernel.length >> 1; const nearBlur = this.nearTexture !== null; const defines = new Map(); defines.set('{KERNEL_COUNT}', kernelCount); defines.set('{INV_KERNEL_COUNT}', 1.0 / kernelCount); if (nearBlur) defines.set('NEAR_BLUR', ''); this.shader = ShaderUtils.createShader(this.device, { uniqueName: `DofBlurShader-${kernelCount}-${nearBlur ? 'nearBlur' : 'noNearBlur'}`, attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'dofBlurPS', fragmentDefines: defines }); } execute() { if (!this.shader) { this.createShader(); } this.nearTextureId.setValue(this.nearTexture); this.farTextureId.setValue(this.farTexture); this.cocTextureId.setValue(this.cocTexture); this.kernelId.setValue(this.kernel); this.kernelCountId.setValue(this.kernel.length >> 1); this.blurRadiusNearId.setValue(this.blurRadiusNear); this.blurRadiusFarId.setValue(this.blurRadiusFar); super.execute(); } constructor(device, nearTexture, farTexture, cocTexture){ super(device), this.blurRadiusNear = 1, this.blurRadiusFar = 1, this._blurRings = 3, this._blurRingPoints = 3; this.nearTexture = nearTexture; this.farTexture = farTexture; this.cocTexture = cocTexture; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('dofBlurPS', glsldofBlurPS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('dofBlurPS', wgsldofBlurPS); const { scope } = device; this.kernelId = scope.resolve('kernel[0]'); this.kernelCountId = scope.resolve('kernelCount'); this.blurRadiusNearId = scope.resolve('blurRadiusNear'); this.blurRadiusFarId = scope.resolve('blurRadiusFar'); this.nearTextureId = scope.resolve('nearTexture'); this.farTextureId = scope.resolve('farTexture'); this.cocTextureId = scope.resolve('cocTexture'); } } class RenderPassDof extends RenderPass { destroy() { this.destroyRenderPasses(); this.cocPass = null; this.farPass = null; this.blurPass = null; this.destroyRT(this.cocRT); this.destroyRT(this.farRt); this.destroyRT(this.blurRt); this.cocRT = null; this.farRt = null; this.blurRt = null; } destroyRenderPasses() { for(let i = 0; i < this.beforePasses.length; i++){ this.beforePasses[i].destroy(); } this.beforePasses.length = 0; } destroyRT(rt) { if (rt) { rt.destroyTextureBuffers(); rt.destroy(); } } setupCocPass(device, cameraComponent, sourceTexture, nearBlur) { const format = nearBlur ? PIXELFORMAT_RG8 : PIXELFORMAT_R8; this.cocRT = this.createRenderTarget('CoCTexture', format); this.cocTexture = this.cocRT.colorBuffer; const cocPass = new RenderPassCoC(device, cameraComponent, nearBlur); cocPass.init(this.cocRT, { resizeSource: sourceTexture }); cocPass.setClearColor(Color.BLACK); return cocPass; } setupFarPass(device, sourceTexture, scale) { this.farRt = this.createRenderTarget('FarDofTexture', sourceTexture.format); const farPass = new RenderPassDownsample(device, sourceTexture, { boxFilter: true, premultiplyTexture: this.cocTexture, premultiplySrcChannel: 'r' }); farPass.init(this.farRt, { resizeSource: sourceTexture, scaleX: scale, scaleY: scale }); farPass.setClearColor(Color.BLACK); return farPass; } setupBlurPass(device, nearTexture, nearBlur, scale) { const farTexture = this.farRt?.colorBuffer; this.blurRt = this.createRenderTarget('DofBlurTexture', nearTexture.format); this.blurTexture = this.blurRt.colorBuffer; const blurPass = new RenderPassDofBlur(device, nearBlur ? nearTexture : null, farTexture, this.cocTexture); blurPass.init(this.blurRt, { resizeSource: nearTexture, scaleX: scale, scaleY: scale }); blurPass.setClearColor(Color.BLACK); return blurPass; } createTexture(name, format) { return new Texture(this.device, { name: name, width: 1, height: 1, format: format, mipmaps: false, minFilter: FILTER_LINEAR, magFilter: FILTER_LINEAR, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); } createRenderTarget(name, format) { return new RenderTarget({ colorBuffer: this.createTexture(name, format), depth: false, stencil: false }); } frameUpdate() { super.frameUpdate(); this.cocPass.focusDistance = this.focusDistance; this.cocPass.focusRange = this.focusRange; this.blurPass.blurRadiusNear = this.blurRadius; this.blurPass.blurRadiusFar = this.blurRadius * (this.highQuality ? 1 : 0.5); this.blurPass.blurRings = this.blurRings; this.blurPass.blurRingPoints = this.blurRingPoints; } constructor(device, cameraComponent, sceneTexture, sceneTextureHalf, highQuality, nearBlur){ super(device), this.focusDistance = 100, this.focusRange = 50, this.blurRadius = 1, this.blurRings = 3, this.blurRingPoints = 3, this.highQuality = true, this.cocTexture = null, this.blurTexture = null, this.cocPass = null, this.farPass = null, this.blurPass = null; this.highQuality = highQuality; this.cocPass = this.setupCocPass(device, cameraComponent, sceneTexture, nearBlur); this.beforePasses.push(this.cocPass); const sourceTexture = highQuality ? sceneTexture : sceneTextureHalf; this.farPass = this.setupFarPass(device, sourceTexture, 0.5); this.beforePasses.push(this.farPass); this.blurPass = this.setupBlurPass(device, sceneTextureHalf, nearBlur, highQuality ? 2 : 0.5); this.beforePasses.push(this.blurPass); } } const tempMeshInstances = []; const DEPTH_UNIFORM_NAME = 'uSceneDepthMap'; class RenderPassPrepass extends RenderPass { destroy() { super.destroy(); this.renderTarget?.destroy(); this.renderTarget = null; this.linearDepthTexture?.destroy(); this.linearDepthTexture = null; this.viewBindGroups.forEach((bg)=>{ bg.defaultUniformBuffer.destroy(); bg.destroy(); }); this.viewBindGroups.length = 0; } setupRenderTarget(options) { const { device } = this; this.linearDepthFormat = device.textureFloatRenderable ? PIXELFORMAT_R32F : PIXELFORMAT_RGBA8; this.linearDepthTexture = new Texture(device, { name: 'SceneLinearDepthTexture', width: 1, height: 1, format: this.linearDepthFormat, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); const renderTarget = new RenderTarget({ name: 'PrepassRT', colorBuffer: this.linearDepthTexture, depth: true, samples: 1 }); this.camera.shaderParams.sceneDepthMapLinear = true; this.init(renderTarget, options); } after() { this.device.scope.resolve(DEPTH_UNIFORM_NAME).setValue(this.linearDepthTexture); } execute() { const { renderer, scene, renderTarget } = this; const camera = this.camera.camera; const layers = scene.layers.layerList; const subLayerEnabled = scene.layers.subLayerEnabled; const isTransparent = scene.layers.subLayerList; for(let i = 0; i < layers.length; i++){ const layer = layers[i]; if (layer.id === LAYERID_DEPTH) { break; } if (layer.enabled && subLayerEnabled[i]) { if (layer.camerasSet.has(camera)) { const culledInstances = layer.getCulledInstances(camera); const meshInstances = isTransparent[i] ? culledInstances.transparent : culledInstances.opaque; for(let j = 0; j < meshInstances.length; j++){ const meshInstance = meshInstances[j]; if (meshInstance.material?.depthWrite) { tempMeshInstances.push(meshInstance); } } renderer.renderForwardLayer(camera, renderTarget, null, undefined, SHADER_PREPASS, this.viewBindGroups, { meshInstances: tempMeshInstances }); tempMeshInstances.length = 0; } } } } frameUpdate() { super.frameUpdate(); const { camera } = this; this.setClearDepth(camera.clearDepthBuffer ? 1 : undefined); let clearValue; if (camera.clearDepthBuffer) { const farClip = camera.farClip - Number.MIN_VALUE; clearValue = this.linearDepthClearValue; if (this.linearDepthFormat === PIXELFORMAT_R32F) { clearValue.r = farClip; } else { FloatPacking.float2RGBA8(farClip, clearValue); } } this.setClearColor(clearValue); } constructor(device, scene, renderer, camera, options){ super(device), this.viewBindGroups = [], this.linearDepthClearValue = new Color(0, 0, 0, 0); this.scene = scene; this.renderer = renderer; this.camera = camera; this.setupRenderTarget(options); } } var glslDepthAwareBlurPS = ` #include "screenDepthPS" varying vec2 uv0; uniform sampler2D sourceTexture; uniform vec2 sourceInvResolution; uniform int filterSize; float random(const highp vec2 w) { const vec3 m = vec3(0.06711056, 0.00583715, 52.9829189); return fract(m.z * fract(dot(w, m.xy))); } mediump float bilateralWeight(in mediump float depth, in mediump float sampleDepth) { mediump float diff = (sampleDepth - depth); return max(0.0, 1.0 - diff * diff); } void tap(inout float sum, inout float totalWeight, float weight, float depth, vec2 position) { mediump float color = texture2D(sourceTexture, position).r; mediump float textureDepth = -getLinearScreenDepth(position); mediump float bilateral = bilateralWeight(depth, textureDepth); bilateral *= weight; sum += color * bilateral; totalWeight += bilateral; } void main() { mediump float depth = -getLinearScreenDepth(uv0); mediump float totalWeight = 1.0; mediump float color = texture2D(sourceTexture, uv0 ).r; mediump float sum = color * totalWeight; for (mediump int i = -filterSize; i <= filterSize; i++) { mediump float weight = 1.0; #ifdef HORIZONTAL vec2 offset = vec2(i, 0) * sourceInvResolution; #else vec2 offset = vec2(0, i) * sourceInvResolution; #endif tap(sum, totalWeight, weight, depth, uv0 + offset); } mediump float ao = sum / totalWeight; gl_FragColor.r = ao; } `; var wgslDepthAwareBlurPS = ` #include "screenDepthPS" varying uv0: vec2f; var sourceTexture: texture_2d; var sourceTextureSampler: sampler; uniform sourceInvResolution: vec2f; uniform filterSize: i32; fn random(w: vec2f) -> f32 { const m: vec3f = vec3f(0.06711056, 0.00583715, 52.9829189); return fract(m.z * fract(dot(w, m.xy))); } fn bilateralWeight(depth: f32, sampleDepth: f32) -> f32 { let diff: f32 = (sampleDepth - depth); return max(0.0, 1.0 - diff * diff); } fn tap(sum_ptr: ptr, totalWeight_ptr: ptr, weight: f32, depth: f32, position: vec2f) { let color: f32 = textureSample(sourceTexture, sourceTextureSampler, position).r; let textureDepth: f32 = -getLinearScreenDepth(position); let bilateral: f32 = bilateralWeight(depth, textureDepth) * weight; *sum_ptr = *sum_ptr + color * bilateral; *totalWeight_ptr = *totalWeight_ptr + bilateral; } @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let depth: f32 = -getLinearScreenDepth(input.uv0); var totalWeight: f32 = 1.0; let color: f32 = textureSample(sourceTexture, sourceTextureSampler, input.uv0 ).r; var sum: f32 = color * totalWeight; for (var i: i32 = -uniform.filterSize; i <= uniform.filterSize; i = i + 1) { let weight: f32 = 1.0; #ifdef HORIZONTAL var offset: vec2f = vec2f(f32(i), 0.0) * uniform.sourceInvResolution; #else var offset: vec2f = vec2f(0.0, f32(i)) * uniform.sourceInvResolution; #endif tap(&sum, &totalWeight, weight, depth, input.uv0 + offset); } let ao: f32 = sum / totalWeight; output.color = vec4f(ao, ao, ao, 1.0); return output; } `; class RenderPassDepthAwareBlur extends RenderPassShaderQuad { execute() { this.filterSizeId.setValue(4); this.sourceTextureId.setValue(this.sourceTexture); const { width, height } = this.sourceTexture; this.sourceInvResolutionValue[0] = 1.0 / width; this.sourceInvResolutionValue[1] = 1.0 / height; this.sourceInvResolutionId.setValue(this.sourceInvResolutionValue); super.execute(); } constructor(device, sourceTexture, cameraComponent, horizontal){ super(device); this.sourceTexture = sourceTexture; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('depthAwareBlurPS', glslDepthAwareBlurPS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('depthAwareBlurPS', wgslDepthAwareBlurPS); const defines = new Map(); if (horizontal) defines.set('HORIZONTAL', ''); ShaderUtils.addScreenDepthChunkDefines(device, cameraComponent.shaderParams, defines); this.shader = ShaderUtils.createShader(device, { uniqueName: `DepthAware${horizontal ? 'Horizontal' : 'Vertical'}BlurShader`, attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'depthAwareBlurPS', fragmentDefines: defines }); const scope = this.device.scope; this.sourceTextureId = scope.resolve('sourceTexture'); this.sourceInvResolutionId = scope.resolve('sourceInvResolution'); this.sourceInvResolutionValue = new Float32Array(2); this.filterSizeId = scope.resolve('filterSize'); } } var glslSsaoPS = ` #include "screenDepthPS" varying vec2 uv0; uniform vec2 uInvResolution; uniform float uAspect; #define saturate(x) clamp(x,0.0,1.0) highp float getWFromProjectionMatrix(const mat4 p, const vec3 v) { return -v.z; } highp float getViewSpaceZFromW(const mat4 p, const float w) { return -w; } const float kLog2LodRate = 3.0; float random(const highp vec2 w) { const vec3 m = vec3(0.06711056, 0.00583715, 52.9829189); return fract(m.z * fract(dot(w, m.xy))); } highp vec2 getFragCoord() { return gl_FragCoord.xy; } highp vec3 computeViewSpacePositionFromDepth(highp vec2 uv, highp float linearDepth) { return vec3((0.5 - uv) * vec2(uAspect, 1.0) * linearDepth, linearDepth); } highp vec3 faceNormal(highp vec3 dpdx, highp vec3 dpdy) { return normalize(cross(dpdx, dpdy)); } highp vec3 computeViewSpaceNormal(const highp vec3 position) { return faceNormal(dFdx(position), dFdy(position)); } highp vec3 computeViewSpaceNormal(const highp vec3 position, const highp vec2 uv) { highp vec2 uvdx = uv + vec2(uInvResolution.x, 0.0); highp vec2 uvdy = uv + vec2(0.0, uInvResolution.y); highp vec3 px = computeViewSpacePositionFromDepth(uvdx, -getLinearScreenDepth(uvdx)); highp vec3 py = computeViewSpacePositionFromDepth(uvdy, -getLinearScreenDepth(uvdy)); highp vec3 dpdx = px - position; highp vec3 dpdy = py - position; return faceNormal(dpdx, dpdy); } uniform vec2 uSampleCount; uniform float uSpiralTurns; #define PI (3.14159) mediump vec3 tapLocation(mediump float i, const mediump float noise) { mediump float offset = ((2.0 * PI) * 2.4) * noise; mediump float angle = ((i * uSampleCount.y) * uSpiralTurns) * (2.0 * PI) + offset; mediump float radius = (i + noise + 0.5) * uSampleCount.y; return vec3(cos(angle), sin(angle), radius * radius); } highp vec2 startPosition(const float noise) { float angle = ((2.0 * PI) * 2.4) * noise; return vec2(cos(angle), sin(angle)); } uniform vec2 uAngleIncCosSin; highp mat2 tapAngleStep() { highp vec2 t = uAngleIncCosSin; return mat2(t.x, t.y, -t.y, t.x); } mediump vec3 tapLocationFast(mediump float i, mediump vec2 p, const mediump float noise) { mediump float radius = (i + noise + 0.5) * uSampleCount.y; return vec3(p, radius * radius); } uniform float uMaxLevel; uniform float uInvRadiusSquared; uniform float uMinHorizonAngleSineSquared; uniform float uBias; uniform float uPeak2; void computeAmbientOcclusionSAO(inout mediump float occlusion, mediump float i, mediump float ssDiskRadius, const highp vec2 uv, const highp vec3 origin, const mediump vec3 normal, const mediump vec2 tapPosition, const float noise) { mediump vec3 tap = tapLocationFast(i, tapPosition, noise); mediump float ssRadius = max(1.0, tap.z * ssDiskRadius); mediump vec2 uvSamplePos = uv + vec2(ssRadius * tap.xy) * uInvResolution; mediump float level = clamp(floor(log2(ssRadius)) - kLog2LodRate, 0.0, float(uMaxLevel)); highp float occlusionDepth = -getLinearScreenDepth(uvSamplePos); highp vec3 p = computeViewSpacePositionFromDepth(uvSamplePos, occlusionDepth); vec3 v = p - origin; float vv = dot(v, v); float vn = dot(v, normal); mediump float w = max(0.0, 1.0 - vv * uInvRadiusSquared); w = w * w; w *= step(vv * uMinHorizonAngleSineSquared, vn * vn); occlusion += w * max(0.0, vn + origin.z * uBias) / (vv + uPeak2); } uniform float uProjectionScaleRadius; uniform float uIntensity; uniform float uRandomize; float scalableAmbientObscurance(highp vec2 uv, highp vec3 origin, vec3 normal) { float noise = random(getFragCoord()) + uRandomize; highp vec2 tapPosition = startPosition(noise); highp mat2 angleStep = tapAngleStep(); float ssDiskRadius = -(uProjectionScaleRadius / origin.z); float occlusion = 0.0; for (float i = 0.0; i < uSampleCount.x; i += 1.0) { computeAmbientOcclusionSAO(occlusion, i, ssDiskRadius, uv, origin, normal, tapPosition, noise); tapPosition = angleStep * tapPosition; } return occlusion; } uniform float uPower; void main() { highp vec2 uv = uv0; highp float depth = -getLinearScreenDepth(uv0); highp vec3 origin = computeViewSpacePositionFromDepth(uv, depth); vec3 normal = computeViewSpaceNormal(origin, uv); float occlusion = 0.0; if (uIntensity > 0.0) { occlusion = scalableAmbientObscurance(uv, origin, normal); } float ao = max(0.0, 1.0 - occlusion * uIntensity); ao = pow(ao, uPower); gl_FragColor = vec4(ao, ao, ao, 1.0); } `; var wgslSsaoPS = ` #include "screenDepthPS" varying uv0: vec2f; uniform uInvResolution: vec2f; uniform uAspect: f32; fn getWFromProjectionMatrix(p: mat4x4f, v: vec3f) -> f32 { return -v.z; } fn getViewSpaceZFromW(p: mat4x4f, w: f32) -> f32 { return -w; } const kLog2LodRate: f32 = 3.0; fn random(w: vec2f) -> f32 { const m: vec3f = vec3f(0.06711056, 0.00583715, 52.9829189); return fract(m.z * fract(dot(w, m.xy))); } fn getFragCoord() -> vec2f { return pcPosition.xy; } fn computeViewSpacePositionFromDepth(uv: vec2f, linearDepth: f32) -> vec3f { return vec3f((0.5 - uv) * vec2f(uniform.uAspect, 1.0) * linearDepth, linearDepth); } fn faceNormal(dpdx: vec3f, dpdy: vec3f) -> vec3f { return normalize(cross(dpdx, dpdy)); } fn computeViewSpaceNormalDeriv(position: vec3f) -> vec3f { return faceNormal(dpdx(position), dpdy(position)); } fn computeViewSpaceNormalDepth(position: vec3f, uv: vec2f) -> vec3f { let uvdx: vec2f = uv + vec2f(uniform.uInvResolution.x, 0.0); let uvdy: vec2f = uv + vec2f(0.0, uniform.uInvResolution.y); let px: vec3f = computeViewSpacePositionFromDepth(uvdx, -getLinearScreenDepth(uvdx)); let py: vec3f = computeViewSpacePositionFromDepth(uvdy, -getLinearScreenDepth(uvdy)); let dpdx: vec3f = px - position; let dpdy: vec3f = py - position; return faceNormal(dpdx, dpdy); } uniform uSampleCount: vec2f; uniform uSpiralTurns: f32; const PI: f32 = 3.14159; fn tapLocation(i: f32, noise: f32) -> vec3f { let offset: f32 = ((2.0 * PI) * 2.4) * noise; let angle: f32 = ((i * uniform.uSampleCount.y) * uniform.uSpiralTurns) * (2.0 * PI) + offset; let radius: f32 = (i + noise + 0.5) * uniform.uSampleCount.y; return vec3f(cos(angle), sin(angle), radius * radius); } fn startPosition(noise: f32) -> vec2f { let angle: f32 = ((2.0 * PI) * 2.4) * noise; return vec2f(cos(angle), sin(angle)); } uniform uAngleIncCosSin: vec2f; fn tapAngleStep() -> mat2x2f { let t: vec2f = uniform.uAngleIncCosSin; return mat2x2f(vec2f(t.x, t.y), vec2f(-t.y, t.x)); } fn tapLocationFast(i: f32, p: vec2f, noise_in: f32) -> vec3f { let radius: f32 = (i + noise_in + 0.5) * uniform.uSampleCount.y; return vec3f(p.x, p.y, radius * radius); } uniform uMaxLevel: f32; uniform uInvRadiusSquared: f32; uniform uMinHorizonAngleSineSquared: f32; uniform uBias: f32; uniform uPeak2: f32; fn computeAmbientOcclusionSAO(occlusion_ptr: ptr, i: f32, ssDiskRadius: f32, uv: vec2f, origin: vec3f, normal: vec3f, tapPosition: vec2f, noise: f32) { let tap: vec3f = tapLocationFast(i, tapPosition, noise); let ssRadius: f32 = max(1.0, tap.z * ssDiskRadius); let uvSamplePos: vec2f = uv + (ssRadius * tap.xy) * uniform.uInvResolution; let level: f32 = clamp(floor(log2(ssRadius)) - kLog2LodRate, 0.0, uniform.uMaxLevel); let occlusionDepth: f32 = -getLinearScreenDepth(uvSamplePos); let p: vec3f = computeViewSpacePositionFromDepth(uvSamplePos, occlusionDepth); let v: vec3f = p - origin; let vv: f32 = dot(v, v); let vn: f32 = dot(v, normal); var w_val: f32 = max(0.0, 1.0 - vv * uniform.uInvRadiusSquared); w_val = w_val * w_val; w_val = w_val * step(vv * uniform.uMinHorizonAngleSineSquared, vn * vn); *occlusion_ptr = *occlusion_ptr + w_val * max(0.0, vn + origin.z * uniform.uBias) / (vv + uniform.uPeak2); } uniform uProjectionScaleRadius: f32; uniform uIntensity: f32; uniform uRandomize: f32; fn scalableAmbientObscurance(uv: vec2f, origin: vec3f, normal: vec3f) -> f32 { let noise: f32 = random(getFragCoord()) + uniform.uRandomize; var tapPosition: vec2f = startPosition(noise); let angleStep: mat2x2f = tapAngleStep(); let ssDiskRadius: f32 = -(uniform.uProjectionScaleRadius / origin.z); var occlusion: f32 = 0.0; for (var i: i32 = 0; i < i32(uniform.uSampleCount.x); i = i + 1) { computeAmbientOcclusionSAO(&occlusion, f32(i), ssDiskRadius, uv, origin, normal, tapPosition, noise); tapPosition = angleStep * tapPosition; } return occlusion; } uniform uPower: f32; @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; let uv: vec2f = input.uv0; let depth: f32 = -getLinearScreenDepth(input.uv0); let origin: vec3f = computeViewSpacePositionFromDepth(uv, depth); let normal: vec3f = computeViewSpaceNormalDepth(origin, uv); var occlusion: f32 = 0.0; if (uniform.uIntensity > 0.0) { occlusion = scalableAmbientObscurance(uv, origin, normal); } var ao: f32 = max(0.0, 1.0 - occlusion * uniform.uIntensity); ao = pow(ao, uniform.uPower); output.color = vec4f(ao, ao, ao, 1.0); return output; } `; class RenderPassSsao extends RenderPassShaderQuad { destroy() { this.renderTarget?.destroyTextureBuffers(); this.renderTarget?.destroy(); this.renderTarget = null; if (this.afterPasses.length > 0) { const blurRt = this.afterPasses[0].renderTarget; blurRt?.destroyTextureBuffers(); blurRt?.destroy(); } this.afterPasses.forEach((pass)=>pass.destroy()); this.afterPasses.length = 0; super.destroy(); } set scale(value) { this._scale = value; this.scaleX = value; this.scaleY = value; } get scale() { return this._scale; } createRenderTarget(name) { return new RenderTarget({ depth: false, colorBuffer: new Texture(this.device, { name: name, width: 1, height: 1, format: PIXELFORMAT_R8, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }) }); } execute() { const { device, sourceTexture, sampleCount, minAngle, scale } = this; const { width, height } = this.renderTarget.colorBuffer; const scope = device.scope; scope.resolve('uAspect').setValue(width / height); scope.resolve('uInvResolution').setValue([ 1.0 / width, 1.0 / height ]); scope.resolve('uSampleCount').setValue([ sampleCount, 1.0 / sampleCount ]); const minAngleSin = Math.sin(minAngle * math.DEG_TO_RAD); scope.resolve('uMinHorizonAngleSineSquared').setValue(minAngleSin * minAngleSin); const spiralTurns = 10.0; const step = 1.0 / (sampleCount - 0.5) * spiralTurns * 2.0 * 3.141; const radius = this.radius / scale; const bias = 0.001; const peak = 0.1 * radius; const intensity = 2 * (peak * 2.0 * 3.141) * this.intensity / sampleCount; const projectionScale = 0.5 * sourceTexture.height; scope.resolve('uSpiralTurns').setValue(spiralTurns); scope.resolve('uAngleIncCosSin').setValue([ Math.cos(step), Math.sin(step) ]); scope.resolve('uMaxLevel').setValue(0.0); scope.resolve('uInvRadiusSquared').setValue(1.0 / (radius * radius)); scope.resolve('uBias').setValue(bias); scope.resolve('uPeak2').setValue(peak * peak); scope.resolve('uIntensity').setValue(intensity); scope.resolve('uPower').setValue(this.power); scope.resolve('uProjectionScaleRadius').setValue(projectionScale * radius); scope.resolve('uRandomize').setValue(this.randomize ? this._blueNoise.value() : 0); super.execute(); } after() { this.ssaoTextureId.setValue(this.ssaoTexture); const srcTexture = this.sourceTexture; this.ssaoTextureSizeInvId.setValue([ 1.0 / srcTexture.width, 1.0 / srcTexture.height ]); } constructor(device, sourceTexture, cameraComponent, blurEnabled){ super(device), this.radius = 5, this.intensity = 1, this.power = 1, this.sampleCount = 10, this.minAngle = 5, this.randomize = false, this._scale = 1, this._blueNoise = new BlueNoise(19); this.sourceTexture = sourceTexture; this.cameraComponent = cameraComponent; ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set('ssaoPS', glslSsaoPS); ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set('ssaoPS', wgslSsaoPS); const defines = new Map(); ShaderUtils.addScreenDepthChunkDefines(device, cameraComponent.shaderParams, defines); this.shader = ShaderUtils.createShader(device, { uniqueName: 'SsaoShader', attributes: { aPosition: SEMANTIC_POSITION }, vertexChunk: 'quadVS', fragmentChunk: 'ssaoPS', fragmentDefines: defines }); const rt = this.createRenderTarget('SsaoFinalTexture'); this.ssaoTexture = rt.colorBuffer; this.init(rt, { resizeSource: this.sourceTexture }); const clearColor = new Color(0, 0, 0, 0); this.setClearColor(clearColor); if (blurEnabled) { const blurRT = this.createRenderTarget('SsaoTempTexture'); const blurPassHorizontal = new RenderPassDepthAwareBlur(device, rt.colorBuffer, cameraComponent, true); blurPassHorizontal.init(blurRT, { resizeSource: rt.colorBuffer }); blurPassHorizontal.setClearColor(clearColor); const blurPassVertical = new RenderPassDepthAwareBlur(device, blurRT.colorBuffer, cameraComponent, false); blurPassVertical.init(rt, { resizeSource: rt.colorBuffer }); blurPassVertical.setClearColor(clearColor); this.afterPasses.push(blurPassHorizontal); this.afterPasses.push(blurPassVertical); } this.ssaoTextureId = device.scope.resolve('ssaoTexture'); this.ssaoTextureSizeInvId = device.scope.resolve('ssaoTextureSizeInv'); } } class CameraFrameOptions { constructor(){ this.stencil = false; this.samples = 1; this.sceneColorMap = false; this.lastGrabLayerId = LAYERID_SKYBOX; this.lastGrabLayerIsTransparent = false; this.lastSceneLayerId = LAYERID_IMMEDIATE; this.lastSceneLayerIsTransparent = true; this.taaEnabled = false; this.bloomEnabled = false; this.ssaoType = SSAOTYPE_NONE; this.ssaoBlurEnabled = true; this.prepassEnabled = false; this.dofEnabled = false; this.dofNearBlur = false; this.dofHighQuality = true; } } const _defaultOptions = new CameraFrameOptions(); class RenderPassCameraFrame extends RenderPass { destroy() { this.reset(); } reset() { this.sceneTexture = null; this.sceneTextureHalf = null; if (this.rt) { this.rt.destroyTextureBuffers(); this.rt.destroy(); this.rt = null; } if (this.rtHalf) { this.rtHalf.destroyTextureBuffers(); this.rtHalf.destroy(); this.rtHalf = null; } this.beforePasses.forEach((pass)=>pass.destroy()); this.beforePasses.length = 0; this.prePass = null; this.scenePass = null; this.scenePassTransparent = null; this.colorGrabPass = null; this.composePass = null; this.bloomPass = null; this.ssaoPass = null; this.taaPass = null; this.afterPass = null; this.scenePassHalf = null; this.dofPass = null; } sanitizeOptions(options) { options = Object.assign({}, _defaultOptions, options); if (options.taaEnabled || options.ssaoType !== SSAOTYPE_NONE || options.dofEnabled) { options.prepassEnabled = true; } return options; } set renderTargetScale(value) { this._renderTargetScale = value; if (this.scenePass) { this.scenePass.scaleX = value; this.scenePass.scaleY = value; } } get renderTargetScale() { return this._renderTargetScale; } needsReset(options) { const currentOptions = this.options; const arraysNotEqual = (arr1, arr2)=>arr1 !== arr2 && (!(Array.isArray(arr1) && Array.isArray(arr2)) || arr1.length !== arr2.length || !arr1.every((value, index)=>value === arr2[index])); return options.ssaoType !== currentOptions.ssaoType || options.ssaoBlurEnabled !== currentOptions.ssaoBlurEnabled || options.taaEnabled !== currentOptions.taaEnabled || options.samples !== currentOptions.samples || options.stencil !== currentOptions.stencil || options.bloomEnabled !== currentOptions.bloomEnabled || options.prepassEnabled !== currentOptions.prepassEnabled || options.sceneColorMap !== currentOptions.sceneColorMap || options.dofEnabled !== currentOptions.dofEnabled || options.dofNearBlur !== currentOptions.dofNearBlur || options.dofHighQuality !== currentOptions.dofHighQuality || arraysNotEqual(options.formats, currentOptions.formats); } update(options) { options = this.sanitizeOptions(options); if (this.needsReset(options) || this.layersDirty) { this.layersDirty = false; this.reset(); } this.options = options; if (!this.sceneTexture) { this.setupRenderPasses(this.options); } } createRenderTarget(name, depth, stencil, samples, flipY) { const texture = new Texture(this.device, { name: name, width: 4, height: 4, format: this.hdrFormat, mipmaps: false, minFilter: FILTER_LINEAR, magFilter: FILTER_LINEAR, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); return new RenderTarget({ colorBuffer: texture, depth: depth, stencil: stencil, samples: samples, flipY: flipY }); } setupRenderPasses(options) { const { device } = this; const cameraComponent = this.cameraComponent; const targetRenderTarget = cameraComponent.renderTarget; this.hdrFormat = device.getRenderableHdrFormat(options.formats, true, options.samples) || PIXELFORMAT_RGBA8; this._bloomEnabled = options.bloomEnabled && this.hdrFormat !== PIXELFORMAT_RGBA8; this._sceneHalfEnabled = this._bloomEnabled || options.dofEnabled; cameraComponent.shaderParams.ssaoEnabled = options.ssaoType === SSAOTYPE_LIGHTING; const flipY = !!targetRenderTarget?.flipY; this.rt = this.createRenderTarget('SceneColor', true, options.stencil, options.samples, flipY); this.sceneTexture = this.rt.colorBuffer; if (this._sceneHalfEnabled) { this.rtHalf = this.createRenderTarget('SceneColorHalf', false, false, 1, flipY); this.sceneTextureHalf = this.rtHalf.colorBuffer; } this.sceneOptions = { resizeSource: targetRenderTarget, scaleX: this.renderTargetScale, scaleY: this.renderTargetScale }; this.createPasses(options); const allPasses = this.collectPasses(); this.beforePasses = allPasses.filter((element)=>element !== undefined && element !== null); } collectPasses() { return [ this.prePass, this.ssaoPass, this.scenePass, this.colorGrabPass, this.scenePassTransparent, this.taaPass, this.scenePassHalf, this.bloomPass, this.dofPass, this.composePass, this.afterPass ]; } createPasses(options) { this.setupScenePrepass(options); this.setupSsaoPass(options); const scenePassesInfo = this.setupScenePass(options); const sceneTextureWithTaa = this.setupTaaPass(options); this.setupSceneHalfPass(options, sceneTextureWithTaa); this.setupBloomPass(options, this.sceneTextureHalf); this.setupDofPass(options, this.sceneTexture, this.sceneTextureHalf); this.setupComposePass(options); this.setupAfterPass(options, scenePassesInfo); } setupScenePrepass(options) { if (options.prepassEnabled) { const { app, device, cameraComponent } = this; const { scene, renderer } = app; this.prePass = new RenderPassPrepass(device, scene, renderer, cameraComponent, this.sceneOptions); } } setupScenePassSettings(pass) { pass.gammaCorrection = GAMMA_NONE; pass.toneMapping = TONEMAP_NONE; } setupScenePass(options) { const { app, device, cameraComponent } = this; const { scene, renderer } = app; const composition = scene.layers; this.scenePass = new RenderPassForward(device, composition, scene, renderer); this.setupScenePassSettings(this.scenePass); this.scenePass.init(this.rt, this.sceneOptions); const lastLayerId = options.sceneColorMap ? options.lastGrabLayerId : options.lastSceneLayerId; const lastLayerIsTransparent = options.sceneColorMap ? options.lastGrabLayerIsTransparent : options.lastSceneLayerIsTransparent; const ret = { lastAddedIndex: 0, clearRenderTarget: true }; ret.lastAddedIndex = this.scenePass.addLayers(composition, cameraComponent, ret.lastAddedIndex, ret.clearRenderTarget, lastLayerId, lastLayerIsTransparent); ret.clearRenderTarget = false; if (options.sceneColorMap) { this.colorGrabPass = new RenderPassColorGrab(device); this.colorGrabPass.source = this.rt; this.scenePassTransparent = new RenderPassForward(device, composition, scene, renderer); this.setupScenePassSettings(this.scenePassTransparent); this.scenePassTransparent.init(this.rt); ret.lastAddedIndex = this.scenePassTransparent.addLayers(composition, cameraComponent, ret.lastAddedIndex, ret.clearRenderTarget, options.lastSceneLayerId, options.lastSceneLayerIsTransparent); if (!this.scenePassTransparent.rendersAnything) { this.scenePassTransparent.destroy(); this.scenePassTransparent = null; } if (this.scenePassTransparent) { if (options.prepassEnabled) { this.scenePassTransparent.depthStencilOps.storeDepth = true; } } } return ret; } setupSsaoPass(options) { const { ssaoBlurEnabled, ssaoType } = options; const { device, cameraComponent } = this; if (ssaoType !== SSAOTYPE_NONE) { this.ssaoPass = new RenderPassSsao(device, this.sceneTexture, cameraComponent, ssaoBlurEnabled); } } setupSceneHalfPass(options, sourceTexture) { if (this._sceneHalfEnabled) { this.scenePassHalf = new RenderPassDownsample(this.device, this.sceneTexture, { boxFilter: true, removeInvalid: true }); this.scenePassHalf.name = 'RenderPassSceneHalf'; this.scenePassHalf.init(this.rtHalf, { resizeSource: sourceTexture, scaleX: 0.5, scaleY: 0.5 }); this.scenePassHalf.setClearColor(Color.BLACK); } } setupBloomPass(options, inputTexture) { if (this._bloomEnabled) { this.bloomPass = new RenderPassBloom(this.device, inputTexture, this.hdrFormat); } } setupDofPass(options, inputTexture, inputTextureHalf) { if (options.dofEnabled) { this.dofPass = new RenderPassDof(this.device, this.cameraComponent, inputTexture, inputTextureHalf, options.dofHighQuality, options.dofNearBlur); } } setupTaaPass(options) { let textureWithTaa = this.sceneTexture; if (options.taaEnabled) { this.taaPass = new RenderPassTAA(this.device, this.sceneTexture, this.cameraComponent); textureWithTaa = this.taaPass.historyTexture; } return textureWithTaa; } setupComposePass(options) { this.composePass = new RenderPassCompose(this.device); this.composePass.bloomTexture = this.bloomPass?.bloomTexture; this.composePass.taaEnabled = options.taaEnabled; this.composePass.cocTexture = this.dofPass?.cocTexture; this.composePass.blurTexture = this.dofPass?.blurTexture; this.composePass.blurTextureUpscale = !this.dofPass?.highQuality; const cameraComponent = this.cameraComponent; const targetRenderTarget = cameraComponent.renderTarget; this.composePass.init(targetRenderTarget); this.composePass.ssaoTexture = options.ssaoType === SSAOTYPE_COMBINE ? this.ssaoPass.ssaoTexture : null; } setupAfterPass(options, scenePassesInfo) { const { app, cameraComponent } = this; const { scene, renderer } = app; const composition = scene.layers; const targetRenderTarget = cameraComponent.renderTarget; this.afterPass = new RenderPassForward(this.device, composition, scene, renderer); this.afterPass.init(targetRenderTarget); this.afterPass.addLayers(composition, cameraComponent, scenePassesInfo.lastAddedIndex, scenePassesInfo.clearRenderTarget); } frameUpdate() { if (this.layersDirty) { this.cameraFrame.update(); } super.frameUpdate(); const sceneTexture = this.taaPass?.update() ?? this.rt.colorBuffer; this.composePass.sceneTexture = sceneTexture; this.scenePassHalf?.setSourceTexture(sceneTexture); } constructor(app, cameraFrame, cameraComponent, options = {}){ super(app.graphicsDevice), this._renderTargetScale = 1, this.layersDirty = false, this.rt = null; this.app = app; this.cameraComponent = cameraComponent; this.cameraFrame = cameraFrame; this.options = this.sanitizeOptions(options); this.setupRenderPasses(this.options); } } class CameraFrame { destroy() { this.disable(); this.cameraLayersChanged.off(); } enable() { this.renderPassCamera = this.createRenderPass(); this.cameraComponent.renderPasses = [ this.renderPassCamera ]; } disable() { const cameraComponent = this.cameraComponent; cameraComponent.renderPasses?.forEach((renderPass)=>{ renderPass.destroy(); }); cameraComponent.renderPasses = []; cameraComponent.rendering = null; cameraComponent.jitter = 0; cameraComponent.shaderParams.ssaoEnabled = false; this.renderPassCamera = null; } createRenderPass() { return new RenderPassCameraFrame(this.app, this, this.cameraComponent, this.options); } set enabled(value) { if (this._enabled !== value) { if (value) { this.enable(); } else { this.disable(); } this._enabled = value; } } get enabled() { return this._enabled; } updateOptions() { const { options, rendering, bloom, taa, ssao } = this; options.stencil = rendering.stencil; options.samples = rendering.samples; options.sceneColorMap = rendering.sceneColorMap; options.prepassEnabled = rendering.sceneDepthMap; options.bloomEnabled = bloom.intensity > 0; options.taaEnabled = taa.enabled; options.ssaoType = ssao.type; options.ssaoBlurEnabled = ssao.blurEnabled; options.formats = rendering.renderFormats.slice(); options.dofEnabled = this.dof.enabled; options.dofNearBlur = this.dof.nearBlur; options.dofHighQuality = this.dof.highQuality; } update() { if (!this._enabled) return; const cameraComponent = this.cameraComponent; const { options, renderPassCamera, rendering, bloom, grading, vignette, fringing, taa, ssao } = this; this.updateOptions(); renderPassCamera.update(options); const { composePass, bloomPass, ssaoPass, dofPass } = renderPassCamera; renderPassCamera.renderTargetScale = math.clamp(rendering.renderTargetScale, 0.1, 1); composePass.toneMapping = rendering.toneMapping; composePass.sharpness = rendering.sharpness; if (options.bloomEnabled && bloomPass) { composePass.bloomIntensity = bloom.intensity; bloomPass.blurLevel = bloom.blurLevel; } if (options.dofEnabled) { dofPass.focusDistance = this.dof.focusDistance; dofPass.focusRange = this.dof.focusRange; dofPass.blurRadius = this.dof.blurRadius; dofPass.blurRings = this.dof.blurRings; dofPass.blurRingPoints = this.dof.blurRingPoints; } if (options.ssaoType !== SSAOTYPE_NONE) { ssaoPass.intensity = ssao.intensity; ssaoPass.power = ssao.power; ssaoPass.radius = ssao.radius; ssaoPass.sampleCount = ssao.samples; ssaoPass.minAngle = ssao.minAngle; ssaoPass.scale = ssao.scale; ssaoPass.randomize = ssao.randomize; } composePass.gradingEnabled = grading.enabled; if (grading.enabled) { composePass.gradingSaturation = grading.saturation; composePass.gradingBrightness = grading.brightness; composePass.gradingContrast = grading.contrast; composePass.gradingTint = grading.tint; } composePass.colorLUT = this.colorLUT.texture; composePass.colorLUTIntensity = this.colorLUT.intensity; composePass.vignetteEnabled = vignette.intensity > 0; if (composePass.vignetteEnabled) { composePass.vignetteInner = vignette.inner; composePass.vignetteOuter = vignette.outer; composePass.vignetteCurvature = vignette.curvature; composePass.vignetteIntensity = vignette.intensity; composePass.vignetteColor.copy(vignette.color); } composePass.fringingEnabled = fringing.intensity > 0; if (composePass.fringingEnabled) { composePass.fringingIntensity = fringing.intensity; } cameraComponent.jitter = taa.enabled ? taa.jitter : 0; composePass.debug = this.debug; if (composePass.debug === 'ssao' && options.ssaoType === SSAOTYPE_NONE) composePass.debug = null; if (composePass.debug === 'vignette' && !composePass.vignetteEnabled) composePass.debug = null; } constructor(app, cameraComponent){ this._enabled = true; this.rendering = { renderFormats: [ PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F ], stencil: false, renderTargetScale: 1.0, samples: 1, sceneColorMap: false, sceneDepthMap: false, toneMapping: 0, sharpness: 0.0 }; this.ssao = { type: SSAOTYPE_NONE, blurEnabled: true, randomize: false, intensity: 0.5, radius: 30, samples: 12, power: 6, minAngle: 10, scale: 1 }; this.bloom = { intensity: 0, blurLevel: 16 }; this.grading = { enabled: false, brightness: 1, contrast: 1, saturation: 1, tint: new Color(1, 1, 1, 1) }; this.colorLUT = { texture: null, intensity: 1 }; this.vignette = { intensity: 0, inner: 0.5, outer: 1, curvature: 0.5, color: new Color(0, 0, 0) }; this.taa = { enabled: false, jitter: 1 }; this.fringing = { intensity: 0 }; this.dof = { enabled: false, nearBlur: false, focusDistance: 100, focusRange: 10, blurRadius: 3, blurRings: 4, blurRingPoints: 5, highQuality: true }; this.debug = null; this.options = new CameraFrameOptions(); this.renderPassCamera = null; this.app = app; this.cameraComponent = cameraComponent; this.updateOptions(); this.enable(); this.cameraLayersChanged = cameraComponent.on('set:layers', ()=>{ if (this.renderPassCamera) this.renderPassCamera.layersDirty = true; }); } } const tmpV1$1 = new Vec3(); const rotation$3 = new Quat(); class Pose { copy(other) { return this.set(other.position, other.angles, other.distance); } clone() { return new Pose(this.position.clone(), this.angles.clone(), this.distance); } equalsApprox(other, epsilon = 1e-6) { return this.position.equalsApprox(other.position, epsilon) && this.angles.equalsApprox(other.angles, epsilon) && Math.abs(this.distance - other.distance) < epsilon; } lerp(lhs, rhs, alpha1, alpha2 = alpha1, alpha3 = alpha1) { this.position.lerp(lhs.position, rhs.position, alpha1); this.angles.x = math.lerpAngle(lhs.angles.x, rhs.angles.x, alpha2) % 360; this.angles.y = math.lerpAngle(lhs.angles.y, rhs.angles.y, alpha2) % 360; this.angles.z = math.lerpAngle(lhs.angles.z, rhs.angles.z, alpha2) % 360; this.distance = math.lerp(lhs.distance, rhs.distance, alpha3); return this; } move(offset) { this.position.add(offset); this.position.x = math.clamp(this.position.x, this.xRange.x, this.xRange.y); this.position.y = math.clamp(this.position.y, this.yRange.x, this.yRange.y); this.position.z = math.clamp(this.position.z, this.zRange.x, this.zRange.y); return this; } rotate(euler) { this.angles.add(euler); this.angles.x %= 360; this.angles.y %= 360; this.angles.z %= 360; this.angles.x = math.clamp(this.angles.x, this.pitchRange.x, this.pitchRange.y); this.angles.y = math.clamp(this.angles.y, this.yawRange.x, this.yawRange.y); return this; } set(position, angles, distance) { this.position.copy(position); this.angles.copy(angles); this.distance = distance; return this; } look(from, to) { this.position.copy(from); this.distance = from.distance(to); const dir = tmpV1$1.sub2(to, from).normalize(); const elev = Math.atan2(-dir.y, Math.sqrt(dir.x * dir.x + dir.z * dir.z)) * math.RAD_TO_DEG; const azim = Math.atan2(-dir.x, -dir.z) * math.RAD_TO_DEG; this.angles.set(-elev, azim, 0); return this; } getFocus(out) { return rotation$3.setFromEulerAngles(this.angles).transformVector(Vec3.FORWARD, out).mulScalar(this.distance).add(this.position); } constructor(position = Vec3.ZERO, angles = Vec3.ZERO, distance = 0){ this.position = new Vec3(); this.angles = new Vec3(); this.distance = 0; this.pitchRange = new Vec2(-Infinity, Infinity); this.yawRange = new Vec2(-Infinity, Infinity); this.xRange = new Vec2(-Infinity, Infinity); this.yRange = new Vec2(-Infinity, Infinity); this.zRange = new Vec2(-Infinity, Infinity); this.set(position, angles, distance); } } class InputDelta { add(other) { for(let i = 0; i < this._value.length; i++){ this._value[i] += other._value[i] || 0; } return this; } append(offsets) { for(let i = 0; i < this._value.length; i++){ this._value[i] += offsets[i] || 0; } return this; } copy(other) { for(let i = 0; i < this._value.length; i++){ this._value[i] = other._value[i] || 0; } return this; } length() { let sum = 0; for (const value of this._value){ sum += value * value; } return Math.sqrt(sum); } read() { const value = this._value.slice(); this._value.fill(0); return value; } constructor(arg){ if (Array.isArray(arg)) { this._value = arg.slice(); } else { this._value = new Array(+arg).fill(0); } } } class InputFrame { read() { const frame = {}; for(const name in this.deltas){ frame[name] = this.deltas[name].read(); } return frame; } constructor(data){ this.deltas = {}; for(const name in data){ this.deltas[name] = new InputDelta(data[name]); } } } class InputSource extends InputFrame { on(event, callback) { this._events.on(event, callback); } off(event, callback) { this._events.off(event, callback); } fire(event, ...args) { this._events.fire(event, ...args); } attach(element) { if (this._element) { this.detach(); } this._element = element; } detach() { if (!this._element) { return; } this._element = null; this.read(); } destroy() { this.detach(); this._events.off(); } constructor(...args){ super(...args), this._element = null, this._events = new EventHandler(); } } class InputConsumer { update(frame, dt) { frame.read(); } } let InputController$1 = class InputController extends InputConsumer { attach(pose, smooth = true) {} detach() {} update(frame, dt) { super.update(frame, dt); return this._pose; } destroy() { this.detach(); } constructor(...args){ super(...args), this._pose = new Pose(); } }; const DOUBLE_TAP_THRESHOLD = 250; const DOUBLE_TAP_VARIANCE = 100; const movementState = ()=>{ const state = new Map(); return { down: (event)=>{ state.set(event.pointerId, [ event.screenX, event.screenY ]); }, move: (event)=>{ if (!state.has(event.pointerId)) { return [ 0, 0 ]; } const prev = state.get(event.pointerId); const mvX = event.screenX - prev[0]; const mvY = event.screenY - prev[1]; prev[0] = event.screenX; prev[1] = event.screenY; return [ mvX, mvY ]; }, up: (event)=>{ state.delete(event.pointerId); } }; }; const v$1 = new Vec2(); class VirtualJoystick { get value() { return this._value; } down(x, y) { this._position.set(x, y); this._value.set(0, 0); return [ x, y, x, y ]; } move(x, y) { v$1.set(x - this._position.x, y - this._position.y); if (v$1.length() > this._range) { v$1.normalize().mulScalar(this._range); } this._value.set(math.clamp(v$1.x / this._range, -1, 1), math.clamp(v$1.y / this._range, -1, 1)); const { x: bx, y: by } = this._position; return [ bx, by, bx + v$1.x, by + v$1.y ]; } up() { this._position.set(0, 0); this._value.set(0, 0); return [ -1, -1, -1, -1 ]; } constructor({ range } = {}){ this._range = 70; this._position = new Vec2(); this._value = new Vec2(); this._range = range ?? this._range; } } const startsWith = (str, prefix)=>str.indexOf(prefix) === 0; const endsWith = (str, suffix)=>str.indexOf(suffix, str.length - suffix.length) !== -1; class DualGestureSource extends InputSource { set layout(value) { if (this._layout === value) { return; } this._layout = value; this.read(); this._pointerData.clear(); } get layout() { return this._layout; } get leftJoystick() { return this._leftJoystick; } get rightJoystick() { return this._rightJoystick; } _onPointerDown(event) { const { pointerType, pointerId, clientX, clientY } = event; this._movementState.down(event); if (pointerType !== 'touch') { return; } this._element?.setPointerCapture(pointerId); const left = clientX < window.innerWidth * 0.5; this._pointerData.set(pointerId, { x: clientX, y: clientY, left }); const now = Date.now(); const sqrDist = (this._lastPointer.x - clientX) ** 2 + (this._lastPointer.y - clientY) ** 2; if (sqrDist < DOUBLE_TAP_VARIANCE && now - this._lastPointer.time < DOUBLE_TAP_THRESHOLD) { this.deltas.doubleTap.append([ 1 ]); } this._lastPointer.x = clientX; this._lastPointer.y = clientY; this._lastPointer.time = now; if (left && startsWith(this._layout, 'joystick')) { this.fire('joystick:position:left', this._leftJoystick.down(clientX, clientY)); } if (!left && endsWith(this._layout, 'joystick')) { this.fire('joystick:position:right', this._rightJoystick.down(clientX, clientY)); } } _onPointerMove(event) { const { pointerType, pointerId, target, clientX, clientY } = event; const [movementX, movementY] = this._movementState.move(event); if (pointerType !== 'touch') { return; } if (target !== this._element) { return; } const data = this._pointerData.get(pointerId); if (!data) { return; } const { left } = data; data.x = clientX; data.y = clientY; if (left) { if (startsWith(this._layout, 'joystick')) { this.fire('joystick:position:left', this._leftJoystick.move(clientX, clientY)); } else { this.deltas.leftInput.append([ movementX, movementY ]); } } else { if (endsWith(this._layout, 'joystick')) { this.fire('joystick:position:right', this._rightJoystick.move(clientX, clientY)); } else { this.deltas.rightInput.append([ movementX, movementY ]); } } } _onPointerUp(event) { const { pointerType, pointerId } = event; this._movementState.up(event); if (pointerType !== 'touch') { return; } this._element?.releasePointerCapture(pointerId); const data = this._pointerData.get(pointerId); if (!data) { return; } const { left } = data; this._pointerData.delete(pointerId); if (left && startsWith(this._layout, 'joystick')) { this.fire('joystick:position:left', this._leftJoystick.up()); } if (!left && endsWith(this._layout, 'joystick')) { this.fire('joystick:position:right', this._rightJoystick.up()); } } attach(element) { super.attach(element); this._element = element; this._element.addEventListener('pointerdown', this._onPointerDown); this._element.addEventListener('pointermove', this._onPointerMove); this._element.addEventListener('pointerup', this._onPointerUp); this._element.addEventListener('pointercancel', this._onPointerUp); } detach() { if (!this._element) { return; } this._element.removeEventListener('pointerdown', this._onPointerDown); this._element.removeEventListener('pointermove', this._onPointerMove); this._element.removeEventListener('pointerup', this._onPointerUp); this._element.removeEventListener('pointercancel', this._onPointerUp); this._pointerData.clear(); super.detach(); } read() { this.deltas.leftInput.append([ this._leftJoystick.value.x, this._leftJoystick.value.y ]); this.deltas.rightInput.append([ this._rightJoystick.value.x, this._rightJoystick.value.y ]); return super.read(); } destroy() { this._leftJoystick.up(); this._rightJoystick.up(); super.destroy(); } constructor(layout){ super({ leftInput: [ 0, 0 ], rightInput: [ 0, 0 ], doubleTap: [ 0 ] }), this._movementState = movementState(), this._layout = 'joystick-touch', this._pointerData = new Map(), this._lastPointer = { x: 0, y: 0, time: 0 }; if (layout) { this.layout = layout; } this._leftJoystick = new VirtualJoystick(); this._rightJoystick = new VirtualJoystick(); this._onPointerDown = this._onPointerDown.bind(this); this._onPointerMove = this._onPointerMove.bind(this); this._onPointerUp = this._onPointerUp.bind(this); } } const tmpVa = new Vec2(); class MultiTouchSource extends InputSource { _onPointerDown(event) { const { pointerId, pointerType } = event; this._movementState.down(event); if (pointerType !== 'touch') { return; } this._element?.setPointerCapture(pointerId); this._pointerEvents.set(pointerId, event); this.deltas.count.append([ 1 ]); if (this._pointerEvents.size > 1) { this._getMidPoint(this._pointerPos); this._pinchDist = this._getPinchDist(); } } _onPointerMove(event) { const { pointerType, target, pointerId } = event; const [movementX, movementY] = this._movementState.move(event); if (pointerType !== 'touch') { return; } if (target !== this._element) { return; } if (this._pointerEvents.size === 0) { return; } this._pointerEvents.set(pointerId, event); if (this._pointerEvents.size > 1) { const mid = this._getMidPoint(tmpVa); this.deltas.touch.append([ mid.x - this._pointerPos.x, mid.y - this._pointerPos.y ]); this._pointerPos.copy(mid); const pinchDist = this._getPinchDist(); if (this._pinchDist > 0) { this.deltas.pinch.append([ this._pinchDist - pinchDist ]); } this._pinchDist = pinchDist; } else { this.deltas.touch.append([ movementX, movementY ]); } } _onPointerUp(event) { const { pointerType, pointerId } = event; this._movementState.up(event); if (pointerType !== 'touch') { return; } this._element?.releasePointerCapture(pointerId); this._pointerEvents.delete(pointerId); this.deltas.count.append([ -1 ]); if (this._pointerEvents.size < 2) { this._pinchDist = -1; } this._pointerPos.set(0, 0); } _onContextMenu(event) { event.preventDefault(); } _getMidPoint(out) { if (this._pointerEvents.size < 2) { return out.set(0, 0); } const [a, b] = this._pointerEvents.values(); const dx = a.clientX - b.clientX; const dy = a.clientY - b.clientY; return out.set(b.clientX + dx * 0.5, b.clientY + dy * 0.5); } _getPinchDist() { if (this._pointerEvents.size < 2) { return 0; } const [a, b] = this._pointerEvents.values(); const dx = a.clientX - b.clientX; const dy = a.clientY - b.clientY; return Math.sqrt(dx * dx + dy * dy); } attach(element) { super.attach(element); this._element = element; this._element.addEventListener('pointerdown', this._onPointerDown); this._element.addEventListener('pointermove', this._onPointerMove); this._element.addEventListener('pointerup', this._onPointerUp); this._element.addEventListener('pointercancel', this._onPointerUp); this._element.addEventListener('contextmenu', this._onContextMenu); } detach() { if (!this._element) { return; } this._element.removeEventListener('pointerdown', this._onPointerDown); this._element.removeEventListener('pointermove', this._onPointerMove); this._element.removeEventListener('pointerup', this._onPointerUp); this._element.removeEventListener('pointercancel', this._onPointerUp); this._element.removeEventListener('contextmenu', this._onContextMenu); this._pointerEvents.clear(); super.detach(); } constructor(){ super({ touch: [ 0, 0 ], count: [ 0 ], pinch: [ 0 ] }), this._movementState = movementState(), this._pointerEvents = new Map(), this._pointerPos = new Vec2(), this._pinchDist = -1; this._onPointerDown = this._onPointerDown.bind(this); this._onPointerMove = this._onPointerMove.bind(this); this._onPointerUp = this._onPointerUp.bind(this); this._onContextMenu = this._onContextMenu.bind(this); } } const PASSIVE = { passive: false }; const KEY_CODES = { A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9, K: 10, L: 11, M: 12, N: 13, O: 14, P: 15, Q: 16, R: 17, S: 18, T: 19, U: 20, V: 21, W: 22, X: 23, Y: 24, Z: 25, '0': 26, '1': 27, '2': 28, '3': 29, '4': 30, '5': 31, '6': 32, '7': 33, '8': 34, '9': 35, UP: 36, DOWN: 37, LEFT: 38, RIGHT: 39, SPACE: 40, SHIFT: 41, CTRL: 42 }; const KEY_COUNT = Object.keys(KEY_CODES).length; const array = Array(KEY_COUNT).fill(0); class KeyboardMouseSource extends InputSource { _onWheel(event) { event.preventDefault(); this.deltas.wheel.append([ event.deltaY ]); } _onPointerDown(event) { this._movementState.down(event); if (event.pointerType !== 'mouse') { return; } if (this._pointerLock) { if (document.pointerLockElement !== this._element) { this._element?.requestPointerLock(); } } else { this._element?.setPointerCapture(event.pointerId); } this._clearButtons(); this._button[event.button] = 1; this.deltas.button.append(this._button); if (this._pointerId !== -1) { return; } this._pointerId = event.pointerId; } _onPointerMove(event) { const [movementX, movementY] = this._pointerLock && document.pointerLockElement === this._element ? [ event.movementX, event.movementY ] : this._movementState.move(event); if (event.pointerType !== 'mouse') { return; } if (event.target !== this._element) { return; } if (this._pointerLock) { if (document.pointerLockElement !== this._element) { return; } } else { if (this._pointerId !== event.pointerId) { return; } } this.deltas.mouse.append([ movementX, movementY ]); } _onPointerUp(event) { this._movementState.up(event); if (event.pointerType !== 'mouse') { return; } if (!this._pointerLock) { this._element?.releasePointerCapture(event.pointerId); } this._clearButtons(); this.deltas.button.append(this._button); if (this._pointerId !== event.pointerId) { return; } this._pointerId = -1; } _onContextMenu(event) { event.preventDefault(); } _onKeyDown(event) { if (this._pointerLock && document.pointerLockElement !== this._element) { return; } event.stopPropagation(); this._setKey(event.code, 1); } _onKeyUp(event) { event.stopPropagation(); this._setKey(event.code, 0); } _clearButtons() { for(let i = 0; i < this._button.length; i++){ if (this._button[i] === 1) { this._button[i] = -1; continue; } this._button[i] = 0; } } _setKey(code, value) { if (!this._keyMap.has(code)) { return; } this._keyNow[this._keyMap.get(code) ?? 0] = value; } attach(element) { super.attach(element); this._element = element; this._element.addEventListener('wheel', this._onWheel, PASSIVE); this._element.addEventListener('pointerdown', this._onPointerDown); this._element.addEventListener('pointermove', this._onPointerMove); this._element.addEventListener('pointerup', this._onPointerUp); this._element.addEventListener('pointercancel', this._onPointerUp); this._element.addEventListener('pointerleave', this._onPointerUp); this._element.addEventListener('contextmenu', this._onContextMenu); window.addEventListener('keydown', this._onKeyDown, false); window.addEventListener('keyup', this._onKeyUp, false); } detach() { if (!this._element) { return; } this._element.removeEventListener('wheel', this._onWheel, PASSIVE); this._element.removeEventListener('pointerdown', this._onPointerDown); this._element.removeEventListener('pointermove', this._onPointerMove); this._element.removeEventListener('pointerup', this._onPointerUp); this._element.removeEventListener('pointercancel', this._onPointerUp); this._element.removeEventListener('pointerleave', this._onPointerUp); this._element.removeEventListener('contextmenu', this._onContextMenu); window.removeEventListener('keydown', this._onKeyDown, false); window.removeEventListener('keyup', this._onKeyUp, false); this._keyNow.fill(0); this._keyPrev.fill(0); super.detach(); } read() { for(let i = 0; i < array.length; i++){ array[i] = this._keyNow[i] - this._keyPrev[i]; this._keyPrev[i] = this._keyNow[i]; } this.deltas.key.append(array); return super.read(); } constructor({ pointerLock = false } = {}){ super({ key: Array(KEY_COUNT).fill(0), button: [ 0, 0, 0 ], mouse: [ 0, 0 ], wheel: [ 0 ] }), this._movementState = movementState(), this._pointerId = -1, this._keyMap = new Map(), this._keyPrev = Array(KEY_COUNT).fill(0), this._keyNow = Array(KEY_COUNT).fill(0), this._button = Array(3).fill(0); this._pointerLock = pointerLock ?? false; const { keyCode } = KeyboardMouseSource; for(let i = 0; i < 26; i++){ const code = `Key${String.fromCharCode('A'.charCodeAt(0) + i)}`; this._keyMap.set(code, keyCode.A + i); } for(let i = 0; i < 10; i++){ const code = `Digit${i}`; this._keyMap.set(code, keyCode['0'] + i); } this._keyMap.set('ArrowUp', keyCode.UP); this._keyMap.set('ArrowDown', keyCode.DOWN); this._keyMap.set('ArrowLeft', keyCode.LEFT); this._keyMap.set('ArrowRight', keyCode.RIGHT); this._keyMap.set('Space', keyCode.SPACE); this._keyMap.set('ShiftLeft', keyCode.SHIFT); this._keyMap.set('ShiftRight', keyCode.SHIFT); this._keyMap.set('ControlLeft', keyCode.CTRL); this._keyMap.set('ControlRight', keyCode.CTRL); this._onWheel = this._onWheel.bind(this); this._onPointerDown = this._onPointerDown.bind(this); this._onPointerMove = this._onPointerMove.bind(this); this._onPointerUp = this._onPointerUp.bind(this); this._onContextMenu = this._onContextMenu.bind(this); this._onKeyDown = this._onKeyDown.bind(this); this._onKeyUp = this._onKeyUp.bind(this); } } KeyboardMouseSource.keyCode = KEY_CODES; const BUTTON_CODES = { A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, SELECT: 8, START: 9, LEFT_STICK: 10, RIGHT_STICK: 11 }; const BUTTON_COUNT = Object.keys(BUTTON_CODES).length; class GamepadSource extends InputSource { read() { const gamepads = navigator.getGamepads(); for(let i = 0; i < gamepads.length; i++){ const gp = gamepads[i]; if (!gp) { continue; } if (gp.mapping !== 'standard') { continue; } if (gp.axes.length < 4) { continue; } if (gp.buttons.length < BUTTON_COUNT) { continue; } const { buttons, axes } = gp; for(let j = 0; j < this._buttonPrev.length; j++){ const state = +buttons[j].pressed; this.deltas.buttons[j] = state - this._buttonPrev[j]; this._buttonPrev[j] = state; } this.deltas.leftStick.append([ axes[0], axes[1] ]); this.deltas.rightStick.append([ axes[2], axes[3] ]); } return super.read(); } constructor(){ super({ buttons: Array(BUTTON_COUNT).fill(0), leftStick: [ 0, 0 ], rightStick: [ 0, 0 ] }), this._buttonPrev = Array(BUTTON_COUNT).fill(0); } } GamepadSource.buttonCode = BUTTON_CODES; const damp = (damping, dt)=>1 - Math.pow(damping, dt * 1000); const offset$1 = new Vec3(); const angles$1 = new Vec3(); const forward = new Vec3(); const right = new Vec3(); const up = new Vec3(); const rotation$2 = new Quat(); let FlyController$1 = class FlyController extends InputController$1 { set pitchRange(value) { this._targetPose.pitchRange.copy(value); this._pose.copy(this._targetPose.rotate(Vec3.ZERO)); } get pitchRange() { return this._targetPose.pitchRange; } set yawRange(value) { this._targetPose.yawRange.copy(value); this._pose.copy(this._targetPose.rotate(Vec3.ZERO)); } get yawRange() { return this._targetPose.yawRange; } attach(pose, smooth = true) { this._targetPose.copy(pose); if (!smooth) { this._pose.copy(this._targetPose); } } detach() { this._targetPose.copy(this._pose); } update(frame, dt) { const { move, rotate } = frame.read(); this._targetPose.rotate(angles$1.set(-rotate[1], -rotate[0], 0)); rotation$2.setFromEulerAngles(this._pose.angles); rotation$2.transformVector(Vec3.FORWARD, forward); rotation$2.transformVector(Vec3.RIGHT, right); rotation$2.transformVector(Vec3.UP, up); offset$1.set(0, 0, 0); offset$1.add(forward.mulScalar(move[2])); offset$1.add(right.mulScalar(move[0])); offset$1.add(up.mulScalar(move[1])); this._targetPose.move(offset$1); return this._pose.lerp(this._pose, this._targetPose, damp(this.moveDamping, dt), damp(this.rotateDamping, dt)); } destroy() { this.detach(); } constructor(...args){ super(...args), this._targetPose = new Pose(), this.rotateDamping = 0.98, this.moveDamping = 0.98; } }; const dir = new Vec3(); const offset = new Vec3(); const angles = new Vec3(); const rotation$1 = new Quat(); let OrbitController$1 = class OrbitController extends InputController$1 { set pitchRange(range) { this._targetRootPose.pitchRange.copy(range); this._rootPose.copy(this._targetRootPose.rotate(Vec3.ZERO)); } get pitchRange() { return this._targetRootPose.pitchRange; } set yawRange(range) { this._targetRootPose.yawRange.copy(range); this._rootPose.copy(this._targetRootPose.rotate(Vec3.ZERO)); } get yawRange() { return this._targetRootPose.yawRange; } set zoomRange(range) { this._targetChildPose.zRange.copy(range); this._childPose.copy(this._targetChildPose.move(Vec3.ZERO)); } get zoomRange() { return this._targetRootPose.zRange; } attach(pose, smooth = true) { this._targetRootPose.set(pose.getFocus(dir), pose.angles, 0); this._targetChildPose.position.set(0, 0, pose.distance); if (!smooth) { this._rootPose.copy(this._targetRootPose); this._childPose.copy(this._targetChildPose); } } detach() { this._targetRootPose.copy(this._rootPose); this._targetChildPose.copy(this._childPose); } update(frame, dt) { const { move, rotate } = frame.read(); offset.set(move[0], move[1], 0); rotation$1.setFromEulerAngles(this._rootPose.angles).transformVector(offset, offset); this._targetRootPose.move(offset); const { z: dist } = this._targetChildPose.position; this._targetChildPose.move(offset.set(0, 0, dist * (1 + move[2]) - dist)); this._targetRootPose.rotate(angles.set(-rotate[1], -rotate[0], 0)); this._rootPose.lerp(this._rootPose, this._targetRootPose, damp(this.moveDamping, dt), damp(this.rotateDamping, dt), 1); this._childPose.lerp(this._childPose, this._targetChildPose, damp(this.zoomDamping, dt), 1, 1); rotation$1.setFromEulerAngles(this._rootPose.angles).transformVector(this._childPose.position, offset).add(this._rootPose.position); return this._pose.set(offset, this._rootPose.angles, this._childPose.position.z); } destroy() { this.detach(); } constructor(...args){ super(...args), this._targetRootPose = new Pose(), this._rootPose = new Pose(), this._targetChildPose = new Pose(), this._childPose = new Pose(), this.rotateDamping = 0.98, this.moveDamping = 0.98, this.zoomDamping = 0.98; } }; /** * Base class for all PlayCanvas Web Components that initialize asynchronously. */ class AsyncElement extends HTMLElement { /** @ignore */ constructor() { super(); this._readyPromise = new Promise((resolve) => { this._readyResolve = resolve; }); } get closestApp() { var _a; return (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pc-app'); } get closestEntity() { var _a; return (_a = this.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pc-entity'); } /** * Called when the element is fully initialized and ready. * Subclasses should call this when they're ready. */ _onReady() { this._readyResolve(); this.dispatchEvent(new CustomEvent('ready')); } /** * Returns a promise that resolves with this element when it's ready. * @returns A promise that resolves with this element when it's ready. */ ready() { return this._readyPromise.then(() => this); } } /** * The ModuleElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-module/ | ``} elements. * The ModuleElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class ModuleElement extends HTMLElement { /** @ignore */ constructor() { super(); this.loadPromise = this.loadModule(); } async loadModule() { const name = this.getAttribute('name'); const glueUrl = this.getAttribute('glue'); const wasmUrl = this.getAttribute('wasm'); const fallbackUrl = this.getAttribute('fallback'); const config = { glueUrl, wasmUrl, fallbackUrl }; if (name === 'Basis') { basisInitialize(config); } else { WasmModule.setConfig(name, config); await new Promise((resolve) => { WasmModule.getInstance(name, () => resolve()); }); } } getLoadPromise() { return this.loadPromise; } } customElements.define('pc-module', ModuleElement); /** * The AppElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | ``} elements. * The AppElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class AppElement extends AsyncElement { /** * Creates a new AppElement instance. * * @ignore */ constructor() { super(); /** * The canvas element. */ this._canvas = null; this._alpha = true; this._backend = 'webgl2'; this._antialias = true; this._depth = true; this._stencil = true; this._highResolution = true; this._hierarchyReady = false; this._picker = null; this._hasPointerListeners = { pointerenter: false, pointerleave: false, pointerdown: false, pointerup: false, pointermove: false }; this._hoveredEntity = null; this._pointerHandlers = { pointermove: null, pointerdown: null, pointerup: null }; /** * The PlayCanvas application instance. */ this.app = null; // Bind methods to maintain 'this' context this._onWindowResize = this._onWindowResize.bind(this); } async connectedCallback() { // Get all pc-module elements that are direct children of the pc-app element const moduleElements = this.querySelectorAll(':scope > pc-module'); // Wait for all modules to load await Promise.all(Array.from(moduleElements).map(module => module.getLoadPromise())); // Create and append the canvas to the element this._canvas = document.createElement('canvas'); this.appendChild(this._canvas); // Configure device types based on backend selection const backendToDeviceTypes = { webgpu: ['webgpu', 'webgl2'], // fallback to webgl2 if webgpu not available webgl2: ['webgl2'], null: ['null'] }; const deviceTypes = backendToDeviceTypes[this._backend] || []; const device = await createGraphicsDevice(this._canvas, { // @ts-ignore - alpha needs to be documented alpha: this._alpha, antialias: this._antialias, depth: this._depth, deviceTypes: deviceTypes, stencil: this._stencil }); device.maxPixelRatio = this._highResolution ? window.devicePixelRatio : 1; const createOptions = new AppOptions(); createOptions.graphicsDevice = device; createOptions.keyboard = new Keyboard(window); createOptions.mouse = new Mouse(this._canvas); createOptions.componentSystems = [ AnimComponentSystem, AnimationComponentSystem, AudioListenerComponentSystem, ButtonComponentSystem, CameraComponentSystem, CollisionComponentSystem, ElementComponentSystem, GSplatComponentSystem, JointComponentSystem, LayoutChildComponentSystem, LayoutGroupComponentSystem, LightComponentSystem, ModelComponentSystem, ParticleSystemComponentSystem, RenderComponentSystem, RigidBodyComponentSystem, ScreenComponentSystem, ScriptComponentSystem, ScrollbarComponentSystem, ScrollViewComponentSystem, SoundComponentSystem, SpriteComponentSystem, ZoneComponentSystem ]; createOptions.resourceHandlers = [ AnimClipHandler, AnimationHandler, AnimStateGraphHandler, AudioHandler, BinaryHandler, CssHandler, ContainerHandler, CubemapHandler, FolderHandler, FontHandler, GSplatHandler, HierarchyHandler, HtmlHandler, JsonHandler, MaterialHandler, ModelHandler, RenderHandler, ScriptHandler, SceneHandler, ShaderHandler, SpriteHandler, TemplateHandler, TextHandler, TextureAtlasHandler, TextureHandler ]; createOptions.soundManager = new SoundManager(); createOptions.lightmapper = Lightmapper; createOptions.batchManager = BatchManager; createOptions.xr = XrManager; this.app = new AppBase(this._canvas); this.app.init(createOptions); this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW); this.app.setCanvasResolution(RESOLUTION_AUTO); this._pickerCreate(); // Get all pc-asset elements that are direct children of the pc-app element const assetElements = this.querySelectorAll(':scope > pc-asset'); Array.from(assetElements).forEach((assetElement) => { assetElement.createAsset(); const asset = assetElement.asset; if (asset) { this.app.assets.add(asset); } }); // Get all pc-material elements that are direct children of the pc-app element const materialElements = this.querySelectorAll(':scope > pc-material'); Array.from(materialElements).forEach((materialElement) => { materialElement.createMaterial(); }); // Create all entities const entityElements = this.querySelectorAll('pc-entity'); Array.from(entityElements).forEach((entityElement) => { entityElement.createEntity(this.app); }); // Build hierarchy entityElements.forEach((entityElement) => { entityElement.buildHierarchy(this.app); }); this._hierarchyReady = true; // Load assets before starting the application this.app.preload(() => { // Start the application this.app.start(); // Handle window resize to keep the canvas responsive window.addEventListener('resize', this._onWindowResize); this._onReady(); }); } disconnectedCallback() { this._pickerDestroy(); // Clean up the application if (this.app) { this.app.destroy(); this.app = null; } // Remove event listeners window.removeEventListener('resize', this._onWindowResize); // Remove the canvas if (this._canvas && this.contains(this._canvas)) { this.removeChild(this._canvas); this._canvas = null; } } _onWindowResize() { if (this.app) { this.app.resizeCanvas(); } } _pickerCreate() { const { width, height } = this.app.graphicsDevice; this._picker = new Picker$1(this.app, width, height); // Create bound handlers but don't attach them yet this._pointerHandlers.pointermove = this._onPointerMove.bind(this); this._pointerHandlers.pointerdown = this._onPointerDown.bind(this); this._pointerHandlers.pointerup = this._onPointerUp.bind(this); // Listen for pointer listeners being added/removed ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => { this.addEventListener(`${type}:connect`, () => this._onPointerListenerAdded(type)); this.addEventListener(`${type}:disconnect`, () => this._onPointerListenerRemoved(type)); }); } _pickerDestroy() { if (this._canvas) { Object.entries(this._pointerHandlers).forEach(([type, handler]) => { if (handler) { this._canvas.removeEventListener(type, handler); } }); } this._picker = null; this._pointerHandlers = { pointermove: null, pointerdown: null, pointerup: null }; } // New helper to convert CSS coordinates to canvas (picker) coordinates _getPickerCoordinates(event) { // Get the canvas' bounding rectangle in CSS pixels. const canvasRect = this._canvas.getBoundingClientRect(); // Compute scale factors based on canvas actual resolution vs. its CSS display size. const scaleX = this._canvas.width / canvasRect.width; const scaleY = this._canvas.height / canvasRect.height; // Convert the client coordinates accordingly. const x = (event.clientX - canvasRect.left) * scaleX; const y = (event.clientY - canvasRect.top) * scaleY; return { x, y }; } _onPointerMove(event) { if (!this._picker || !this.app) return; const camera = this.app.root.findComponent('camera'); if (!camera) return; // Use the helper to convert event coordinates into canvas/picker coordinates. const { x, y } = this._getPickerCoordinates(event); this._picker.prepare(camera, this.app.scene); const selection = this._picker.getSelection(x, y); // Get the currently hovered entity by walking up the hierarchy let newHoverEntity = null; if (selection.length > 0) { let currentNode = selection[0].node; while (currentNode !== null) { const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`); if (entityElement) { newHoverEntity = entityElement; break; } currentNode = currentNode.parent; } } // Handle enter/leave events if (this._hoveredEntity !== newHoverEntity) { if (this._hoveredEntity && this._hoveredEntity.hasListeners('pointerleave')) { this._hoveredEntity.dispatchEvent(new PointerEvent('pointerleave', event)); } if (newHoverEntity && newHoverEntity.hasListeners('pointerenter')) { newHoverEntity.dispatchEvent(new PointerEvent('pointerenter', event)); } } // Update hover state this._hoveredEntity = newHoverEntity; // Handle pointermove event if (newHoverEntity && newHoverEntity.hasListeners('pointermove')) { newHoverEntity.dispatchEvent(new PointerEvent('pointermove', event)); } } _onPointerDown(event) { if (!this._picker || !this.app) return; const camera = this.app.root.findComponent('camera'); if (!camera) return; // Convert the event's pointer coordinates const { x, y } = this._getPickerCoordinates(event); this._picker.prepare(camera, this.app.scene); const selection = this._picker.getSelection(x, y); if (selection.length > 0) { let currentNode = selection[0].node; while (currentNode !== null) { const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`); if (entityElement && entityElement.hasListeners('pointerdown')) { entityElement.dispatchEvent(new PointerEvent('pointerdown', event)); break; } currentNode = currentNode.parent; } } } _onPointerUp(event) { if (!this._picker || !this.app) return; const camera = this.app.root.findComponent('camera'); if (!camera) return; // Convert CSS coordinates to picker coordinates const { x, y } = this._getPickerCoordinates(event); this._picker.prepare(camera, this.app.scene); const selection = this._picker.getSelection(x, y); if (selection.length > 0) { const entityElement = this.querySelector(`pc-entity[name="${selection[0].node.name}"]`); if (entityElement && entityElement.hasListeners('pointerup')) { entityElement.dispatchEvent(new PointerEvent('pointerup', event)); } } } _onPointerListenerAdded(type) { if (!this._hasPointerListeners[type] && this._canvas) { this._hasPointerListeners[type] = true; // For enter/leave events, we need the move handler const handler = (type === 'pointerenter' || type === 'pointerleave') ? this._pointerHandlers.pointermove : this._pointerHandlers[type]; if (handler) { this._canvas.addEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler); } } } _onPointerListenerRemoved(type) { const hasListeners = Array.from(this.querySelectorAll('pc-entity')) .some(entity => entity.hasListeners(type)); if (!hasListeners && this._canvas) { this._hasPointerListeners[type] = false; const handler = (type === 'pointerenter' || type === 'pointerleave') ? this._pointerHandlers.pointermove : this._pointerHandlers[type]; if (handler) { this._canvas.removeEventListener(type === 'pointerenter' || type === 'pointerleave' ? 'pointermove' : type, handler); } } } /** * Sets the alpha flag. * @param value - The alpha flag. */ set alpha(value) { this._alpha = value; } /** * Gets the alpha flag. * @returns The alpha flag. */ get alpha() { return this._alpha; } /** * Sets the antialias flag. * @param value - The antialias flag. */ set antialias(value) { this._antialias = value; } /** * Gets the antialias flag. * @returns The antialias flag. */ get antialias() { return this._antialias; } /** * Sets the graphics backend. * @param value - The graphics backend ('webgpu', 'webgl2', or 'null'). */ set backend(value) { this._backend = value; } /** * Gets the graphics backend. * @returns The graphics backend. */ get backend() { return this._backend; } /** * Sets the depth flag. * @param value - The depth flag. */ set depth(value) { this._depth = value; } /** * Gets the depth flag. * @returns The depth flag. */ get depth() { return this._depth; } /** * Gets the hierarchy ready flag. * @returns The hierarchy ready flag. * @ignore */ get hierarchyReady() { return this._hierarchyReady; } /** * Sets the high resolution flag. When true, the application will render at the device's * physical resolution. When false, the application will render at CSS resolution. * @param value - The high resolution flag. */ set highResolution(value) { this._highResolution = value; if (this.app) { this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1; } } /** * Gets the high resolution flag. * @returns The high resolution flag. */ get highResolution() { return this._highResolution; } /** * Sets the stencil flag. * @param value - The stencil flag. */ set stencil(value) { this._stencil = value; } /** * Gets the stencil flag. * @returns The stencil flag. */ get stencil() { return this._stencil; } static get observedAttributes() { return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'alpha': this.alpha = newValue !== 'false'; break; case 'antialias': this.antialias = newValue !== 'false'; break; case 'backend': if (newValue === 'webgpu' || newValue === 'webgl2' || newValue === 'null') { this.backend = newValue; } break; case 'depth': this.depth = newValue !== 'false'; break; case 'high-resolution': this.highResolution = newValue !== 'false'; break; case 'stencil': this.stencil = newValue !== 'false'; break; } } } customElements.define('pc-app', AppElement); const CSS_COLORS = { aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', aquamarine: '#7fffd4', azure: '#f0ffff', beige: '#f5f5dc', bisque: '#ffe4c4', black: '#000000', blanchedalmond: '#ffebcd', blue: '#0000ff', blueviolet: '#8a2be2', brown: '#a52a2a', burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e', coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c', cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b', darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b', darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc', darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b', darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3', deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969', dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22', fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700', goldenrod: '#daa520', gray: '#808080', green: '#008000', greenyellow: '#adff2f', grey: '#808080', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c', indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa', lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6', lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3', lightgreen: '#90ee90', lightgrey: '#d3d3d3', lightpink: '#ffb6c1', lightsalmon: '#ffa07a', lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899', lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32', linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa', mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371', mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc', mediumvioletred: '#c71585', midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1', moccasin: '#ffe4b5', navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6', olive: '#808000', olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500', orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee', palevioletred: '#db7093', papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb', plum: '#dda0dd', powderblue: '#b0e0e6', purple: '#800080', rebeccapurple: '#663399', red: '#ff0000', rosybrown: '#bc8f8f', royalblue: '#4169e1', saddlebrown: '#8b4513', salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57', seashell: '#fff5ee', sienna: '#a0522d', silver: '#c0c0c0', skyblue: '#87ceeb', slateblue: '#6a5acd', slategray: '#708090', slategrey: '#708090', snow: '#fffafa', springgreen: '#00ff7f', steelblue: '#4682b4', tan: '#d2b48c', teal: '#008080', thistle: '#d8bfd8', tomato: '#ff6347', turquoise: '#40e0d0', violet: '#ee82ee', wheat: '#f5deb3', white: '#ffffff', whitesmoke: '#f5f5f5', yellow: '#ffff00', yellowgreen: '#9acd32' }; /** * Parse a color string into a Color object. String can be in the format of '#rgb', '#rgba', * '#rrggbb', '#rrggbbaa', or a string of 3 or 4 comma-delimited numbers. * * @param value - The color string to parse. * @returns The parsed Color object. */ const parseColor = (value) => { // Check if it's a CSS color name first const hexColor = CSS_COLORS[value.toLowerCase()]; if (hexColor) { return new Color().fromString(hexColor); } if (value.startsWith('#')) { return new Color().fromString(value); } const components = value.split(' ').map(Number); return new Color(components); }; /** * Parse an Euler angles string into a Quat object. String can be in the format of 'x,y,z'. * * @param value - The Euler angles string to parse. * @returns The parsed Quat object. */ const parseQuat = (value) => { const [x, y, z] = value.split(' ').map(Number); const q = new Quat(); q.setFromEulerAngles(x, y, z); return q; }; /** * Parse a Vec2 string into a Vec2 object. String can be in the format of 'x,y'. * * @param value - The Vec2 string to parse. * @returns The parsed Vec2 object. */ const parseVec2 = (value) => { const components = value.split(' ').map(Number); return new Vec2(components); }; /** * Parse a Vec3 string into a Vec3 object. String can be in the format of 'x,y,z'. * * @param value - The Vec3 string to parse. * @returns The parsed Vec3 object. */ const parseVec3 = (value) => { const components = value.split(' ').map(Number); return new Vec3(components); }; /** * Parse a Vec4 string into a Vec4 object. String can be in the format of 'x,y,z,w'. * * @param value - The Vec4 string to parse. * @returns The parsed Vec4 object. */ const parseVec4 = (value) => { const components = value.split(' ').map(Number); return new Vec4(components); }; /** * The EntityElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | ``} elements. * The EntityElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class EntityElement extends AsyncElement { constructor() { super(...arguments); /** * Whether the entity is enabled. */ this._enabled = true; /** * The name of the entity. */ this._name = 'Untitled'; /** * The position of the entity. */ this._position = new Vec3(); /** * The rotation of the entity. */ this._rotation = new Vec3(); /** * The scale of the entity. */ this._scale = new Vec3(1, 1, 1); /** * The tags of the entity. */ this._tags = []; /** * The pointer event listeners for the entity. */ this._listeners = {}; /** * The PlayCanvas entity instance. */ this.entity = null; } createEntity(app) { // Create a new entity this.entity = new Entity(this.getAttribute('name') || this._name, app); const enabled = this.getAttribute('enabled'); if (enabled) { this.entity.enabled = enabled !== 'false'; } const position = this.getAttribute('position'); if (position) { this.entity.setLocalPosition(parseVec3(position)); } const rotation = this.getAttribute('rotation'); if (rotation) { this.entity.setLocalEulerAngles(parseVec3(rotation)); } const scale = this.getAttribute('scale'); if (scale) { this.entity.setLocalScale(parseVec3(scale)); } const tags = this.getAttribute('tags'); if (tags) { this.entity.tags.add(tags.split(',').map(tag => tag.trim())); } // Handle pointer events const pointerEvents = [ 'onpointerenter', 'onpointerleave', 'onpointerdown', 'onpointerup', 'onpointermove' ]; pointerEvents.forEach((eventName) => { const handler = this.getAttribute(eventName); if (handler) { const eventType = eventName.substring(2); // remove 'on' prefix const eventHandler = (event) => { try { /* eslint-disable-next-line no-new-func */ new Function('event', handler).call(this, event); } catch (e) { console.error('Error in event handler:', e); } }; this.addEventListener(eventType, eventHandler); } }); } buildHierarchy(app) { if (!this.entity) return; const closestEntity = this.closestEntity; if (closestEntity === null || closestEntity === void 0 ? void 0 : closestEntity.entity) { closestEntity.entity.addChild(this.entity); } else { app.root.addChild(this.entity); } this._onReady(); } connectedCallback() { // Wait for app to be ready const closestApp = this.closestApp; if (!closestApp) return; // If app is already running, create entity immediately if (closestApp.hierarchyReady) { const app = closestApp.app; this.createEntity(app); this.buildHierarchy(app); // Handle any child entities that might exist const childEntities = this.querySelectorAll('pc-entity'); childEntities.forEach((child) => { child.createEntity(app); }); childEntities.forEach((child) => { child.buildHierarchy(app); }); } } disconnectedCallback() { if (this.entity) { // Notify all children that their entities are about to become invalid const children = this.querySelectorAll('pc-entity'); children.forEach((child) => { child.entity = null; }); // Destroy the entity this.entity.destroy(); this.entity = null; } } /** * Sets the enabled state of the entity. * @param value - Whether the entity is enabled. */ set enabled(value) { this._enabled = value; if (this.entity) { this.entity.enabled = value; } } /** * Gets the enabled state of the entity. * @returns Whether the entity is enabled. */ get enabled() { return this._enabled; } /** * Sets the name of the entity. * @param value - The name of the entity. */ set name(value) { this._name = value; if (this.entity) { this.entity.name = value; } } /** * Gets the name of the entity. * @returns The name of the entity. */ get name() { return this._name; } /** * Sets the position of the entity. * @param value - The position of the entity. */ set position(value) { this._position = value; if (this.entity) { this.entity.setLocalPosition(this._position); } } /** * Gets the position of the entity. * @returns The position of the entity. */ get position() { return this._position; } /** * Sets the rotation of the entity. * @param value - The rotation of the entity. */ set rotation(value) { this._rotation = value; if (this.entity) { this.entity.setLocalEulerAngles(this._rotation); } } /** * Gets the rotation of the entity. * @returns The rotation of the entity. */ get rotation() { return this._rotation; } /** * Sets the scale of the entity. * @param value - The scale of the entity. */ set scale(value) { this._scale = value; if (this.entity) { this.entity.setLocalScale(this._scale); } } /** * Gets the scale of the entity. * @returns The scale of the entity. */ get scale() { return this._scale; } /** * Sets the tags of the entity. * @param value - The tags of the entity. */ set tags(value) { this._tags = value; if (this.entity) { this.entity.tags.clear(); this.entity.tags.add(this._tags); } } /** * Gets the tags of the entity. * @returns The tags of the entity. */ get tags() { return this._tags; } static get observedAttributes() { return [ 'enabled', 'name', 'position', 'rotation', 'scale', 'tags', 'onpointerenter', 'onpointerleave', 'onpointerdown', 'onpointerup', 'onpointermove' ]; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'enabled': this.enabled = newValue !== 'false'; break; case 'name': this.name = newValue; break; case 'position': this.position = parseVec3(newValue); break; case 'rotation': this.rotation = parseVec3(newValue); break; case 'scale': this.scale = parseVec3(newValue); break; case 'tags': this.tags = newValue.split(',').map(tag => tag.trim()); break; case 'onpointerenter': case 'onpointerleave': case 'onpointerdown': case 'onpointerup': case 'onpointermove': if (newValue) { const eventName = name.substring(2); // Use Function.prototype.bind to avoid new Function const handler = (event) => { try { const handlerStr = this.getAttribute(eventName) || ''; /* eslint-disable-next-line no-new-func */ new Function('event', handlerStr).call(this, event); } catch (e) { console.error('Error in event handler:', e); } }; this.addEventListener(eventName, handler); } break; } } addEventListener(type, listener, options) { if (!this._listeners[type]) { this._listeners[type] = []; } this._listeners[type].push(listener); super.addEventListener(type, listener, options); if (type.startsWith('pointer')) { this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true })); } } removeEventListener(type, listener, options) { if (this._listeners[type]) { this._listeners[type] = this._listeners[type].filter(l => l !== listener); } super.removeEventListener(type, listener, options); if (type.startsWith('pointer')) { this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true })); } } hasListeners(type) { var _a; return Boolean((_a = this._listeners[type]) === null || _a === void 0 ? void 0 : _a.length); } } customElements.define('pc-entity', EntityElement); // This file is part of meshoptimizer library and is distributed under the terms of MIT License. // Copyright (C) 2016-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) var MeshoptDecoder = (function() { // Built with clang version 14.0.4 // Built from meshoptimizer 0.18 var wasm_base = "b9H79Tebbbe8Fv9Gbb9Gvuuuuueu9Giuuub9Geueu9Giuuueuikqbeeedddillviebeoweuec:q;iekr;leDo9TW9T9VV95dbH9F9F939H79T9F9J9H229F9Jt9VV7bb8A9TW79O9V9Wt9F9KW9J9V9KW9wWVtW949c919M9MWVbeY9TW79O9V9Wt9F9KW9J9V9KW69U9KW949c919M9MWVbdE9TW79O9V9Wt9F9KW9J9V9KW69U9KW949tWG91W9U9JWbiL9TW79O9V9Wt9F9KW9J9V9KWS9P2tWV9p9JtblK9TW79O9V9Wt9F9KW9J9V9KWS9P2tWV9r919HtbvL9TW79O9V9Wt9F9KW9J9V9KWS9P2tWVT949Wbol79IV9Rbrq:P8Yqdbk;3sezu8Jjjjjbcj;eb9Rgv8Kjjjjbc9:hodnadcefal0mbcuhoaiRbbc:Ge9hmbavaialfgrad9Radz1jjjbhwcj;abad9UhoaicefhldnadTmbaoc;WFbGgocjdaocjd6EhDcbhqinaqae9pmeaDaeaq9RaqaDfae6Egkcsfgocl4cifcd4hxdndndndnaoc9WGgmTmbcbhPcehsawcjdfhzalhHinaraH9Rax6midnaraHaxfgl9RcK6mbczhoinawcj;cbfaogifgoc9WfhOdndndndndnaHaic9WfgAco4fRbbaAci4coG4ciGPlbedibkaO9cb83ibaOcwf9cb83ibxikaOalRblalRbbgAco4gCaCciSgCE86bbaocGfalclfaCfgORbbaAcl4ciGgCaCciSgCE86bbaocVfaOaCfgORbbaAcd4ciGgCaCciSgCE86bbaoc7faOaCfgORbbaAciGgAaAciSgAE86bbaoctfaOaAfgARbbalRbegOco4gCaCciSgCE86bbaoc91faAaCfgARbbaOcl4ciGgCaCciSgCE86bbaoc4faAaCfgARbbaOcd4ciGgCaCciSgCE86bbaoc93faAaCfgARbbaOciGgOaOciSgOE86bbaoc94faAaOfgARbbalRbdgOco4gCaCciSgCE86bbaoc95faAaCfgARbbaOcl4ciGgCaCciSgCE86bbaoc96faAaCfgARbbaOcd4ciGgCaCciSgCE86bbaoc97faAaCfgARbbaOciGgOaOciSgOE86bbaoc98faAaOfgORbbalRbiglco4gAaAciSgAE86bbaoc99faOaAfgORbbalcl4ciGgAaAciSgAE86bbaoc9:faOaAfgORbbalcd4ciGgAaAciSgAE86bbaocufaOaAfgoRbbalciGglalciSglE86bbaoalfhlxdkaOalRbwalRbbgAcl4gCaCcsSgCE86bbaocGfalcwfaCfgORbbaAcsGgAaAcsSgAE86bbaocVfaOaAfgORbbalRbegAcl4gCaCcsSgCE86bbaoc7faOaCfgORbbaAcsGgAaAcsSgAE86bbaoctfaOaAfgORbbalRbdgAcl4gCaCcsSgCE86bbaoc91faOaCfgORbbaAcsGgAaAcsSgAE86bbaoc4faOaAfgORbbalRbigAcl4gCaCcsSgCE86bbaoc93faOaCfgORbbaAcsGgAaAcsSgAE86bbaoc94faOaAfgORbbalRblgAcl4gCaCcsSgCE86bbaoc95faOaCfgORbbaAcsGgAaAcsSgAE86bbaoc96faOaAfgORbbalRbvgAcl4gCaCcsSgCE86bbaoc97faOaCfgORbbaAcsGgAaAcsSgAE86bbaoc98faOaAfgORbbalRbogAcl4gCaCcsSgCE86bbaoc99faOaCfgORbbaAcsGgAaAcsSgAE86bbaoc9:faOaAfgORbbalRbrglcl4gAaAcsSgAE86bbaocufaOaAfgoRbbalcsGglalcsSglE86bbaoalfhlxekaOal8Pbb83bbaOcwfalcwf8Pbb83bbalczfhlkdnaiam9pmbaiczfhoaral9RcL0mekkaiam6mialTmidnakTmbawaPfRbbhOcbhoazhiinaiawcj;cbfaofRbbgAce4cbaAceG9R7aOfgO86bbaiadfhiaocefgoak9hmbkkazcefhzaPcefgPad6hsalhHaPad9hmexvkkcbhlasceGmdxikalaxad2fhCdnakTmbcbhHcehsawcjdfhminaral9Rax6mialTmdalaxfhlawaHfRbbhOcbhoamhiinaiawcj;cbfaofRbbgAce4cbaAceG9R7aOfgO86bbaiadfhiaocefgoak9hmbkamcefhmaHcefgHad6hsaHad9hmbkaChlxikcbhocehsinaral9Rax6mdalTmealaxfhlaocefgoad6hsadao9hmbkaChlxdkcbhlasceGTmekc9:hoxikabaqad2fawcjdfakad2z1jjjb8Aawawcjdfakcufad2fadz1jjjb8Aakaqfhqalmbkc9:hoxekcbc99aral9Radcaadca0ESEhokavcj;ebf8Kjjjjbaok;yzeHu8Jjjjjbc;ae9Rgv8Kjjjjbc9:hodnaeci9UgrcHfal0mbcuhoaiRbbgwc;WeGc;Ge9hmbawcsGgDce0mbavc;abfcFecjez:jjjjb8AavcUf9cu83ibavc8Wf9cu83ibavcyf9cu83ibavcaf9cu83ibavcKf9cu83ibavczf9cu83ibav9cu83iwav9cu83ibaialfc9WfhqaicefgwarfhodnaeTmbcmcsaDceSEhkcbhxcbhmcbhDcbhicbhlindnaoaq9nmbc9:hoxikdndnawRbbgrc;Ve0mbavc;abfalarcl4cu7fcsGcitfgPydlhsaPydbhzdnarcsGgPak9pmbavaiarcu7fcsGcdtfydbaxaPEhraPThPdndnadcd9hmbabaDcetfgHaz87ebaHcdfas87ebaHclfar87ebxekabaDcdtfgHazBdbaHclfasBdbaHcwfarBdbkaxaPfhxavc;abfalcitfgHarBdbaHasBdlavaicdtfarBdbavc;abfalcefcsGglcitfgHazBdbaHarBdlaiaPfhialcefhlxdkdndnaPcsSmbamaPfaPc987fcefhmxekaocefhrao8SbbgPcFeGhHdndnaPcu9mmbarhoxekaocvfhoaHcFbGhHcrhPdninar8SbbgOcFbGaPtaHVhHaOcu9kmearcefhraPcrfgPc8J9hmbxdkkarcefhokaHce4cbaHceG9R7amfhmkdndnadcd9hmbabaDcetfgraz87ebarcdfas87ebarclfam87ebxekabaDcdtfgrazBdbarclfasBdbarcwfamBdbkavc;abfalcitfgramBdbarasBdlavaicdtfamBdbavc;abfalcefcsGglcitfgrazBdbaramBdlaicefhialcefhlxekdnarcpe0mbaxcefgOavaiaqarcsGfRbbgPcl49RcsGcdtfydbaPcz6gHEhravaiaP9RcsGcdtfydbaOaHfgsaPcsGgOEhPaOThOdndnadcd9hmbabaDcetfgzax87ebazcdfar87ebazclfaP87ebxekabaDcdtfgzaxBdbazclfarBdbazcwfaPBdbkavaicdtfaxBdbavc;abfalcitfgzarBdbazaxBdlavaicefgicsGcdtfarBdbavc;abfalcefcsGcitfgzaPBdbazarBdlavaiaHfcsGgicdtfaPBdbavc;abfalcdfcsGglcitfgraxBdbaraPBdlalcefhlaiaOfhiasaOfhxxekaxcbaoRbbgzEgAarc;:eSgrfhsazcsGhCazcl4hXdndnazcs0mbascefhOxekashOavaiaX9RcsGcdtfydbhskdndnaCmbaOcefhxxekaOhxavaiaz9RcsGcdtfydbhOkdndnarTmbaocefhrxekaocdfhrao8SbegHcFeGhPdnaHcu9kmbaocofhAaPcFbGhPcrhodninar8SbbgHcFbGaotaPVhPaHcu9kmearcefhraocrfgoc8J9hmbkaAhrxekarcefhrkaPce4cbaPceG9R7amfgmhAkdndnaXcsSmbarhPxekarcefhPar8SbbgocFeGhHdnaocu9kmbarcvfhsaHcFbGhHcrhodninaP8SbbgrcFbGaotaHVhHarcu9kmeaPcefhPaocrfgoc8J9hmbkashPxekaPcefhPkaHce4cbaHceG9R7amfgmhskdndnaCcsSmbaPhoxekaPcefhoaP8SbbgrcFeGhHdnarcu9kmbaPcvfhOaHcFbGhHcrhrdninao8SbbgPcFbGartaHVhHaPcu9kmeaocefhoarcrfgrc8J9hmbkaOhoxekaocefhokaHce4cbaHceG9R7amfgmhOkdndnadcd9hmbabaDcetfgraA87ebarcdfas87ebarclfaO87ebxekabaDcdtfgraABdbarclfasBdbarcwfaOBdbkavc;abfalcitfgrasBdbaraABdlavaicdtfaABdbavc;abfalcefcsGcitfgraOBdbarasBdlavaicefgicsGcdtfasBdbavc;abfalcdfcsGcitfgraABdbaraOBdlavaiazcz6aXcsSVfgicsGcdtfaOBdbaiaCTaCcsSVfhialcifhlkawcefhwalcsGhlaicsGhiaDcifgDae6mbkkcbc99aoaqSEhokavc;aef8Kjjjjbaok:llevu8Jjjjjbcz9Rhvc9:hodnaecvfal0mbcuhoaiRbbc;:eGc;qe9hmbav9cb83iwaicefhraialfc98fhwdnaeTmbdnadcdSmbcbhDindnaraw6mbc9:skarcefhoar8SbbglcFeGhidndnalcu9mmbaohrxekarcvfhraicFbGhicrhldninao8SbbgdcFbGaltaiVhiadcu9kmeaocefhoalcrfglc8J9hmbxdkkaocefhrkabaDcdtfaicd4cbaice4ceG9R7avcwfaiceGcdtVgoydbfglBdbaoalBdbaDcefgDae9hmbxdkkcbhDindnaraw6mbc9:skarcefhoar8SbbglcFeGhidndnalcu9mmbaohrxekarcvfhraicFbGhicrhldninao8SbbgdcFbGaltaiVhiadcu9kmeaocefhoalcrfglc8J9hmbxdkkaocefhrkabaDcetfaicd4cbaice4ceG9R7avcwfaiceGcdtVgoydbfgl87ebaoalBdbaDcefgDae9hmbkkcbc99arawSEhokaok:Lvoeue99dud99eud99dndnadcl9hmbaeTmeindndnabcdfgd8Sbb:Yab8Sbbgi:Ygl:l:tabcefgv8Sbbgo:Ygr:l:tgwJbb;:9cawawNJbbbbawawJbbbb9GgDEgq:mgkaqaicb9iEalMgwawNakaqaocb9iEarMgqaqNMM:r:vglNJbbbZJbbb:;aDEMgr:lJbbb9p9DTmbar:Ohixekcjjjj94hikadai86bbdndnaqalNJbbbZJbbb:;aqJbbbb9GEMgq:lJbbb9p9DTmbaq:Ohdxekcjjjj94hdkavad86bbdndnawalNJbbbZJbbb:;awJbbbb9GEMgw:lJbbb9p9DTmbaw:Ohdxekcjjjj94hdkabad86bbabclfhbaecufgembxdkkaeTmbindndnabclfgd8Ueb:Yab8Uebgi:Ygl:l:tabcdfgv8Uebgo:Ygr:l:tgwJb;:FSawawNJbbbbawawJbbbb9GgDEgq:mgkaqaicb9iEalMgwawNakaqaocb9iEarMgqaqNMM:r:vglNJbbbZJbbb:;aDEMgr:lJbbb9p9DTmbar:Ohixekcjjjj94hikadai87ebdndnaqalNJbbbZJbbb:;aqJbbbb9GEMgq:lJbbb9p9DTmbaq:Ohdxekcjjjj94hdkavad87ebdndnawalNJbbbZJbbb:;awJbbbb9GEMgw:lJbbb9p9DTmbaw:Ohdxekcjjjj94hdkabad87ebabcwfhbaecufgembkkk;siliui99iue99dnaeTmbcbhiabhlindndnJ;Zl81Zalcof8UebgvciV:Y:vgoal8Ueb:YNgrJb;:FSNJbbbZJbbb:;arJbbbb9GEMgw:lJbbb9p9DTmbaw:OhDxekcjjjj94hDkalclf8Uebhqalcdf8UebhkabavcefciGaiVcetfaD87ebdndnaoak:YNgwJb;:FSNJbbbZJbbb:;awJbbbb9GEMgx:lJbbb9p9DTmbax:Ohkxekcjjjj94hkkabavcdfciGaiVcetfak87ebdndnaoaq:YNgoJb;:FSNJbbbZJbbb:;aoJbbbb9GEMgx:lJbbb9p9DTmbax:Ohqxekcjjjj94hqkabavcufciGaiVcetfaq87ebdndnJbbjZararN:tawawN:taoaoN:tgrJbbbbarJbbbb9GE:rJb;:FSNJbbbZMgr:lJbbb9p9DTmbar:Ohqxekcjjjj94hqkabavciGaiVcetfaq87ebalcwfhlaiclfhiaecufgembkkk9mbdnadcd4ae2geTmbinababydbgdcwtcw91:Yadce91cjjj;8ifcjjj98G::NUdbabclfhbaecufgembkkk9teiucbcbydj1jjbgeabcifc98GfgbBdj1jjbdndnabZbcztgd9nmbcuhiabad9RcFFifcz4nbcuSmekaehikaik;LeeeudndnaeabVciGTmbabhixekdndnadcz9pmbabhixekabhiinaiaeydbBdbaiclfaeclfydbBdbaicwfaecwfydbBdbaicxfaecxfydbBdbaiczfhiaeczfheadc9Wfgdcs0mbkkadcl6mbinaiaeydbBdbaeclfheaiclfhiadc98fgdci0mbkkdnadTmbinaiaeRbb86bbaicefhiaecefheadcufgdmbkkabk;aeedudndnabciGTmbabhixekaecFeGc:b:c:ew2hldndnadcz9pmbabhixekabhiinaialBdbaicxfalBdbaicwfalBdbaiclfalBdbaiczfhiadc9Wfgdcs0mbkkadcl6mbinaialBdbaiclfhiadc98fgdci0mbkkdnadTmbinaiae86bbaicefhiadcufgdmbkkabkkkebcjwklz9Kbb"; var wasm_simd = "b9H79TebbbeKl9Gbb9Gvuuuuueu9Giuuub9Geueuikqbbebeedddilve9Weeeviebeoweuec:q;Aekr;leDo9TW9T9VV95dbH9F9F939H79T9F9J9H229F9Jt9VV7bb8A9TW79O9V9Wt9F9KW9J9V9KW9wWVtW949c919M9MWVbdY9TW79O9V9Wt9F9KW9J9V9KW69U9KW949c919M9MWVblE9TW79O9V9Wt9F9KW9J9V9KW69U9KW949tWG91W9U9JWbvL9TW79O9V9Wt9F9KW9J9V9KWS9P2tWV9p9JtboK9TW79O9V9Wt9F9KW9J9V9KWS9P2tWV9r919HtbrL9TW79O9V9Wt9F9KW9J9V9KWS9P2tWVT949Wbwl79IV9RbDq;t9tqlbzik9:evu8Jjjjjbcz9Rhbcbheincbhdcbhiinabcwfadfaicjuaead4ceGglE86bbaialfhiadcefgdcw9hmbkaec:q:yjjbfai86bbaecitc:q1jjbfab8Piw83ibaecefgecjd9hmbkk;h8JlHud97euo978Jjjjjbcj;kb9Rgv8Kjjjjbc9:hodnadcefal0mbcuhoaiRbbc:Ge9hmbavaialfgrad9Rad;8qbbcj;abad9UhoaicefhldnadTmbaoc;WFbGgocjdaocjd6EhwcbhDinaDae9pmeawaeaD9RaDawfae6Egqcsfgoc9WGgkci2hxakcethmaocl4cifcd4hPabaDad2fhscbhzdnincehHalhOcbhAdninaraO9RaP6miavcj;cbfaAak2fhCaOaPfhlcbhidnakc;ab6mbaral9Rc;Gb6mbcbhoinaCaofhidndndndndnaOaoco4fRbbgXciGPlbedibkaipxbbbbbbbbbbbbbbbbpklbxikaialpbblalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLgQcdp:meaQpmbzeHdOiAlCvXoQrLpxiiiiiiiiiiiiiiiip9ogLpxiiiiiiiiiiiiiiiip8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklbalclfaYpQbfaKc:q:yjjbfRbbfhlxdkaialpbbwalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLpxssssssssssssssssp9ogLpxssssssssssssssssp8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklbalcwfaYpQbfaKc:q:yjjbfRbbfhlxekaialpbbbpklbalczfhlkdndndndndnaXcd4ciGPlbedibkaipxbbbbbbbbbbbbbbbbpklzxikaialpbblalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLgQcdp:meaQpmbzeHdOiAlCvXoQrLpxiiiiiiiiiiiiiiiip9ogLpxiiiiiiiiiiiiiiiip8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklzalclfaYpQbfaKc:q:yjjbfRbbfhlxdkaialpbbwalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLpxssssssssssssssssp9ogLpxssssssssssssssssp8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklzalcwfaYpQbfaKc:q:yjjbfRbbfhlxekaialpbbbpklzalczfhlkdndndndndnaXcl4ciGPlbedibkaipxbbbbbbbbbbbbbbbbpklaxikaialpbblalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLgQcdp:meaQpmbzeHdOiAlCvXoQrLpxiiiiiiiiiiiiiiiip9ogLpxiiiiiiiiiiiiiiiip8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklaalclfaYpQbfaKc:q:yjjbfRbbfhlxdkaialpbbwalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLpxssssssssssssssssp9ogLpxssssssssssssssssp8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklaalcwfaYpQbfaKc:q:yjjbfRbbfhlxekaialpbbbpklaalczfhlkdndndndndnaXco4Plbedibkaipxbbbbbbbbbbbbbbbbpkl8WxikaialpbblalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLgQcdp:meaQpmbzeHdOiAlCvXoQrLpxiiiiiiiiiiiiiiiip9ogLpxiiiiiiiiiiiiiiiip8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgXcitc:q1jjbfpbibaXc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgXcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spkl8WalclfaYpQbfaXc:q:yjjbfRbbfhlxdkaialpbbwalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLpxssssssssssssssssp9ogLpxssssssssssssssssp8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgXcitc:q1jjbfpbibaXc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgXcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spkl8WalcwfaYpQbfaXc:q:yjjbfRbbfhlxekaialpbbbpkl8Walczfhlkaoc;abfhiaocjefak0meaihoaral9Rc;Fb0mbkkdndnaiak9pmbaici4hoinaral9RcK6mdaCaifhXdndndndndnaOaico4fRbbaocoG4ciGPlbedibkaXpxbbbbbbbbbbbbbbbbpklbxikaXalpbblalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLgQcdp:meaQpmbzeHdOiAlCvXoQrLpxiiiiiiiiiiiiiiiip9ogLpxiiiiiiiiiiiiiiiip8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklbalclfaYpQbfaKc:q:yjjbfRbbfhlxdkaXalpbbwalpbbbgQclp:meaQpmbzeHdOiAlCvXoQrLpxssssssssssssssssp9ogLpxssssssssssssssssp8JgQp5b9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibaKc:q:yjjbfpbbbgYaYpmbbbbbbbbbbbbbbbbaQp5e9cjF;8;4;W;G;ab9:9cU1:NgKcitc:q1jjbfpbibp9UpmbedilvorzHOACXQLpPaLaQp9spklbalcwfaYpQbfaKc:q:yjjbfRbbfhlxekaXalpbbbpklbalczfhlkaocdfhoaiczfgiak6mbkkalTmbaAci6hHalhOaAcefgohAaoclSmdxekkcbhlaHceGmdkdnakTmbavcjdfazfhiavazfpbdbhYcbhXinaiavcj;cbfaXfgopblbgLcep9TaLpxeeeeeeeeeeeeeeeegQp9op9Hp9rgLaoakfpblbg8Acep9Ta8AaQp9op9Hp9rg8ApmbzeHdOiAlCvXoQrLgEaoamfpblbg3cep9Ta3aQp9op9Hp9rg3aoaxfpblbg5cep9Ta5aQp9op9Hp9rg5pmbzeHdOiAlCvXoQrLg8EpmbezHdiOAlvCXorQLgQaQpmbedibedibedibediaYp9UgYp9AdbbaiadfgoaYaQaQpmlvorlvorlvorlvorp9UgYp9AdbbaoadfgoaYaQaQpmwDqkwDqkwDqkwDqkp9UgYp9AdbbaoadfgoaYaQaQpmxmPsxmPsxmPsxmPsp9UgYp9AdbbaoadfgoaYaEa8EpmwDKYqk8AExm35Ps8E8FgQaQpmbedibedibedibedip9UgYp9AdbbaoadfgoaYaQaQpmlvorlvorlvorlvorp9UgYp9AdbbaoadfgoaYaQaQpmwDqkwDqkwDqkwDqkp9UgYp9AdbbaoadfgoaYaQaQpmxmPsxmPsxmPsxmPsp9UgYp9AdbbaoadfgoaYaLa8ApmwKDYq8AkEx3m5P8Es8FgLa3a5pmwKDYq8AkEx3m5P8Es8Fg8ApmbezHdiOAlvCXorQLgQaQpmbedibedibedibedip9UgYp9AdbbaoadfgoaYaQaQpmlvorlvorlvorlvorp9UgYp9AdbbaoadfgoaYaQaQpmwDqkwDqkwDqkwDqkp9UgYp9AdbbaoadfgoaYaQaQpmxmPsxmPsxmPsxmPsp9UgYp9AdbbaoadfgoaYaLa8ApmwDKYqk8AExm35Ps8E8FgQaQpmbedibedibedibedip9UgYp9AdbbaoadfgoaYaQaQpmlvorlvorlvorlvorp9UgYp9AdbbaoadfgoaYaQaQpmwDqkwDqkwDqkwDqkp9UgYp9AdbbaoadfgoaYaQaQpmxmPsxmPsxmPsxmPsp9UgYp9AdbbaoadfhiaXczfgXak6mbkkazclfgzad6mbkasavcjdfaqad2;8qbbavavcjdfaqcufad2fad;8qbbaqaDfhDc9:hoalmexikkc9:hoxekcbc99aral9Radcaadca0ESEhokavcj;kbf8Kjjjjbaokwbz:bjjjbk;uzeHu8Jjjjjbc;ae9Rgv8Kjjjjbc9:hodnaeci9UgrcHfal0mbcuhoaiRbbgwc;WeGc;Ge9hmbawcsGgDce0mbavc;abfcFecje;8kbavcUf9cu83ibavc8Wf9cu83ibavcyf9cu83ibavcaf9cu83ibavcKf9cu83ibavczf9cu83ibav9cu83iwav9cu83ibaialfc9WfhqaicefgwarfhodnaeTmbcmcsaDceSEhkcbhxcbhmcbhDcbhicbhlindnaoaq9nmbc9:hoxikdndnawRbbgrc;Ve0mbavc;abfalarcl4cu7fcsGcitfgPydlhsaPydbhzdnarcsGgPak9pmbavaiarcu7fcsGcdtfydbaxaPEhraPThPdndnadcd9hmbabaDcetfgHaz87ebaHcdfas87ebaHclfar87ebxekabaDcdtfgHazBdbaHclfasBdbaHcwfarBdbkaxaPfhxavc;abfalcitfgHarBdbaHasBdlavaicdtfarBdbavc;abfalcefcsGglcitfgHazBdbaHarBdlaiaPfhialcefhlxdkdndnaPcsSmbamaPfaPc987fcefhmxekaocefhrao8SbbgPcFeGhHdndnaPcu9mmbarhoxekaocvfhoaHcFbGhHcrhPdninar8SbbgOcFbGaPtaHVhHaOcu9kmearcefhraPcrfgPc8J9hmbxdkkarcefhokaHce4cbaHceG9R7amfhmkdndnadcd9hmbabaDcetfgraz87ebarcdfas87ebarclfam87ebxekabaDcdtfgrazBdbarclfasBdbarcwfamBdbkavc;abfalcitfgramBdbarasBdlavaicdtfamBdbavc;abfalcefcsGglcitfgrazBdbaramBdlaicefhialcefhlxekdnarcpe0mbaxcefgOavaiaqarcsGfRbbgPcl49RcsGcdtfydbaPcz6gHEhravaiaP9RcsGcdtfydbaOaHfgsaPcsGgOEhPaOThOdndnadcd9hmbabaDcetfgzax87ebazcdfar87ebazclfaP87ebxekabaDcdtfgzaxBdbazclfarBdbazcwfaPBdbkavaicdtfaxBdbavc;abfalcitfgzarBdbazaxBdlavaicefgicsGcdtfarBdbavc;abfalcefcsGcitfgzaPBdbazarBdlavaiaHfcsGgicdtfaPBdbavc;abfalcdfcsGglcitfgraxBdbaraPBdlalcefhlaiaOfhiasaOfhxxekaxcbaoRbbgzEgAarc;:eSgrfhsazcsGhCazcl4hXdndnazcs0mbascefhOxekashOavaiaX9RcsGcdtfydbhskdndnaCmbaOcefhxxekaOhxavaiaz9RcsGcdtfydbhOkdndnarTmbaocefhrxekaocdfhrao8SbegHcFeGhPdnaHcu9kmbaocofhAaPcFbGhPcrhodninar8SbbgHcFbGaotaPVhPaHcu9kmearcefhraocrfgoc8J9hmbkaAhrxekarcefhrkaPce4cbaPceG9R7amfgmhAkdndnaXcsSmbarhPxekarcefhPar8SbbgocFeGhHdnaocu9kmbarcvfhsaHcFbGhHcrhodninaP8SbbgrcFbGaotaHVhHarcu9kmeaPcefhPaocrfgoc8J9hmbkashPxekaPcefhPkaHce4cbaHceG9R7amfgmhskdndnaCcsSmbaPhoxekaPcefhoaP8SbbgrcFeGhHdnarcu9kmbaPcvfhOaHcFbGhHcrhrdninao8SbbgPcFbGartaHVhHaPcu9kmeaocefhoarcrfgrc8J9hmbkaOhoxekaocefhokaHce4cbaHceG9R7amfgmhOkdndnadcd9hmbabaDcetfgraA87ebarcdfas87ebarclfaO87ebxekabaDcdtfgraABdbarclfasBdbarcwfaOBdbkavc;abfalcitfgrasBdbaraABdlavaicdtfaABdbavc;abfalcefcsGcitfgraOBdbarasBdlavaicefgicsGcdtfasBdbavc;abfalcdfcsGcitfgraABdbaraOBdlavaiazcz6aXcsSVfgicsGcdtfaOBdbaiaCTaCcsSVfhialcifhlkawcefhwalcsGhlaicsGhiaDcifgDae6mbkkcbc99aoaqSEhokavc;aef8Kjjjjbaok:llevu8Jjjjjbcz9Rhvc9:hodnaecvfal0mbcuhoaiRbbc;:eGc;qe9hmbav9cb83iwaicefhraialfc98fhwdnaeTmbdnadcdSmbcbhDindnaraw6mbc9:skarcefhoar8SbbglcFeGhidndnalcu9mmbaohrxekarcvfhraicFbGhicrhldninao8SbbgdcFbGaltaiVhiadcu9kmeaocefhoalcrfglc8J9hmbxdkkaocefhrkabaDcdtfaicd4cbaice4ceG9R7avcwfaiceGcdtVgoydbfglBdbaoalBdbaDcefgDae9hmbxdkkcbhDindnaraw6mbc9:skarcefhoar8SbbglcFeGhidndnalcu9mmbaohrxekarcvfhraicFbGhicrhldninao8SbbgdcFbGaltaiVhiadcu9kmeaocefhoalcrfglc8J9hmbxdkkaocefhrkabaDcetfaicd4cbaice4ceG9R7avcwfaiceGcdtVgoydbfgl87ebaoalBdbaDcefgDae9hmbkkcbc99arawSEhokaok:EPliuo97eue978Jjjjjbca9Rhidndnadcl9hmbdnaec98GglTmbcbhvabhdinadadpbbbgocKp:RecKp:Sep;6egraocwp:RecKp:Sep;6earp;Geaoczp:RecKp:Sep;6egwp;Gep;Kep;LegDpxbbbbbbbbbbbbbbbbp:2egqarpxbbbjbbbjbbbjbbbjgkp9op9rp;Kegrpxbb;:9cbb;:9cbb;:9cbb;:9cararp;MeaDaDp;Meawaqawakp9op9rp;Kegrarp;Mep;Kep;Kep;Jep;Negwp;Mepxbbn0bbn0bbn0bbn0gqp;KepxFbbbFbbbFbbbFbbbp9oaopxbbbFbbbFbbbFbbbFp9op9qarawp;Meaqp;Kecwp:RepxbFbbbFbbbFbbbFbbp9op9qaDawp;Meaqp;Keczp:RepxbbFbbbFbbbFbbbFbp9op9qpkbbadczfhdavclfgval6mbkkalae9pmeaiaeciGgvcdtgdVcbczad9R;8kbaiabalcdtfglad;8qbbdnavTmbaiaipblbgocKp:RecKp:Sep;6egraocwp:RecKp:Sep;6earp;Geaoczp:RecKp:Sep;6egwp;Gep;Kep;LegDpxbbbbbbbbbbbbbbbbp:2egqarpxbbbjbbbjbbbjbbbjgkp9op9rp;Kegrpxbb;:9cbb;:9cbb;:9cbb;:9cararp;MeaDaDp;Meawaqawakp9op9rp;Kegrarp;Mep;Kep;Kep;Jep;Negwp;Mepxbbn0bbn0bbn0bbn0gqp;KepxFbbbFbbbFbbbFbbbp9oaopxbbbFbbbFbbbFbbbFp9op9qarawp;Meaqp;Kecwp:RepxbFbbbFbbbFbbbFbbp9op9qaDawp;Meaqp;Keczp:RepxbbFbbbFbbbFbbbFbp9op9qpklbkalaiad;8qbbskdnaec98GgxTmbcbhvabhdinadczfglalpbbbgopxbbbbbbFFbbbbbbFFgkp9oadpbbbgDaopmlvorxmPsCXQL358E8FpxFubbFubbFubbFubbp9op;6eaDaopmbediwDqkzHOAKY8AEgoczp:Sep;6egrp;Geaoczp:Reczp:Sep;6egwp;Gep;Kep;Legopxb;:FSb;:FSb;:FSb;:FSawaopxbbbbbbbbbbbbbbbbp:2egqawpxbbbjbbbjbbbjbbbjgmp9op9rp;Kegwawp;Meaoaop;Mearaqaramp9op9rp;Kegoaop;Mep;Kep;Kep;Jep;Negrp;Mepxbbn0bbn0bbn0bbn0gqp;Keczp:Reawarp;Meaqp;KepxFFbbFFbbFFbbFFbbp9op9qgwaoarp;Meaqp;KepxFFbbFFbbFFbbFFbbp9ogopmwDKYqk8AExm35Ps8E8Fp9qpkbbadaDakp9oawaopmbezHdiOAlvCXorQLp9qpkbbadcafhdavclfgvax6mbkkaxae9pmbaiaeciGgvcitgdfcbcaad9R;8kbaiabaxcitfglad;8qbbdnavTmbaiaipblzgopxbbbbbbFFbbbbbbFFgkp9oaipblbgDaopmlvorxmPsCXQL358E8FpxFubbFubbFubbFubbp9op;6eaDaopmbediwDqkzHOAKY8AEgoczp:Sep;6egrp;Geaoczp:Reczp:Sep;6egwp;Gep;Kep;Legopxb;:FSb;:FSb;:FSb;:FSawaopxbbbbbbbbbbbbbbbbp:2egqawpxbbbjbbbjbbbjbbbjgmp9op9rp;Kegwawp;Meaoaop;Mearaqaramp9op9rp;Kegoaop;Mep;Kep;Kep;Jep;Negrp;Mepxbbn0bbn0bbn0bbn0gqp;Keczp:Reawarp;Meaqp;KepxFFbbFFbbFFbbFFbbp9op9qgwaoarp;Meaqp;KepxFFbbFFbbFFbbFFbbp9ogopmwDKYqk8AExm35Ps8E8Fp9qpklzaiaDakp9oawaopmbezHdiOAlvCXorQLp9qpklbkalaiad;8qbbkk;4wllue97euv978Jjjjjbc8W9Rhidnaec98GglTmbcbhvabhoinaiaopbbbgraoczfgwpbbbgDpmlvorxmPsCXQL358E8Fgqczp:Segkclp:RepklbaopxbbjZbbjZbbjZbbjZpx;Zl81Z;Zl81Z;Zl81Z;Zl81Zakpxibbbibbbibbbibbbp9qp;6ep;NegkaraDpmbediwDqkzHOAKY8AEgrczp:Reczp:Sep;6ep;MegDaDp;Meakarczp:Sep;6ep;Megxaxp;Meakaqczp:Reczp:Sep;6ep;Megqaqp;Mep;Kep;Kep;Lepxbbbbbbbbbbbbbbbbp:4ep;Jepxb;:FSb;:FSb;:FSb;:FSgkp;Mepxbbn0bbn0bbn0bbn0grp;KepxFFbbFFbbFFbbFFbbgmp9oaxakp;Mearp;Keczp:Rep9qgxaqakp;Mearp;Keczp:ReaDakp;Mearp;Keamp9op9qgkpmbezHdiOAlvCXorQLgrp5baipblbpEb:T:j83ibaocwfarp5eaipblbpEe:T:j83ibawaxakpmwDKYqk8AExm35Ps8E8Fgkp5baipblbpEd:T:j83ibaocKfakp5eaipblbpEi:T:j83ibaocafhoavclfgval6mbkkdnalae9pmbaiaeciGgvcitgofcbcaao9R;8kbaiabalcitfgwao;8qbbdnavTmbaiaipblbgraipblzgDpmlvorxmPsCXQL358E8Fgqczp:Segkclp:RepklaaipxbbjZbbjZbbjZbbjZpx;Zl81Z;Zl81Z;Zl81Z;Zl81Zakpxibbbibbbibbbibbbp9qp;6ep;NegkaraDpmbediwDqkzHOAKY8AEgrczp:Reczp:Sep;6ep;MegDaDp;Meakarczp:Sep;6ep;Megxaxp;Meakaqczp:Reczp:Sep;6ep;Megqaqp;Mep;Kep;Kep;Lepxbbbbbbbbbbbbbbbbp:4ep;Jepxb;:FSb;:FSb;:FSb;:FSgkp;Mepxbbn0bbn0bbn0bbn0grp;KepxFFbbFFbbFFbbFFbbgmp9oaxakp;Mearp;Keczp:Rep9qgxaqakp;Mearp;Keczp:ReaDakp;Mearp;Keamp9op9qgkpmbezHdiOAlvCXorQLgrp5baipblapEb:T:j83ibaiarp5eaipblapEe:T:j83iwaiaxakpmwDKYqk8AExm35Ps8E8Fgkp5baipblapEd:T:j83izaiakp5eaipblapEi:T:j83iKkawaiao;8qbbkk:Pddiue978Jjjjjbc;ab9Rhidnadcd4ae2glc98GgvTmbcbhdabheinaeaepbbbgocwp:Recwp:Sep;6eaocep:SepxbbjZbbjZbbjZbbjZp:UepxbbjFbbjFbbjFbbjFp9op;Mepkbbaeczfheadclfgdav6mbkkdnaval9pmbaialciGgdcdtgeVcbc;abae9R;8kbaiabavcdtfgvae;8qbbdnadTmbaiaipblbgocwp:Recwp:Sep;6eaocep:SepxbbjZbbjZbbjZbbjZp:UepxbbjFbbjFbbjFbbjFp9op;Mepklbkavaiae;8qbbkk9teiucbcbydj1jjbgeabcifc98GfgbBdj1jjbdndnabZbcztgd9nmbcuhiabad9RcFFifcz4nbcuSmekaehikaikkkebcjwklz9Tbb"; var detector = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,3,2,0,0,5,3,1,0,1,12,1,0,10,22,2,12,0,65,0,65,0,65,0,252,10,0,0,11,7,0,65,0,253,15,26,11]); var wasmpack = new Uint8Array([32,0,65,2,1,106,34,33,3,128,11,4,13,64,6,253,10,7,15,116,127,5,8,12,40,16,19,54,20,9,27,255,113,17,42,67,24,23,146,148,18,14,22,45,70,69,56,114,101,21,25,63,75,136,108,28,118,29,73,115]); if (typeof WebAssembly !== 'object') { return { supported: false, }; } var wasm = WebAssembly.validate(detector) ? wasm_simd : wasm_base; var instance; var ready = WebAssembly.instantiate(unpack(wasm), {}) .then(function(result) { instance = result.instance; instance.exports.__wasm_call_ctors(); }); function unpack(data) { var result = new Uint8Array(data.length); for (var i = 0; i < data.length; ++i) { var ch = data.charCodeAt(i); result[i] = ch > 96 ? ch - 97 : ch > 64 ? ch - 39 : ch + 4; } var write = 0; for (var i = 0; i < data.length; ++i) { result[write++] = (result[i] < 60) ? wasmpack[result[i]] : (result[i] - 60) * 64 + result[++i]; } return result.buffer.slice(0, write); } function decode(fun, target, count, size, source, filter) { var sbrk = instance.exports.sbrk; var count4 = (count + 3) & -4; var tp = sbrk(count4 * size); var sp = sbrk(source.length); var heap = new Uint8Array(instance.exports.memory.buffer); heap.set(source, sp); var res = fun(tp, count, size, sp, source.length); if (res == 0 && filter) { filter(tp, count4, size); } target.set(heap.subarray(tp, tp + count * size)); sbrk(tp - sbrk(0)); if (res != 0) { throw new Error("Malformed buffer data: " + res); } } var filters = { NONE: "", OCTAHEDRAL: "meshopt_decodeFilterOct", QUATERNION: "meshopt_decodeFilterQuat", EXPONENTIAL: "meshopt_decodeFilterExp", }; var decoders = { ATTRIBUTES: "meshopt_decodeVertexBuffer", TRIANGLES: "meshopt_decodeIndexBuffer", INDICES: "meshopt_decodeIndexSequence", }; var workers = []; var requestId = 0; function createWorker(url) { var worker = { object: new Worker(url), pending: 0, requests: {} }; worker.object.onmessage = function(event) { var data = event.data; worker.pending -= data.count; worker.requests[data.id][data.action](data.value); delete worker.requests[data.id]; }; return worker; } function initWorkers(count) { var source = "var instance; var ready = WebAssembly.instantiate(new Uint8Array([" + new Uint8Array(unpack(wasm)) + "]), {})" + ".then(function(result) { instance = result.instance; instance.exports.__wasm_call_ctors(); });" + "self.onmessage = workerProcess;" + decode.toString() + workerProcess.toString(); var blob = new Blob([source], {type: 'text/javascript'}); var url = URL.createObjectURL(blob); for (var i = 0; i < count; ++i) { workers[i] = createWorker(url); } URL.revokeObjectURL(url); } function decodeWorker(count, size, source, mode, filter) { var worker = workers[0]; for (var i = 1; i < workers.length; ++i) { if (workers[i].pending < worker.pending) { worker = workers[i]; } } return new Promise(function (resolve, reject) { var data = new Uint8Array(source); var id = requestId++; worker.pending += count; worker.requests[id] = { resolve: resolve, reject: reject }; worker.object.postMessage({ id: id, count: count, size: size, source: data, mode: mode, filter: filter }, [ data.buffer ]); }); } function workerProcess(event) { ready.then(function() { var data = event.data; try { var target = new Uint8Array(data.count * data.size); decode(instance.exports[data.mode], target, data.count, data.size, data.source, instance.exports[data.filter]); self.postMessage({ id: data.id, count: data.count, action: "resolve", value: target }, [ target.buffer ]); } catch (error) { self.postMessage({ id: data.id, count: data.count, action: "reject", value: error }); } }); } return { ready: ready, supported: true, useWorkers: function(count) { initWorkers(count); }, decodeVertexBuffer: function(target, count, size, source, filter) { decode(instance.exports.meshopt_decodeVertexBuffer, target, count, size, source, instance.exports[filters[filter]]); }, decodeIndexBuffer: function(target, count, size, source) { decode(instance.exports.meshopt_decodeIndexBuffer, target, count, size, source); }, decodeIndexSequence: function(target, count, size, source) { decode(instance.exports.meshopt_decodeIndexSequence, target, count, size, source); }, decodeGltfBuffer: function(target, count, size, source, mode, filter) { decode(instance.exports[decoders[mode]], target, count, size, source, instance.exports[filters[filter]]); }, decodeGltfBufferAsync: function(count, size, source, mode, filter) { if (workers.length > 0) { return decodeWorker(count, size, source, decoders[mode], filters[filter]); } return ready.then(function() { var target = new Uint8Array(count * size); decode(instance.exports[decoders[mode]], target, count, size, source, instance.exports[filters[filter]]); return target; }); } }; })(); const extToType = new Map([ ['bin', 'binary'], ['css', 'css'], ['frag', 'shader'], ['glb', 'container'], ['glsl', 'shader'], ['hdr', 'texture'], ['html', 'html'], ['jpg', 'texture'], ['js', 'script'], ['json', 'json'], ['mp3', 'audio'], ['mjs', 'script'], ['ply', 'gsplat'], ['png', 'texture'], ['sog', 'gsplat'], ['txt', 'text'], ['vert', 'shader'], ['webp', 'texture'] ]); // provide buffer view callback so we can handle models compressed with MeshOptimizer // https://github.com/zeux/meshoptimizer const processBufferView = (gltfBuffer, buffers, continuation) => { if (gltfBuffer.extensions && gltfBuffer.extensions.EXT_meshopt_compression) { const extensionDef = gltfBuffer.extensions.EXT_meshopt_compression; Promise.all([MeshoptDecoder.ready, buffers[extensionDef.buffer]]).then((promiseResult) => { const buffer = promiseResult[1]; const byteOffset = extensionDef.byteOffset || 0; const byteLength = extensionDef.byteLength || 0; const count = extensionDef.count; const stride = extensionDef.byteStride; const result = new Uint8Array(count * stride); const source = new Uint8Array(buffer.buffer, buffer.byteOffset + byteOffset, byteLength); MeshoptDecoder.decodeGltfBuffer(result, count, stride, source, extensionDef.mode, extensionDef.filter); continuation(null, result); }); } else { continuation(null, null); } }; /** * The AssetElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-asset/ | ``} elements. * The AssetElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class AssetElement extends HTMLElement { constructor() { super(...arguments); this._lazy = false; /** * The asset that is loaded. */ this.asset = null; } disconnectedCallback() { this.destroyAsset(); } createAsset() { var _a; const id = this.getAttribute('id') || ''; const src = this.getAttribute('src') || ''; let type = this.getAttribute('type'); // If no type is specified, try to infer it from the file extension. if (!type) { const ext = src.split('.').pop(); type = (_a = extToType.get(ext || '')) !== null && _a !== void 0 ? _a : null; } if (!type) { console.warn(`Unsupported asset type: ${src}`); return; } if (type === 'container') { this.asset = new Asset(id, type, { url: src }, undefined, { // @ts-ignore TODO no definition in pc bufferView: { processAsync: processBufferView.bind(this) } }); } else { // @ts-ignore this.asset = new Asset(id, type, { url: src }); } this.asset.preload = !this._lazy; } destroyAsset() { if (this.asset) { this.asset.unload(); this.asset = null; } } /** * Sets whether the asset should be loaded lazily. * @param value - The lazy loading flag. */ set lazy(value) { this._lazy = value; if (this.asset) { this.asset.preload = !value; } } /** * Gets whether the asset should be loaded lazily. * @returns The lazy loading flag. */ get lazy() { return this._lazy; } static get(id) { const assetElement = document.querySelector(`pc-asset[id="${id}"]`); return assetElement === null || assetElement === void 0 ? void 0 : assetElement.asset; } static get observedAttributes() { return ['lazy']; } attributeChangedCallback(name, _oldValue, _newValue) { if (name === 'lazy') { this.lazy = this.hasAttribute('lazy'); } } } customElements.define('pc-asset', AssetElement); /** * Represents a component in the PlayCanvas engine. * * @category Components */ class ComponentElement extends AsyncElement { /** * Creates a new ComponentElement instance. * * @param componentName - The name of the component. * @ignore */ constructor(componentName) { super(); this._enabled = true; this._component = null; this._componentName = componentName; } // Method to be overridden by subclasses to provide initial component data getInitialComponentData() { return {}; } async addComponent() { const entityElement = this.closestEntity; if (entityElement) { await entityElement.ready(); // Add the component to the entity const data = this.getInitialComponentData(); this._component = entityElement.entity.addComponent(this._componentName, data); } } initComponent() { } async connectedCallback() { var _a; await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready()); await this.addComponent(); this.initComponent(); this._onReady(); } disconnectedCallback() { // Remove the component when the element is disconnected if (this.component && this.component.entity) { this._component.entity.removeComponent(this._componentName); this._component = null; } } get component() { return this._component; } /** * Sets the enabled state of the component. * @param value - The enabled state of the component. */ set enabled(value) { this._enabled = value; if (this.component) { this.component.enabled = value; } } /** * Gets the enabled state of the component. * @returns The enabled state of the component. */ get enabled() { return this._enabled; } static get observedAttributes() { return ['enabled']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'enabled': this.enabled = newValue !== 'false'; break; } } } /** * The ListenerComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-listener/ | ``} elements. * The ListenerComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class ListenerComponentElement extends ComponentElement { /** @ignore */ constructor() { super('audiolistener'); } /** * Gets the underlying PlayCanvas audio listener component. * @returns The audio listener component. */ get component() { return super.component; } } customElements.define('pc-listener', ListenerComponentElement); const tonemaps = new Map([ ['none', TONEMAP_NONE], ['linear', TONEMAP_LINEAR], ['filmic', TONEMAP_FILMIC], ['hejl', TONEMAP_HEJL], ['aces', TONEMAP_ACES], ['aces2', TONEMAP_ACES2], ['neutral', TONEMAP_NEUTRAL] ]); /** * The CameraComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-camera/ | ``} elements. * The CameraComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class CameraComponentElement extends ComponentElement { /** @ignore */ constructor() { super('camera'); this._clearColor = new Color(0.75, 0.75, 0.75, 1); this._clearColorBuffer = true; this._clearDepthBuffer = true; this._clearStencilBuffer = false; this._cullFaces = true; this._farClip = 1000; this._flipFaces = false; this._fov = 45; this._frustumCulling = true; this._gamma = 'srgb'; this._horizontalFov = false; this._nearClip = 0.1; this._orthographic = false; this._orthoHeight = 10; this._priority = 0; this._rect = new Vec4(0, 0, 1, 1); this._scissorRect = new Vec4(0, 0, 1, 1); this._tonemap = 'none'; } getInitialComponentData() { return { clearColor: this._clearColor, clearColorBuffer: this._clearColorBuffer, clearDepthBuffer: this._clearDepthBuffer, clearStencilBuffer: this._clearStencilBuffer, cullFaces: this._cullFaces, farClip: this._farClip, flipFaces: this._flipFaces, fov: this._fov, frustumCulling: this._frustumCulling, gammaCorrection: this._gamma === 'srgb' ? GAMMA_SRGB : GAMMA_NONE, horizontalFov: this._horizontalFov, nearClip: this._nearClip, orthographic: this._orthographic, orthoHeight: this._orthoHeight, priority: this._priority, rect: this._rect, scissorRect: this._scissorRect, toneMapping: tonemaps.get(this._tonemap) }; } get xrAvailable() { var _a; const xrManager = (_a = this.component) === null || _a === void 0 ? void 0 : _a.system.app.xr; return xrManager && xrManager.supported && xrManager.isAvailable(XRTYPE_VR); } /** * Starts the camera in XR mode. * @param type - The type of XR mode to start. * @param space - The space to start the camera in. */ startXr(type, space) { if (this.component && this.xrAvailable) { this.component.startXr(type, space, { callback: (err) => { if (err) console.error(`WebXR Immersive VR failed to start: ${err.message}`); } }); } } /** * Ends the camera's XR mode. */ endXr() { if (this.component) { this.component.endXr(); } } /** * Gets the underlying PlayCanvas camera component. * @returns The camera component. */ get component() { return super.component; } /** * Sets the clear color of the camera. * @param value - The clear color. */ set clearColor(value) { this._clearColor = value; if (this.component) { this.component.clearColor = value; } } /** * Gets the clear color of the camera. * @returns The clear color. */ get clearColor() { return this._clearColor; } /** * Sets the clear color buffer of the camera. * @param value - The clear color buffer. */ set clearColorBuffer(value) { this._clearColorBuffer = value; if (this.component) { this.component.clearColorBuffer = value; } } /** * Gets the clear color buffer of the camera. * @returns The clear color buffer. */ get clearColorBuffer() { return this._clearColorBuffer; } /** * Sets the clear depth buffer of the camera. * @param value - The clear depth buffer. */ set clearDepthBuffer(value) { this._clearDepthBuffer = value; if (this.component) { this.component.clearDepthBuffer = value; } } /** * Gets the clear depth buffer of the camera. * @returns The clear depth buffer. */ get clearDepthBuffer() { return this._clearDepthBuffer; } /** * Sets the clear stencil buffer of the camera. * @param value - The clear stencil buffer. */ set clearStencilBuffer(value) { this._clearStencilBuffer = value; if (this.component) { this.component.clearStencilBuffer = value; } } /** * Gets the clear stencil buffer of the camera. * @returns The clear stencil buffer. */ get clearStencilBuffer() { return this._clearStencilBuffer; } /** * Sets the cull faces of the camera. * @param value - The cull faces. */ set cullFaces(value) { this._cullFaces = value; if (this.component) { this.component.cullFaces = value; } } /** * Gets the cull faces of the camera. * @returns The cull faces. */ get cullFaces() { return this._cullFaces; } /** * Sets the far clip distance of the camera. * @param value - The far clip distance. */ set farClip(value) { this._farClip = value; if (this.component) { this.component.farClip = value; } } /** * Gets the far clip distance of the camera. * @returns The far clip distance. */ get farClip() { return this._farClip; } /** * Sets the flip faces of the camera. * @param value - The flip faces. */ set flipFaces(value) { this._flipFaces = value; if (this.component) { this.component.flipFaces = value; } } /** * Gets the flip faces of the camera. * @returns The flip faces. */ get flipFaces() { return this._flipFaces; } /** * Sets the field of view of the camera. * @param value - The field of view. */ set fov(value) { this._fov = value; if (this.component) { this.component.fov = value; } } /** * Gets the field of view of the camera. * @returns The field of view. */ get fov() { return this._fov; } /** * Sets the frustum culling of the camera. * @param value - The frustum culling. */ set frustumCulling(value) { this._frustumCulling = value; if (this.component) { this.component.frustumCulling = value; } } /** * Gets the frustum culling of the camera. * @returns The frustum culling. */ get frustumCulling() { return this._frustumCulling; } /** * Sets the gamma correction of the camera. * @param value - The gamma correction. */ set gamma(value) { this._gamma = value; if (this.component) { this.component.gammaCorrection = value === 'srgb' ? GAMMA_SRGB : GAMMA_NONE; } } /** * Gets the gamma correction of the camera. * @returns The gamma correction. */ get gamma() { return this._gamma; } /** * Sets whether the camera's field of view (fov) is horizontal or vertical. Defaults to false * (meaning it is vertical be default). * @param value - Whether the camera's field of view is horizontal. */ set horizontalFov(value) { this._horizontalFov = value; if (this.component) { this.component.horizontalFov = value; } } /** * Gets whether the camera's field of view (fov) is horizontal or vertical. * @returns Whether the camera's field of view is horizontal. */ get horizontalFov() { return this._horizontalFov; } /** * Sets the near clip distance of the camera. * @param value - The near clip distance. */ set nearClip(value) { this._nearClip = value; if (this.component) { this.component.nearClip = value; } } /** * Gets the near clip distance of the camera. * @returns The near clip distance. */ get nearClip() { return this._nearClip; } /** * Sets the orthographic projection of the camera. * @param value - The orthographic projection. */ set orthographic(value) { this._orthographic = value; if (this.component) { this.component.projection = value ? PROJECTION_ORTHOGRAPHIC : PROJECTION_PERSPECTIVE; } } /** * Gets the orthographic projection of the camera. * @returns The orthographic projection. */ get orthographic() { return this._orthographic; } /** * Sets the orthographic height of the camera. * @param value - The orthographic height. */ set orthoHeight(value) { this._orthoHeight = value; if (this.component) { this.component.orthoHeight = value; } } /** * Gets the orthographic height of the camera. * @returns The orthographic height. */ get orthoHeight() { return this._orthoHeight; } /** * Sets the priority of the camera. * @param value - The priority. */ set priority(value) { this._priority = value; if (this.component) { this.component.priority = value; } } /** * Gets the priority of the camera. * @returns The priority. */ get priority() { return this._priority; } /** * Sets the rect of the camera. * @param value - The rect. */ set rect(value) { this._rect = value; if (this.component) { this.component.rect = value; } } /** * Gets the rect of the camera. * @returns The rect. */ get rect() { return this._rect; } /** * Sets the scissor rect of the camera. * @param value - The scissor rect. */ set scissorRect(value) { this._scissorRect = value; if (this.component) { this.component.scissorRect = value; } } /** * Gets the scissor rect of the camera. * @returns The scissor rect. */ get scissorRect() { return this._scissorRect; } /** * Sets the tone mapping of the camera. * @param value - The tone mapping. */ set tonemap(value) { var _a; this._tonemap = value; if (this.component) { this.component.toneMapping = (_a = tonemaps.get(value)) !== null && _a !== void 0 ? _a : TONEMAP_NONE; } } /** * Gets the tone mapping of the camera. * @returns The tone mapping. */ get tonemap() { return this._tonemap; } static get observedAttributes() { return [ ...super.observedAttributes, 'clear-color', 'clear-color-buffer', 'clear-depth-buffer', 'clear-stencil-buffer', 'cull-faces', 'far-clip', 'flip-faces', 'fov', 'frustum-culling', 'gamma', 'horizontal-fov', 'near-clip', 'orthographic', 'ortho-height', 'priority', 'rect', 'scissor-rect', 'tonemap' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'clear-color': this.clearColor = parseColor(newValue); break; case 'clear-color-buffer': this.clearColorBuffer = newValue !== 'false'; break; case 'clear-depth-buffer': this.clearDepthBuffer = newValue !== 'false'; break; case 'clear-stencil-buffer': this.clearStencilBuffer = newValue !== 'false'; break; case 'cull-faces': this.cullFaces = newValue !== 'false'; break; case 'far-clip': this.farClip = parseFloat(newValue); break; case 'flip-faces': this.flipFaces = newValue !== 'true'; break; case 'fov': this.fov = parseFloat(newValue); break; case 'frustum-culling': this.frustumCulling = newValue !== 'false'; break; case 'gamma': this.gamma = newValue; break; case 'horizontal-fov': this.horizontalFov = this.hasAttribute('horizontal-fov'); break; case 'near-clip': this.nearClip = parseFloat(newValue); break; case 'orthographic': this.orthographic = this.hasAttribute('orthographic'); break; case 'ortho-height': this.orthoHeight = parseFloat(newValue); break; case 'priority': this.priority = parseFloat(newValue); break; case 'rect': this.rect = parseVec4(newValue); break; case 'scissor-rect': this.scissorRect = parseVec4(newValue); break; case 'tonemap': this.tonemap = newValue; break; } } } customElements.define('pc-camera', CameraComponentElement); /** * The CollisionComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-collision/ | ``} elements. * The CollisionComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class CollisionComponentElement extends ComponentElement { /** @ignore */ constructor() { super('collision'); this._angularOffset = new Quat(); this._axis = 1; this._convexHull = false; this._halfExtents = new Vec3(0.5, 0.5, 0.5); this._height = 2; this._linearOffset = new Vec3(); this._radius = 0.5; this._type = 'box'; } getInitialComponentData() { return { axis: this._axis, angularOffset: this._angularOffset, convexHull: this._convexHull, halfExtents: this._halfExtents, height: this._height, linearOffset: this._linearOffset, radius: this._radius, type: this._type }; } /** * Gets the underlying PlayCanvas collision component. * @returns The collision component. */ get component() { return super.component; } set angularOffset(value) { this._angularOffset = value; if (this.component) { this.component.angularOffset = value; } } get angularOffset() { return this._angularOffset; } set axis(value) { this._axis = value; if (this.component) { this.component.axis = value; } } get axis() { return this._axis; } set convexHull(value) { this._convexHull = value; if (this.component) { this.component.convexHull = value; } } get convexHull() { return this._convexHull; } set halfExtents(value) { this._halfExtents = value; if (this.component) { this.component.halfExtents = value; } } get halfExtents() { return this._halfExtents; } set height(value) { this._height = value; if (this.component) { this.component.height = value; } } get height() { return this._height; } set linearOffset(value) { this._linearOffset = value; if (this.component) { this.component.linearOffset = value; } } get linearOffset() { return this._linearOffset; } set radius(value) { this._radius = value; if (this.component) { this.component.radius = value; } } get radius() { return this._radius; } set type(value) { this._type = value; if (this.component) { this.component.type = value; } } get type() { return this._type; } static get observedAttributes() { return [...super.observedAttributes, 'angular-offset', 'axis', 'convex-hull', 'half-extents', 'height', 'linear-offset', 'radius', 'type']; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'angular-offset': this.angularOffset = parseQuat(newValue); break; case 'axis': this.axis = parseInt(newValue, 10); break; case 'convex-hull': this.convexHull = this.hasAttribute('convex-hull'); break; case 'half-extents': this.halfExtents = parseVec3(newValue); break; case 'height': this.height = parseFloat(newValue); break; case 'linear-offset': this.linearOffset = parseVec3(newValue); break; case 'radius': this.radius = parseFloat(newValue); break; case 'type': this.type = newValue; break; } } } customElements.define('pc-collision', CollisionComponentElement); /** * The ElementComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-element/ | ``} elements. * The ElementComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class ElementComponentElement extends ComponentElement { /** @ignore */ constructor() { super('element'); this._anchor = new Vec4(0.5, 0.5, 0.5, 0.5); this._asset = ''; this._autoWidth = true; this._color = new Color(1, 1, 1, 1); this._enableMarkup = false; this._fontSize = 32; this._lineHeight = 32; this._pivot = new Vec2(0.5, 0.5); this._text = ''; this._type = 'group'; this._width = 0; this._wrapLines = false; } initComponent() { this.component._text._material.useFog = true; } getInitialComponentData() { return { anchor: this._anchor, autoWidth: this._autoWidth, color: this._color, enableMarkup: this._enableMarkup, fontAsset: AssetElement.get(this._asset).id, fontSize: this._fontSize, lineHeight: this._lineHeight, pivot: this._pivot, type: this._type, text: this._text, width: this._width, wrapLines: this._wrapLines }; } /** * Gets the underlying PlayCanvas element component. * @returns The element component. */ get component() { return super.component; } /** * Sets the anchor of the element component. * @param value - The anchor. */ set anchor(value) { this._anchor = value; if (this.component) { this.component.anchor = value; } } /** * Gets the anchor of the element component. * @returns The anchor. */ get anchor() { return this._anchor; } /** * Sets the id of the `pc-asset` to use for the font. * @param value - The asset ID. */ set asset(value) { this._asset = value; const asset = AssetElement.get(value); if (this.component && asset) { this.component.fontAsset = asset.id; } } /** * Gets the id of the `pc-asset` to use for the font. * @returns The asset ID. */ get asset() { return this._asset; } /** * Sets whether the element component should automatically adjust its width. * @param value - Whether to automatically adjust the width. */ set autoWidth(value) { this._autoWidth = value; if (this.component) { this.component.autoWidth = value; } } /** * Gets whether the element component should automatically adjust its width. * @returns Whether to automatically adjust the width. */ get autoWidth() { return this._autoWidth; } /** * Sets the color of the element component. * @param value - The color. */ set color(value) { this._color = value; if (this.component) { this.component.color = value; } } /** * Gets the color of the element component. * @returns The color. */ get color() { return this._color; } /** * Sets whether the element component should use markup. * @param value - Whether to enable markup. */ set enableMarkup(value) { this._enableMarkup = value; if (this.component) { this.component.enableMarkup = value; } } /** * Gets whether the element component should use markup. * @returns Whether markup is enabled. */ get enableMarkup() { return this._enableMarkup; } /** * Sets the font size of the element component. * @param value - The font size. */ set fontSize(value) { this._fontSize = value; if (this.component) { this.component.fontSize = value; } } /** * Gets the font size of the element component. * @returns The font size. */ get fontSize() { return this._fontSize; } /** * Sets the line height of the element component. * @param value - The line height. */ set lineHeight(value) { this._lineHeight = value; if (this.component) { this.component.lineHeight = value; } } /** * Gets the line height of the element component. * @returns The line height. */ get lineHeight() { return this._lineHeight; } /** * Sets the pivot of the element component. * @param value - The pivot. */ set pivot(value) { this._pivot = value; if (this.component) { this.component.pivot = value; } } /** * Gets the pivot of the element component. * @returns The pivot. */ get pivot() { return this._pivot; } /** * Sets the text of the element component. * @param value - The text. */ set text(value) { this._text = value; if (this.component) { this.component.text = value; } } /** * Gets the text of the element component. * @returns The text. */ get text() { return this._text; } /** * Sets the type of the element component. * @param value - The type. */ set type(value) { this._type = value; if (this.component) { this.component.type = value; } } /** * Gets the type of the element component. * @returns The type. */ get type() { return this._type; } /** * Sets the width of the element component. * @param value - The width. */ set width(value) { this._width = value; if (this.component) { this.component.width = value; } } /** * Gets the width of the element component. * @returns The width. */ get width() { return this._width; } /** * Sets whether the element component should wrap lines. * @param value - Whether to wrap lines. */ set wrapLines(value) { this._wrapLines = value; if (this.component) { this.component.wrapLines = value; } } /** * Gets whether the element component should wrap lines. * @returns Whether to wrap lines. */ get wrapLines() { return this._wrapLines; } static get observedAttributes() { return [ ...super.observedAttributes, 'anchor', 'asset', 'auto-width', 'color', 'enable-markup', 'font-size', 'line-height', 'pivot', 'text', 'type', 'width', 'wrap-lines' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'anchor': this.anchor = parseVec4(newValue); break; case 'asset': this.asset = newValue; break; case 'auto-width': this.autoWidth = newValue !== 'false'; break; case 'color': this.color = parseColor(newValue); break; case 'enable-markup': this.enableMarkup = this.hasAttribute(name); break; case 'font-size': this.fontSize = Number(newValue); break; case 'line-height': this.lineHeight = Number(newValue); break; case 'pivot': this.pivot = parseVec2(newValue); break; case 'text': this.text = newValue; break; case 'type': this.type = newValue; break; case 'width': this.width = Number(newValue); break; case 'wrap-lines': this.wrapLines = this.hasAttribute(name); break; } } } customElements.define('pc-element', ElementComponentElement); const shadowTypes = new Map([ ['pcf1-16f', SHADOW_PCF1_16F], ['pcf1-32f', SHADOW_PCF1_32F], ['pcf3-16f', SHADOW_PCF3_16F], ['pcf3-32f', SHADOW_PCF3_32F], ['pcf5-16f', SHADOW_PCF5_16F], ['pcf5-32f', SHADOW_PCF5_32F], ['vsm-16f', SHADOW_VSM_16F], ['vsm-32f', SHADOW_VSM_32F], ['pcss-32f', SHADOW_PCSS_32F] ]); /** * The LightComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-light/ | ``} elements. * The LightComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class LightComponentElement extends ComponentElement { /** @ignore */ constructor() { super('light'); this._castShadows = false; this._color = new Color(1, 1, 1); this._innerConeAngle = 40; this._intensity = 1; this._normalOffsetBias = 0.05; this._outerConeAngle = 45; this._range = 10; this._shadowBias = 0.2; this._shadowDistance = 16; this._shadowIntensity = 1; this._shadowResolution = 1024; this._shadowType = 'pcf3-32f'; this._type = 'directional'; this._vsmBias = 0.01; this._vsmBlurSize = 11; } getInitialComponentData() { return { castShadows: this._castShadows, color: this._color, innerConeAngle: this._innerConeAngle, intensity: this._intensity, normalOffsetBias: this._normalOffsetBias, outerConeAngle: this._outerConeAngle, range: this._range, shadowBias: this._shadowBias, shadowDistance: this._shadowDistance, shadowIntensity: this._shadowIntensity, shadowResolution: this._shadowResolution, shadowType: shadowTypes.get(this._shadowType), type: this._type, vsmBias: this._vsmBias, vsmBlurSize: this._vsmBlurSize }; } /** * Gets the underlying PlayCanvas light component. * @returns The light component. */ get component() { return super.component; } /** * Sets the cast shadows flag of the light. * @param value - The cast shadows flag. */ set castShadows(value) { this._castShadows = value; if (this.component) { this.component.castShadows = value; } } /** * Gets the cast shadows flag of the light. * @returns The cast shadows flag. */ get castShadows() { return this._castShadows; } /** * Sets the color of the light. * @param value - The color. */ set color(value) { this._color = value; if (this.component) { this.component.color = value; } } /** * Gets the color of the light. * @returns The color. */ get color() { return this._color; } /** * Sets the inner cone angle of the light. * @param value - The inner cone angle. */ set innerConeAngle(value) { this._innerConeAngle = value; if (this.component) { this.component.innerConeAngle = value; } } /** * Gets the inner cone angle of the light. * @returns The inner cone angle. */ get innerConeAngle() { return this._innerConeAngle; } /** * Sets the intensity of the light. * @param value - The intensity. */ set intensity(value) { this._intensity = value; if (this.component) { this.component.intensity = value; } } /** * Gets the intensity of the light. * @returns The intensity. */ get intensity() { return this._intensity; } /** * Sets the normal offset bias of the light. * @param value - The normal offset bias. */ set normalOffsetBias(value) { this._normalOffsetBias = value; if (this.component) { this.component.normalOffsetBias = value; } } /** * Gets the normal offset bias of the light. * @returns The normal offset bias. */ get normalOffsetBias() { return this._normalOffsetBias; } /** * Sets the outer cone angle of the light. * @param value - The outer cone angle. */ set outerConeAngle(value) { this._outerConeAngle = value; if (this.component) { this.component.outerConeAngle = value; } } /** * Gets the outer cone angle of the light. * @returns The outer cone angle. */ get outerConeAngle() { return this._outerConeAngle; } /** * Sets the range of the light. * @param value - The range. */ set range(value) { this._range = value; if (this.component) { this.component.range = value; } } /** * Gets the range of the light. * @returns The range. */ get range() { return this._range; } /** * Sets the shadow bias of the light. * @param value - The shadow bias. */ set shadowBias(value) { this._shadowBias = value; if (this.component) { this.component.shadowBias = value; } } /** * Gets the shadow bias of the light. * @returns The shadow bias. */ get shadowBias() { return this._shadowBias; } /** * Sets the shadow distance of the light. * @param value - The shadow distance. */ set shadowDistance(value) { this._shadowDistance = value; if (this.component) { this.component.shadowDistance = value; } } /** * Gets the shadow distance of the light. * @returns The shadow distance. */ get shadowDistance() { return this._shadowDistance; } /** * Sets the shadow intensity of the light. * @param value - The shadow intensity. */ set shadowIntensity(value) { this._shadowIntensity = value; if (this.component) { this.component.shadowIntensity = value; } } /** * Gets the shadow intensity of the light. * @returns The shadow intensity. */ get shadowIntensity() { return this._shadowIntensity; } /** * Sets the shadow resolution of the light. * @param value - The shadow resolution. */ set shadowResolution(value) { this._shadowResolution = value; if (this.component) { this.component.shadowResolution = value; } } /** * Gets the shadow resolution of the light. * @returns The shadow resolution. */ get shadowResolution() { return this._shadowResolution; } /** * Sets the shadow type of the light. * @param value - The shadow type. Can be: * * - `pcf1-16f` - 1-tap percentage-closer filtered shadow map with 16-bit depth. * - `pcf1-32f` - 1-tap percentage-closer filtered shadow map with 32-bit depth. * - `pcf3-16f` - 3-tap percentage-closer filtered shadow map with 16-bit depth. * - `pcf3-32f` - 3-tap percentage-closer filtered shadow map with 32-bit depth. * - `pcf5-16f` - 5-tap percentage-closer filtered shadow map with 16-bit depth. * - `pcf5-32f` - 5-tap percentage-closer filtered shadow map with 32-bit depth. * - `vsm-16f` - Variance shadow map with 16-bit depth. * - `vsm-32f` - Variance shadow map with 32-bit depth. * - `pcss-32f` - Percentage-closer soft shadow with 32-bit depth. */ set shadowType(value) { var _a; this._shadowType = value; if (this.component) { this.component.shadowType = (_a = shadowTypes.get(value)) !== null && _a !== void 0 ? _a : SHADOW_PCF3_32F; } } /** * Gets the shadow type of the light. * @returns The shadow type. */ get shadowType() { return this._shadowType; } /** * Sets the type of the light. * @param value - The type. */ set type(value) { if (!['directional', 'omni', 'spot'].includes(value)) { console.warn(`Invalid light type '${value}', using default type '${this._type}'.`); return; } this._type = value; if (this.component) { this.component.type = value; } } /** * Gets the type of the light. * @returns The type. */ get type() { return this._type; } /** * Sets the VSM bias of the light. * @param value - The VSM bias. */ set vsmBias(value) { this._vsmBias = value; if (this.component) { this.component.vsmBias = value; } } /** * Gets the VSM bias of the light. * @returns The VSM bias. */ get vsmBias() { return this._vsmBias; } /** * Sets the VSM blur size of the light. Minimum is 1, maximum is 25. Default is 11. * @param value - The VSM blur size. */ set vsmBlurSize(value) { this._vsmBlurSize = value; if (this.component) { this.component.vsmBlurSize = value; } } /** * Gets the VSM blur size of the light. * @returns The VSM blur size. */ get vsmBlurSize() { return this._vsmBlurSize; } static get observedAttributes() { return [ ...super.observedAttributes, 'color', 'cast-shadows', 'intensity', 'inner-cone-angle', 'normal-offset-bias', 'outer-cone-angle', 'range', 'shadow-bias', 'shadow-distance', 'shadow-intensity', 'shadow-resolution', 'shadow-type', 'type', 'vsm-bias', 'vsm-blur-size' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'color': this.color = parseColor(newValue); break; case 'cast-shadows': this.castShadows = this.hasAttribute('cast-shadows'); break; case 'inner-cone-angle': this.innerConeAngle = Number(newValue); break; case 'intensity': this.intensity = Number(newValue); break; case 'normal-offset-bias': this.normalOffsetBias = Number(newValue); break; case 'outer-cone-angle': this.outerConeAngle = Number(newValue); break; case 'range': this.range = Number(newValue); break; case 'shadow-bias': this.shadowBias = Number(newValue); break; case 'shadow-distance': this.shadowDistance = Number(newValue); break; case 'shadow-resolution': this.shadowResolution = Number(newValue); break; case 'shadow-intensity': this.shadowIntensity = Number(newValue); break; case 'shadow-type': this.shadowType = newValue; break; case 'type': this.type = newValue; break; case 'vsm-bias': this.vsmBias = Number(newValue); break; case 'vsm-blur-size': this.vsmBlurSize = Number(newValue); break; } } } customElements.define('pc-light', LightComponentElement); /** * The ParticleSystemComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-particles/ | ``} elements. * The ParticleSystemComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class ParticleSystemComponentElement extends ComponentElement { /** @ignore */ constructor() { super('particlesystem'); this._asset = ''; } getInitialComponentData() { var _a; const asset = AssetElement.get(this._asset); if (!asset) { return {}; } if (asset.resource.colorMapAsset) { const id = asset.resource.colorMapAsset; const colorMapAsset = (_a = AssetElement.get(id)) === null || _a === void 0 ? void 0 : _a.id; if (colorMapAsset) { asset.resource.colorMapAsset = colorMapAsset; } } return asset.resource; } /** * Gets the underlying PlayCanvas particle system component. * @returns The particle system component. */ get component() { return super.component; } applyConfig(resource) { if (!this.component) { return; } // Set all the config properties on the component for (const key in resource) { if (resource.hasOwnProperty(key)) { this.component[key] = resource[key]; } } } async _loadAsset() { var _a; const appElement = await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready()); const app = appElement === null || appElement === void 0 ? void 0 : appElement.app; const asset = AssetElement.get(this._asset); if (!asset) { return; } if (asset.loaded) { this.applyConfig(asset.resource); } else { asset.once('load', () => { this.applyConfig(asset.resource); }); app.assets.load(asset); } } /** * Sets the id of the `pc-asset` to use for the model. * @param value - The asset ID. */ set asset(value) { this._asset = value; if (this.isConnected) { this._loadAsset(); } } /** * Gets the id of the `pc-asset` to use for the model. * @returns The asset ID. */ get asset() { return this._asset; } // Control methods /** * Starts playing the particle system */ play() { if (this.component) { this.component.play(); } } /** * Pauses the particle system */ pause() { if (this.component) { this.component.pause(); } } /** * Resets the particle system */ reset() { if (this.component) { this.component.reset(); } } /** * Stops the particle system */ stop() { if (this.component) { this.component.stop(); } } static get observedAttributes() { return [ ...super.observedAttributes, 'asset' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'asset': this.asset = newValue; break; } } } customElements.define('pc-particles', ParticleSystemComponentElement); /** * The MaterialElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-material/ | ``} elements. * The MaterialElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class MaterialElement extends HTMLElement { constructor() { super(...arguments); this._diffuse = new Color(1, 1, 1); this._diffuseMap = ''; this._metalnessMap = ''; this._normalMap = ''; this._roughnessMap = ''; this.material = null; } createMaterial() { this.material = new StandardMaterial(); this.material.glossInvert = false; this.material.useMetalness = false; this.material.diffuse = this._diffuse; this.diffuseMap = this._diffuseMap; this.metalnessMap = this._metalnessMap; this.normalMap = this._normalMap; this.roughnessMap = this._roughnessMap; this.material.update(); } disconnectedCallback() { if (this.material) { this.material.destroy(); this.material = null; } } setMap(map, property) { if (this.material) { const asset = AssetElement.get(map); if (asset) { if (asset.loaded) { this.material[property] = asset.resource; this.material[property].anisotropy = 4; } else { asset.once('load', () => { this.material[property] = asset.resource; this.material[property].anisotropy = 4; this.material.update(); }); } } } } set diffuse(value) { this._diffuse = value; if (this.material) { this.material.diffuse = value; } } get diffuse() { return this._diffuse; } set diffuseMap(value) { this._diffuseMap = value; this.setMap(value, 'diffuseMap'); } get diffuseMap() { return this._diffuseMap; } set metalnessMap(value) { this._metalnessMap = value; this.setMap(value, 'metalnessMap'); } get metalnessMap() { return this._metalnessMap; } set normalMap(value) { this._normalMap = value; this.setMap(value, 'normalMap'); } get normalMap() { return this._normalMap; } set roughnessMap(value) { this._roughnessMap = value; this.setMap(value, 'glossMap'); } get roughnessMap() { return this._roughnessMap; } static get(id) { const materialElement = document.querySelector(`pc-material[id="${id}"]`); return materialElement === null || materialElement === void 0 ? void 0 : materialElement.material; } static get observedAttributes() { return ['diffuse', 'diffuse-map', 'metalness-map', 'normal-map', 'roughness-map']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'diffuse': this.diffuse = parseColor(newValue); break; case 'diffuse-map': this.diffuseMap = newValue; break; case 'metalness-map': this.metalnessMap = newValue; break; case 'normal-map': this.normalMap = newValue; break; case 'roughness-map': this.roughnessMap = newValue; break; } } } customElements.define('pc-material', MaterialElement); /** * The RenderComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-render/ | ``} elements. * The RenderComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class RenderComponentElement extends ComponentElement { /** @ignore */ constructor() { super('render'); this._castShadows = true; this._material = ''; this._receiveShadows = true; this._type = 'asset'; } getInitialComponentData() { return { type: this._type, castShadows: this._castShadows, material: MaterialElement.get(this._material), receiveShadows: this._receiveShadows }; } /** * Gets the underlying PlayCanvas render component. * @returns The render component. */ get component() { return super.component; } /** * Sets the type of the render component. * @param value - The type. */ set type(value) { this._type = value; if (this.component) { this.component.type = value; } } /** * Gets the type of the render component. * @returns The type. */ get type() { return this._type; } /** * Sets the cast shadows flag of the render component. * @param value - The cast shadows flag. */ set castShadows(value) { this._castShadows = value; if (this.component) { this.component.castShadows = value; } } /** * Gets the cast shadows flag of the render component. * @returns The cast shadows flag. */ get castShadows() { return this._castShadows; } /** * Sets the material of the render component. * @param value - The id of the material asset to use. */ set material(value) { this._material = value; if (this.component) { this.component.material = MaterialElement.get(value); } } /** * Gets the id of the material asset used by the render component. * @returns The id of the material asset. */ get material() { return this._material; } /** * Sets the receive shadows flag of the render component. * @param value - The receive shadows flag. */ set receiveShadows(value) { this._receiveShadows = value; if (this.component) { this.component.receiveShadows = value; } } /** * Gets the receive shadows flag of the render component. * @returns The receive shadows flag. */ get receiveShadows() { return this._receiveShadows; } static get observedAttributes() { return [...super.observedAttributes, 'cast-shadows', 'material', 'receive-shadows', 'type']; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'cast-shadows': this.castShadows = newValue !== 'false'; break; case 'material': this.material = newValue; break; case 'receive-shadows': this.receiveShadows = newValue !== 'false'; break; case 'type': this.type = newValue; break; } } } customElements.define('pc-render', RenderComponentElement); /** * The RigidBodyComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-rigidbody/ | ``} elements. * The RigidBodyComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class RigidBodyComponentElement extends ComponentElement { /** @ignore */ constructor() { super('rigidbody'); /** * The angular damping of the rigidbody. */ this._angularDamping = 0; /** * The angular factor of the rigidbody. */ this._angularFactor = new Vec3(1, 1, 1); /** * The friction of the rigidbody. */ this._friction = 0.5; /** * The linear damping of the rigidbody. */ this._linearDamping = 0; /** * The linear factor of the rigidbody. */ this._linearFactor = new Vec3(1, 1, 1); /** * The mass of the rigidbody. */ this._mass = 1; /** * The restitution of the rigidbody. */ this._restitution = 0; /** * The rolling friction of the rigidbody. */ this._rollingFriction = 0; /** * The type of the rigidbody. */ this._type = 'static'; } getInitialComponentData() { return { angularDamping: this._angularDamping, angularFactor: this._angularFactor, friction: this._friction, linearDamping: this._linearDamping, linearFactor: this._linearFactor, mass: this._mass, restitution: this._restitution, rollingFriction: this._rollingFriction, type: this._type }; } /** * Gets the underlying PlayCanvas rigidbody component. * @returns The rigidbody component. */ get component() { return super.component; } set angularDamping(value) { this._angularDamping = value; if (this.component) { this.component.angularDamping = value; } } get angularDamping() { return this._angularDamping; } set angularFactor(value) { this._angularFactor = value; if (this.component) { this.component.angularFactor = value; } } get angularFactor() { return this._angularFactor; } set friction(value) { this._friction = value; if (this.component) { this.component.friction = value; } } get friction() { return this._friction; } set linearDamping(value) { this._linearDamping = value; if (this.component) { this.component.linearDamping = value; } } get linearDamping() { return this._linearDamping; } set linearFactor(value) { this._linearFactor = value; if (this.component) { this.component.linearFactor = value; } } get linearFactor() { return this._linearFactor; } set mass(value) { this._mass = value; if (this.component) { this.component.mass = value; } } get mass() { return this._mass; } set restitution(value) { this._restitution = value; if (this.component) { this.component.restitution = value; } } get restitution() { return this._restitution; } set rollingFriction(value) { this._rollingFriction = value; if (this.component) { this.component.rollingFriction = value; } } get rollingFriction() { return this._rollingFriction; } set type(value) { this._type = value; if (this.component) { this.component.type = value; } } get type() { return this._type; } static get observedAttributes() { return [...super.observedAttributes, 'angular-damping', 'angular-factor', 'friction', 'linear-damping', 'linear-factor', 'mass', 'restitution', 'rolling-friction', 'type']; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'angular-damping': this.angularDamping = parseFloat(newValue); break; case 'angular-factor': this.angularFactor = parseVec3(newValue); break; case 'friction': this.friction = parseFloat(newValue); break; case 'linear-damping': this.linearDamping = parseFloat(newValue); break; case 'linear-factor': this.linearFactor = parseVec3(newValue); break; case 'mass': this.mass = parseFloat(newValue); break; case 'restitution': this.restitution = parseFloat(newValue); break; case 'rolling-friction': this.rollingFriction = parseFloat(newValue); break; case 'type': this.type = newValue; break; } } } customElements.define('pc-rigidbody', RigidBodyComponentElement); /** * The ScreenComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-screen/ | ``} elements. * The ScreenComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class ScreenComponentElement extends ComponentElement { /** @ignore */ constructor() { super('screen'); this._screenSpace = false; this._resolution = new Vec2(640, 320); this._referenceResolution = new Vec2(640, 320); this._priority = 0; this._blend = false; this._scaleBlend = 0.5; } getInitialComponentData() { return { priority: this._priority, referenceResolution: this._referenceResolution, resolution: this._resolution, scaleBlend: this._scaleBlend, scaleMode: this._blend ? SCALEMODE_BLEND : SCALEMODE_NONE, screenSpace: this._screenSpace }; } /** * Gets the underlying PlayCanvas screen component. * @returns The screen component. */ get component() { return super.component; } set priority(value) { this._priority = value; if (this.component) { this.component.priority = this._priority; } } get priority() { return this._priority; } set referenceResolution(value) { this._referenceResolution = value; if (this.component) { this.component.referenceResolution = this._referenceResolution; } } get referenceResolution() { return this._referenceResolution; } set resolution(value) { this._resolution = value; if (this.component) { this.component.resolution = this._resolution; } } get resolution() { return this._resolution; } set scaleBlend(value) { this._scaleBlend = value; if (this.component) { this.component.scaleBlend = this._scaleBlend; } } get scaleBlend() { return this._scaleBlend; } set blend(value) { this._blend = value; if (this.component) { this.component.scaleMode = this._blend ? SCALEMODE_BLEND : SCALEMODE_NONE; } } get blend() { return this._blend; } set screenSpace(value) { this._screenSpace = value; if (this.component) { this.component.screenSpace = this._screenSpace; } } get screenSpace() { return this._screenSpace; } static get observedAttributes() { return [ ...super.observedAttributes, 'blend', 'screen-space', 'resolution', 'reference-resolution', 'priority', 'scale-blend' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'priority': this.priority = parseInt(newValue, 10); break; case 'reference-resolution': this.referenceResolution = parseVec2(newValue); break; case 'resolution': this.resolution = parseVec2(newValue); break; case 'scale-blend': this.scaleBlend = parseFloat(newValue); break; case 'blend': this.blend = this.hasAttribute('blend'); break; case 'screen-space': this.screenSpace = this.hasAttribute('screen-space'); break; } } } customElements.define('pc-screen', ScreenComponentElement); /** * The ScriptComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scripts/ | ``} elements. * The ScriptComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class ScriptComponentElement extends ComponentElement { /** @ignore */ constructor() { super('script'); // Create mutation observer to watch for child script elements this.observer = new MutationObserver(this.handleMutations.bind(this)); this.observer.observe(this, { childList: true }); // Listen for script attribute and enable changes this.addEventListener('scriptattributeschange', this.handleScriptAttributesChange.bind(this)); this.addEventListener('scriptenablechange', this.handleScriptEnableChange.bind(this)); } initComponent() { // Handle initial script elements this.querySelectorAll(':scope > pc-script').forEach((scriptElement) => { const scriptName = scriptElement.getAttribute('name'); const attributes = scriptElement.getAttribute('attributes'); if (scriptName) { this.createScript(scriptName, attributes); } }); } /** * Recursively converts raw attribute data into proper PlayCanvas types. Supported conversions: * - "asset:assetId" → resolves to an Asset instance * - "entity:entityId" → resolves to an Entity instance * - "vec2:1,2" → new Vec2(1,2) * - "vec3:1,2,3" → new Vec3(1,2,3) * - "vec4:1,2,3,4" → new Vec4(1,2,3,4) * - "color:1,0.5,0.5,1" → new Color(1,0.5,0.5,1) * @param item - The item to convert. * @returns The converted item. */ convertAttributes(item) { if (typeof item === 'string') { if (item.startsWith('asset:')) { const assetId = item.slice(6); const assetElement = document.querySelector(`pc-asset#${assetId}`); if (assetElement) { return assetElement.asset; } } if (item.startsWith('entity:')) { const entityId = item.slice(7); const entityElement = document.querySelector(`pc-entity[name="${entityId}"]`); if (entityElement) { return entityElement.entity; } } if (item.startsWith('vec2:')) { const parts = item.slice(5).split(',').map(Number); if (parts.length === 2 && parts.every(v => !isNaN(v))) { return new Vec2(parts[0], parts[1]); } } if (item.startsWith('vec3:')) { const parts = item.slice(5).split(',').map(Number); if (parts.length === 3 && parts.every(v => !isNaN(v))) { return new Vec3(parts[0], parts[1], parts[2]); } } if (item.startsWith('vec4:')) { const parts = item.slice(5).split(',').map(Number); if (parts.length === 4 && parts.every(v => !isNaN(v))) { return new Vec4(parts[0], parts[1], parts[2], parts[3]); } } if (item.startsWith('color:')) { const parts = item.slice(6).split(',').map(Number); if (parts.length === 4 && parts.every(v => !isNaN(v))) { return new Color(parts[0], parts[1], parts[2], parts[3]); } } return item; } if (Array.isArray(item)) { // If it's an array of objects, convert each element individually. if (item.length > 0 && typeof item[0] === 'object') { return item.map((el) => this.convertAttributes(el)); } // Otherwise, leave the numeric array unchanged but process each element. return item.map((el) => this.convertAttributes(el)); } if (item && typeof item === 'object') { const result = {}; for (const key in item) { result[key] = this.convertAttributes(item[key]); } return result; } return item; } /** * Preprocess the attributes object by converting its values. * @param attrs - The attributes object to preprocess. * @returns The preprocessed attributes object. */ preprocessAttributes(attrs) { return this.convertAttributes(attrs); } /** * Recursively merge properties from source into target. * @param target - The target object to merge into. * @param source - The source object to merge from. * @returns The merged object. */ mergeDeep(target, source) { for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; } this.mergeDeep(target[key], source[key]); } else { target[key] = source[key]; } } return target; } /** * Update script attributes by merging preprocessed values into the script. * @param script - The script to update. * @param attributes - The attributes to merge into the script. */ applyAttributes(script, attributes) { try { const attributesObject = attributes ? JSON.parse(attributes) : {}; const converted = this.convertAttributes(attributesObject); this.mergeDeep(script, converted); } catch (error) { console.error(`Error parsing attributes JSON string ${attributes}:`, error); } } handleScriptAttributesChange(event) { const scriptElement = event.target; const scriptName = scriptElement.getAttribute('name'); if (!scriptName || !this.component) return; const script = this.component.get(scriptName); if (script) { this.applyAttributes(script, event.detail.attributes); } } handleScriptEnableChange(event) { const scriptElement = event.target; const scriptName = scriptElement.getAttribute('name'); if (!scriptName || !this.component) return; const script = this.component.get(scriptName); if (script) { script.enabled = event.detail.enabled; } } createScript(name, attributes) { if (!this.component) return null; let attributesObject = {}; if (attributes) { try { attributesObject = JSON.parse(attributes); // Preprocess attributes: convert arrays or strings into vectors, colors, asset references, etc. attributesObject = this.preprocessAttributes(attributesObject); } catch (error) { console.error(`Error parsing attributes JSON string ${attributes}:`, error); } } return this.component.create(name, { properties: attributesObject }); } destroyScript(name) { if (!this.component) return; this.component.destroy(name); } handleMutations(mutations) { for (const mutation of mutations) { // Handle added nodes mutation.addedNodes.forEach((node) => { if (node instanceof HTMLElement && node.tagName.toLowerCase() === 'pc-script') { const scriptName = node.getAttribute('name'); const attributes = node.getAttribute('attributes'); if (scriptName) { this.createScript(scriptName, attributes); } } }); // Handle removed nodes mutation.removedNodes.forEach((node) => { if (node instanceof HTMLElement && node.tagName.toLowerCase() === 'pc-script') { const scriptName = node.getAttribute('name'); if (scriptName) { this.destroyScript(scriptName); } } }); } } disconnectedCallback() { var _a; this.observer.disconnect(); (_a = super.disconnectedCallback) === null || _a === void 0 ? void 0 : _a.call(this); } /** * Gets the underlying PlayCanvas script component. * @returns The script component. */ get component() { return super.component; } } customElements.define('pc-scripts', ScriptComponentElement); /** * The ScriptElement interface provides properties and methods for manipulating * `` elements. The ScriptElement interface also inherits the properties and * methods of the {@link HTMLElement} interface. */ class ScriptElement extends HTMLElement { constructor() { super(...arguments); this._attributes = '{}'; this._enabled = true; this._name = ''; } /** * Sets the attributes of the script. * @param value - The attributes of the script. */ set scriptAttributes(value) { this._attributes = value; this.dispatchEvent(new CustomEvent('scriptattributeschange', { detail: { attributes: value }, bubbles: true })); } /** * Gets the attributes of the script. * @returns The attributes of the script. */ get scriptAttributes() { return this._attributes; } /** * Sets the enabled state of the script. * @param value - The enabled state of the script. */ set enabled(value) { this._enabled = value; this.dispatchEvent(new CustomEvent('scriptenablechange', { detail: { enabled: value }, bubbles: true })); } /** * Gets the enabled state of the script. * @returns The enabled state of the script. */ get enabled() { return this._enabled; } /** * Sets the name of the script to create. * @param value - The name. */ set name(value) { this._name = value; } /** * Gets the name of the script. * @returns The name. */ get name() { return this._name; } static get observedAttributes() { return ['attributes', 'enabled', 'name']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'attributes': this.scriptAttributes = newValue; break; case 'enabled': this.enabled = newValue !== 'false'; break; case 'name': this.name = newValue; break; } } } customElements.define('pc-script', ScriptElement); /** * The SoundComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-sounds/ | ``} elements. * The SoundComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class SoundComponentElement extends ComponentElement { /** @ignore */ constructor() { super('sound'); this._distanceModel = 'linear'; this._maxDistance = 10000; this._pitch = 1; this._positional = false; this._refDistance = 1; this._rollOffFactor = 1; this._volume = 1; } getInitialComponentData() { return { distanceModel: this._distanceModel, maxDistance: this._maxDistance, pitch: this._pitch, positional: this._positional, refDistance: this._refDistance, rollOffFactor: this._rollOffFactor, volume: this._volume }; } /** * Gets the underlying PlayCanvas sound component. * @returns The sound component. */ get component() { return super.component; } /** * Sets which algorithm to use to reduce the volume of the sound as it moves away from the listener. * @param value - The distance model. */ set distanceModel(value) { this._distanceModel = value; if (this.component) { this.component.distanceModel = value; } } /** * Gets which algorithm to use to reduce the volume of the sound as it moves away from the listener. * @returns The distance model. */ get distanceModel() { return this._distanceModel; } /** * Sets the maximum distance from the listener at which audio falloff stops. * @param value - The max distance. */ set maxDistance(value) { this._maxDistance = value; if (this.component) { this.component.maxDistance = value; } } /** * Gets the maximum distance from the listener at which audio falloff stops. * @returns The max distance. */ get maxDistance() { return this._maxDistance; } /** * Sets the pitch of the sound. * @param value - The pitch. */ set pitch(value) { this._pitch = value; if (this.component) { this.component.pitch = value; } } /** * Gets the pitch of the sound. * @returns The pitch. */ get pitch() { return this._pitch; } /** * Sets the positional flag of the sound. * @param value - The positional flag. */ set positional(value) { this._positional = value; if (this.component) { this.component.positional = value; } } /** * Gets the positional flag of the sound. * @returns The positional flag. */ get positional() { return this._positional; } /** * Sets the reference distance for reducing volume as the sound source moves further from the listener. Defaults to 1. * @param value - The ref distance. */ set refDistance(value) { this._refDistance = value; if (this.component) { this.component.refDistance = value; } } /** * Gets the reference distance for reducing volume as the sound source moves further from the listener. * @returns The ref distance. */ get refDistance() { return this._refDistance; } /** * Sets the factor used in the falloff equation. Defaults to 1. * @param value - The roll-off factor. */ set rollOffFactor(value) { this._rollOffFactor = value; if (this.component) { this.component.rollOffFactor = value; } } /** * Gets the factor used in the falloff equation. * @returns The roll-off factor. */ get rollOffFactor() { return this._rollOffFactor; } /** * Sets the volume of the sound. * @param value - The volume. */ set volume(value) { this._volume = value; if (this.component) { this.component.volume = value; } } /** * Gets the volume of the sound. * @returns The volume. */ get volume() { return this._volume; } static get observedAttributes() { return [ ...super.observedAttributes, 'distance-model', 'max-distance', 'pitch', 'positional', 'ref-distance', 'roll-off-factor', 'volume' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'distance-model': this.distanceModel = newValue; break; case 'max-distance': this.maxDistance = parseFloat(newValue); break; case 'pitch': this.pitch = parseFloat(newValue); break; case 'positional': this.positional = this.hasAttribute('positional'); break; case 'ref-distance': this.refDistance = parseFloat(newValue); break; case 'roll-off-factor': this.rollOffFactor = parseFloat(newValue); break; case 'volume': this.volume = parseFloat(newValue); break; } } } customElements.define('pc-sounds', SoundComponentElement); /** * The SoundSlotElement interface provides properties and methods for manipulating * `` elements. The SoundSlotElement interface also inherits the properties and * methods of the {@link AsyncElement} interface. */ class SoundSlotElement extends AsyncElement { constructor() { super(...arguments); this._asset = ''; this._autoPlay = false; this._duration = null; this._loop = false; this._name = ''; this._overlap = false; this._pitch = 1; this._startTime = 0; this._volume = 1; /** * The sound slot. */ this.soundSlot = null; } async connectedCallback() { var _a; await ((_a = this.soundElement) === null || _a === void 0 ? void 0 : _a.ready()); const options = { autoPlay: this._autoPlay, loop: this._loop, overlap: this._overlap, pitch: this._pitch, startTime: this._startTime, volume: this._volume }; if (this._duration) { options.duration = this._duration; } this.soundSlot = this.soundElement.component.addSlot(this._name, options); this.asset = this._asset; if (this._autoPlay) { this.soundSlot.play(); } this._onReady(); } disconnectedCallback() { this.soundElement.component.removeSlot(this._name); } get soundElement() { const soundElement = this.parentElement; if (!(soundElement instanceof SoundComponentElement)) { console.warn('pc-sound-slot must be a direct child of a pc-sound element'); return null; } return soundElement; } /** * Sets the id of the `pc-asset` to use for the sound slot. * @param value - The asset. */ set asset(value) { var _a; this._asset = value; if (this.soundSlot) { const id = (_a = AssetElement.get(value)) === null || _a === void 0 ? void 0 : _a.id; if (id) { this.soundSlot.asset = id; } } } /** * Gets the id of the `pc-asset` to use for the sound slot. * @returns The asset. */ get asset() { return this._asset; } /** * Sets the auto play flag of the sound slot. * @param value - The auto play flag. */ set autoPlay(value) { this._autoPlay = value; if (this.soundSlot) { this.soundSlot.autoPlay = value; } } /** * Gets the auto play flag of the sound slot. * @returns The auto play flag. */ get autoPlay() { return this._autoPlay; } /** * Sets the duration of the sound slot. * @param value - The duration. */ set duration(value) { this._duration = value; if (this.soundSlot) { this.soundSlot.duration = value; } } /** * Gets the duration of the sound slot. * @returns The duration. */ get duration() { return this._duration; } /** * Sets the loop flag of the sound slot. * @param value - The loop flag. */ set loop(value) { this._loop = value; if (this.soundSlot) { this.soundSlot.loop = value; } } /** * Gets the loop flag of the sound slot. * @returns The loop flag. */ get loop() { return this._loop; } /** * Sets the name of the sound slot. * @param value - The name. */ set name(value) { this._name = value; if (this.soundSlot) { this.soundSlot.name = value; } } /** * Gets the name of the sound slot. * @returns The name. */ get name() { return this._name; } /** * Sets the overlap flag of the sound slot. * @param value - The overlap flag. */ set overlap(value) { this._overlap = value; if (this.soundSlot) { this.soundSlot.overlap = value; } } /** * Gets the overlap flag of the sound slot. * @returns The overlap flag. */ get overlap() { return this._overlap; } /** * Sets the pitch of the sound slot. * @param value - The pitch. */ set pitch(value) { this._pitch = value; if (this.soundSlot) { this.soundSlot.pitch = value; } } /** * Gets the pitch of the sound slot. * @returns The pitch. */ get pitch() { return this._pitch; } /** * Sets the start time of the sound slot. * @param value - The start time. */ set startTime(value) { this._startTime = value; if (this.soundSlot) { this.soundSlot.startTime = value; } } /** * Gets the start time of the sound slot. * @returns The start time. */ get startTime() { return this._startTime; } /** * Sets the volume of the sound slot. * @param value - The volume. */ set volume(value) { this._volume = value; if (this.soundSlot) { this.soundSlot.volume = value; } } /** * Gets the volume of the sound slot. * @returns The volume. */ get volume() { return this._volume; } static get observedAttributes() { return ['asset', 'auto-play', 'duration', 'loop', 'name', 'overlap', 'pitch', 'start-time', 'volume']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'asset': this.asset = newValue; break; case 'auto-play': this.autoPlay = this.hasAttribute('auto-play'); break; case 'duration': this.duration = parseFloat(newValue); break; case 'loop': this.loop = this.hasAttribute('loop'); break; case 'name': this.name = newValue; break; case 'overlap': this.overlap = this.hasAttribute('overlap'); break; case 'pitch': this.pitch = parseFloat(newValue); break; case 'start-time': this.startTime = parseFloat(newValue); break; case 'volume': this.volume = parseFloat(newValue); break; } } } customElements.define('pc-sound', SoundSlotElement); /** * The SplatComponentElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-splat/ | ``} elements. * The SplatComponentElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. * * @category Components */ class SplatComponentElement extends ComponentElement { /** @ignore */ constructor() { super('gsplat'); this._asset = ''; this._castShadows = false; this._unified = false; } getInitialComponentData() { return { asset: AssetElement.get(this._asset), castShadows: this._castShadows, unified: this._unified }; } /** * Gets the underlying PlayCanvas splat component. * @returns The splat component. */ get component() { return super.component; } /** * Sets id of the `pc-asset` to use for the splat. * @param value - The asset ID. */ set asset(value) { this._asset = value; const asset = AssetElement.get(value); if (this.component && asset) { this.component.asset = asset; } } /** * Gets the id of the `pc-asset` to use for the splat. * @returns The asset ID. */ get asset() { return this._asset; } /** * Sets whether the splat casts shadows. * @param value - Whether the splat casts shadows. */ set castShadows(value) { this._castShadows = value; if (this.component) { this.component.castShadows = value; } } /** * Gets whether the splat casts shadows. * @returns Whether the splat casts shadows. */ get castShadows() { return this._castShadows; } /** * Sets whether the splat supports global sorting and LOD streaming. This property can only be * changed when the component is disabled. * @param value - Whether the splat supports global sorting and LOD streaming. */ set unified(value) { if (this.component && this.component.enabled) { console.warn('The "unified" property can only be changed when the component is disabled.'); return; } this._unified = value; if (this.component) { this.component.unified = value; } } /** * Gets whether the splat supports global sorting and LOD streaming. * @returns Whether the splat supports global sorting and LOD streaming. */ get unified() { return this._unified; } static get observedAttributes() { return [ ...super.observedAttributes, 'asset', 'cast-shadows', 'unified' ]; } attributeChangedCallback(name, _oldValue, newValue) { super.attributeChangedCallback(name, _oldValue, newValue); switch (name) { case 'asset': this.asset = newValue; break; case 'cast-shadows': this.castShadows = this.hasAttribute('cast-shadows'); break; case 'unified': this.unified = this.hasAttribute('unified'); break; } } } customElements.define('pc-splat', SplatComponentElement); /** * The ModelElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-model/ | ``} elements. * The ModelElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class ModelElement extends AsyncElement { constructor() { super(...arguments); this._asset = ''; this._entity = null; } connectedCallback() { this._loadModel(); this._onReady(); } disconnectedCallback() { this._unloadModel(); } _instantiate(container) { this._entity = container.instantiateRenderEntity(); // @ts-ignore if (container.animations.length > 0) { this._entity.addComponent('anim'); // @ts-ignore this._entity.anim.assignAnimation('animation', container.animations[0].resource); } const parentEntityElement = this.closestEntity; if (parentEntityElement) { parentEntityElement.ready().then(() => { parentEntityElement.entity.addChild(this._entity); }); } else { const appElement = this.closestApp; if (appElement) { appElement.ready().then(() => { appElement.app.root.addChild(this._entity); }); } } } async _loadModel() { var _a; this._unloadModel(); const appElement = await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready()); const app = appElement === null || appElement === void 0 ? void 0 : appElement.app; const asset = AssetElement.get(this._asset); if (!asset) { return; } if (asset.loaded) { this._instantiate(asset.resource); } else { asset.once('load', () => { this._instantiate(asset.resource); }); app.assets.load(asset); } } _unloadModel() { var _a; (_a = this._entity) === null || _a === void 0 ? void 0 : _a.destroy(); this._entity = null; } /** * Sets the id of the `pc-asset` to use for the model. * @param value - The asset ID. */ set asset(value) { this._asset = value; if (this.isConnected) { this._loadModel(); } } /** * Gets the id of the `pc-asset` to use for the model. * @returns The asset ID. */ get asset() { return this._asset; } static get observedAttributes() { return ['asset']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'asset': this.asset = newValue; break; } } } customElements.define('pc-model', ModelElement); /** * The SceneElement interface provides properties and methods for manipulating * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-scene/ | ``} elements. * The SceneElement interface also inherits the properties and methods of the * {@link HTMLElement} interface. */ class SceneElement extends AsyncElement { constructor() { super(...arguments); /** * The fog type of the scene. */ this._fog = 'none'; // possible values: 'none', 'linear', 'exp', 'exp2' /** * The color of the fog. */ this._fogColor = new Color(1, 1, 1); /** * The density of the fog. */ this._fogDensity = 0; /** * The start distance of the fog. */ this._fogStart = 0; /** * The end distance of the fog. */ this._fogEnd = 1000; /** * The gravity of the scene. */ this._gravity = new Vec3(0, -9.81, 0); /** * The PlayCanvas scene instance. */ this.scene = null; } async connectedCallback() { var _a; await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready()); this.scene = this.closestApp.app.scene; this.updateSceneSettings(); this._onReady(); } updateSceneSettings() { if (this.scene) { this.scene.fog.type = this._fog; this.scene.fog.color = this._fogColor; this.scene.fog.density = this._fogDensity; this.scene.fog.start = this._fogStart; this.scene.fog.end = this._fogEnd; const appElement = this.parentElement; appElement.app.systems.rigidbody.gravity.copy(this._gravity); } } /** * Sets the fog type of the scene. * @param value - The fog type. */ set fog(value) { this._fog = value; if (this.scene) { this.scene.fog.type = value; } } /** * Gets the fog type of the scene. * @returns The fog type. */ get fog() { return this._fog; } /** * Sets the fog color of the scene. * @param value - The fog color. */ set fogColor(value) { this._fogColor = value; if (this.scene) { this.scene.fog.color = value; } } /** * Gets the fog color of the scene. * @returns The fog color. */ get fogColor() { return this._fogColor; } /** * Sets the fog density of the scene. * @param value - The fog density. */ set fogDensity(value) { this._fogDensity = value; if (this.scene) { this.scene.fog.density = value; } } /** * Gets the fog density of the scene. * @returns The fog density. */ get fogDensity() { return this._fogDensity; } /** * Sets the fog start distance of the scene. * @param value - The fog start distance. */ set fogStart(value) { this._fogStart = value; if (this.scene) { this.scene.fog.start = value; } } /** * Gets the fog start distance of the scene. * @returns The fog start distance. */ get fogStart() { return this._fogStart; } /** * Sets the fog end distance of the scene. * @param value - The fog end distance. */ set fogEnd(value) { this._fogEnd = value; if (this.scene) { this.scene.fog.end = value; } } /** * Gets the fog end distance of the scene. * @returns The fog end distance. */ get fogEnd() { return this._fogEnd; } /** * Sets the gravity of the scene. * @param value - The gravity. */ set gravity(value) { this._gravity = value; if (this.scene) { const appElement = this.parentElement; appElement.app.systems.rigidbody.gravity.copy(value); } } /** * Gets the gravity of the scene. * @returns The gravity. */ get gravity() { return this._gravity; } static get observedAttributes() { return ['fog', 'fog-color', 'fog-density', 'fog-start', 'fog-end', 'gravity']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'fog': this.fog = newValue; break; case 'fog-color': this.fogColor = parseColor(newValue); break; case 'fog-density': this.fogDensity = parseFloat(newValue); break; case 'fog-start': this.fogStart = parseFloat(newValue); break; case 'fog-end': this.fogEnd = parseFloat(newValue); break; case 'gravity': this.gravity = parseVec3(newValue); break; // ... handle other attributes as well } } } customElements.define('pc-scene', SceneElement); /** * The SkyElement interface provides properties and methods for manipulating * `` elements. The SkyElement interface also inherits the properties and * methods of the {@link HTMLElement} interface. */ class SkyElement extends AsyncElement { constructor() { super(...arguments); this._asset = ''; this._center = new Vec3(0, 0.01, 0); this._intensity = 1; this._rotation = new Vec3(); this._level = 0; this._lighting = false; this._scale = new Vec3(100, 100, 100); this._type = 'infinite'; this._scene = null; } connectedCallback() { this._loadSkybox(); this._onReady(); } disconnectedCallback() { this._unloadSkybox(); } _generateSkybox(asset) { if (!this._scene) return; const source = asset.resource; const skybox = EnvLighting.generateSkyboxCubemap(source); skybox.anisotropy = 4; this._scene.skybox = skybox; if (this._lighting) { const lighting = EnvLighting.generateLightingSource(source); const envAtlas = EnvLighting.generateAtlas(lighting); this._scene.envAtlas = envAtlas; } const layer = this._scene.layers.getLayerById(LAYERID_SKYBOX); if (layer) { layer.enabled = this._type !== 'none'; } this._scene.sky.type = this._type; this._scene.sky.node.setLocalScale(this._scale); this._scene.sky.center = this._center; this._scene.skyboxIntensity = this._intensity; this._scene.skyboxMip = this._level; } async _loadSkybox() { var _a; const appElement = await ((_a = this.closestApp) === null || _a === void 0 ? void 0 : _a.ready()); const app = appElement === null || appElement === void 0 ? void 0 : appElement.app; if (!app) { return; } const asset = AssetElement.get(this._asset); if (!asset) { return; } this._scene = app.scene; if (asset.loaded) { this._generateSkybox(asset); } else { asset.once('load', () => { this._generateSkybox(asset); }); app.assets.load(asset); } } _unloadSkybox() { var _a, _b; if (!this._scene) return; (_a = this._scene.skybox) === null || _a === void 0 ? void 0 : _a.destroy(); // @ts-ignore this._scene.skybox = null; (_b = this._scene.envAtlas) === null || _b === void 0 ? void 0 : _b.destroy(); // @ts-ignore this._scene.envAtlas = null; this._scene = null; } /** * Sets the id of the `pc-asset` to use for the skybox. * @param value - The asset ID. */ set asset(value) { this._asset = value; if (this.isConnected) { this._loadSkybox(); } } /** * Gets the id of the `pc-asset` to use for the skybox. * @returns The asset ID. */ get asset() { return this._asset; } /** * Sets the center of the skybox. * @param value - The center. */ set center(value) { this._center = value; if (this._scene) { this._scene.sky.center = this._center; } } /** * Gets the center of the skybox. * @returns The center. */ get center() { return this._center; } /** * Sets the intensity of the skybox. * @param value - The intensity. */ set intensity(value) { this._intensity = value; if (this._scene) { this._scene.skyboxIntensity = this._intensity; } } /** * Gets the intensity of the skybox. * @returns The intensity. */ get intensity() { return this._intensity; } /** * Sets the mip level of the skybox. * @param value - The mip level. */ set level(value) { this._level = value; if (this._scene) { this._scene.skyboxMip = this._level; } } /** * Gets the mip level of the skybox. * @returns The mip level. */ get level() { return this._level; } /** * Sets whether the skybox is used as a light source. * @param value - Whether to use lighting. */ set lighting(value) { this._lighting = value; } /** * Gets whether the skybox is used as a light source. * @returns Whether to use lighting. */ get lighting() { return this._lighting; } /** * Sets the Euler rotation of the skybox. * @param value - The rotation. */ set rotation(value) { this._rotation = value; if (this._scene) { this._scene.skyboxRotation = new Quat().setFromEulerAngles(value); } } /** * Gets the Euler rotation of the skybox. * @returns The rotation. */ get rotation() { return this._rotation; } /** * Sets the scale of the skybox. * @param value - The scale. */ set scale(value) { this._scale = value; if (this._scene) { this._scene.sky.node.setLocalScale(this._scale); } } /** * Gets the scale of the skybox. * @returns The scale. */ get scale() { return this._scale; } /** * Sets the type of the skybox. * @param value - The type. */ set type(value) { this._type = value; if (this._scene) { this._scene.sky.type = this._type; const layer = this._scene.layers.getLayerById(LAYERID_SKYBOX); if (layer) { layer.enabled = this._type !== 'none'; } } } /** * Gets the type of the skybox. * @returns The type. */ get type() { return this._type; } static get observedAttributes() { return ['asset', 'center', 'intensity', 'level', 'lighting', 'rotation', 'scale', 'type']; } attributeChangedCallback(name, _oldValue, newValue) { switch (name) { case 'asset': this.asset = newValue; break; case 'center': this.center = parseVec3(newValue); break; case 'intensity': this.intensity = parseFloat(newValue); break; case 'level': this.level = parseInt(newValue, 10); break; case 'lighting': this.lighting = this.hasAttribute(name); break; case 'rotation': this.rotation = parseVec3(newValue); break; case 'scale': this.scale = parseVec3(newValue); break; case 'type': this.type = newValue; break; } } } customElements.define('pc-sky', SkyElement); // creates an observer proxy object to wrap some target object. fires events when properties change. const observe = (events, target) => { const members = new Set(Object.keys(target)); return new Proxy(target, { set(target, property, value, receiver) { // prevent setting symbol properties if (typeof property === 'symbol') { console.error('Cannot set symbol property on target'); return false; } // not allowed to set a new value on target if (!members.has(property)) { console.error('Cannot set new property on target'); return false; } // set and fire event if value changed if (target[property] !== value) { const prev = target[property]; target[property] = value; events.fire(`${property}:changed`, value, prev); } return true; } }); }; const migrateV1 = (settings) => { if (settings.animTracks) { settings.animTracks?.forEach((track) => { // some early settings did not have frameRate set on anim tracks if (!track.frameRate) { const defaultFrameRate = 30; track.frameRate = defaultFrameRate; const times = track.keyframes.times; for (let i = 0; i < times.length; i++) { times[i] *= defaultFrameRate; } } // smoothness property added in v1.4.0 if (!track.hasOwnProperty('smoothness')) { track.smoothness = 0; } }); } else { // some scenes were published without animTracks settings.animTracks = []; } return settings; }; const migrateAnimTrackV2 = (animTrackV1, fov) => { return { name: animTrackV1.name, duration: animTrackV1.duration, frameRate: animTrackV1.frameRate, loopMode: animTrackV1.loopMode, interpolation: animTrackV1.interpolation, smoothness: animTrackV1.smoothness, keyframes: { times: animTrackV1.keyframes.times, values: { position: animTrackV1.keyframes.values.position, target: animTrackV1.keyframes.values.target, fov: new Array(animTrackV1.keyframes.times.length).fill(fov) } } }; }; const migrateV2 = (v1) => { return { version: 2, tonemapping: 'none', highPrecisionRendering: false, background: { color: v1.background.color || [0, 0, 0] }, postEffectSettings: { sharpness: { enabled: false, amount: 0 }, bloom: { enabled: false, intensity: 1, blurLevel: 2 }, grading: { enabled: false, brightness: 0, contrast: 1, saturation: 1, tint: [1, 1, 1] }, vignette: { enabled: false, intensity: 0.5, inner: 0.3, outer: 0.75, curvature: 1 }, fringing: { enabled: false, intensity: 0.5 } }, animTracks: v1.animTracks.map((animTrackV1) => { return migrateAnimTrackV2(animTrackV1, v1.camera.fov || 60); }), cameras: [{ initial: { position: (v1.camera.position || [0, 0, 5]), target: (v1.camera.target || [0, 0, 0]), fov: v1.camera.fov || 75 } }], annotations: [], startMode: v1.camera.startAnim === 'animTrack' ? 'animTrack' : 'default', hasStartPose: !!(v1.camera.position && v1.camera.target) }; }; // import a json object to conform to the latest settings schema. settings is assumed to be one of the well-formed schemas const importSettings = (settings) => { let result; const version = settings.version; if (version === undefined) { // v1 -> v2 result = migrateV2(migrateV1(settings)); } else if (version === 2) { // already v2 result = settings; } else { throw new Error(`Unsupported experience settings version: ${version}`); } return result; }; class Tooltip { register; unregister; destroy; constructor(dom) { const { style } = dom; style.display = 'none'; const targets = new Map(); let timer = 0; this.register = (target, textString, direction = 'bottom') => { const activate = () => { const rect = target.getBoundingClientRect(); const midx = Math.floor((rect.left + rect.right) * 0.5); const midy = Math.floor((rect.top + rect.bottom) * 0.5); switch (direction) { case 'left': style.left = `${rect.left}px`; style.top = `${midy}px`; style.transform = 'translate(calc(-100% - 10px), -50%)'; break; case 'right': style.left = `${rect.right}px`; style.top = `${midy}px`; style.transform = 'translate(10px, -50%)'; break; case 'top': style.left = `${midx}px`; style.top = `${rect.top}px`; style.transform = 'translate(-50%, calc(-100% - 10px))'; break; case 'bottom': style.left = `${midx}px`; style.top = `${rect.bottom}px`; style.transform = 'translate(-50%, 10px)'; break; } dom.textContent = textString; style.display = 'inline'; }; const startTimer = (fn) => { timer = window.setTimeout(() => { fn(); timer = -1; }, 250); }; const cancelTimer = () => { if (timer >= 0) { clearTimeout(timer); timer = -1; } }; const enter = () => { cancelTimer(); if (style.display === 'inline') { activate(); } else { startTimer(() => activate()); } }; const leave = () => { cancelTimer(); if (style.display === 'inline') { startTimer(() => { style.display = 'none'; }); } }; target.addEventListener('pointerenter', enter); target.addEventListener('pointerleave', leave); targets.set(target, { enter, leave }); }; this.unregister = (target) => { const value = targets.get(target); if (value) { target.removeEventListener('pointerenter', value.enter); target.removeEventListener('pointerleave', value.leave); targets.delete(target); } }; this.destroy = () => { for (const target of targets.keys()) { this.unregister(target); } }; } } const v = new Vec3(); // update the poster image to start blurry and then resolve to sharp during loading const initPoster = (events) => { const poster = document.getElementById('poster'); events.on('firstFrame', () => { poster.style.display = 'none'; document.documentElement.style.setProperty('--canvas-opacity', '1'); }); const blur = (progress) => { poster.style.filter = `blur(${Math.floor((100 - progress) * 0.4)}px)`; }; events.on('progress:changed', blur); }; const initUI = (global) => { const { config, events, state } = global; // Acquire Elements const docRoot = document.documentElement; const dom = [ 'ui', 'controlsWrap', 'arMode', 'vrMode', 'enterFullscreen', 'exitFullscreen', 'info', 'infoPanel', 'desktopTab', 'touchTab', 'desktopInfoPanel', 'touchInfoPanel', 'timelineContainer', 'handle', 'time', 'buttonContainer', 'play', 'pause', 'settings', 'settingsPanel', 'orbitCamera', 'flyCamera', 'hqCheck', 'hqOption', 'lqCheck', 'lqOption', 'reset', 'frame', 'screenshot', 'bgHue', 'bgSaturation', 'bgLightness', 'fov', 'bgHueValue', 'bgSaturationValue', 'bgLightnessValue', 'fovValue', 'loadingText', 'loadingBar', 'joystickBase', 'joystick', 'tooltip' ].reduce((acc, id) => { acc[id] = document.getElementById(id); return acc; }, {}); // Handle loading progress updates events.on('progress:changed', (progress) => { dom.loadingText.textContent = `${progress}%`; if (progress < 100) { dom.loadingBar.style.backgroundImage = `linear-gradient(90deg, #F60 0%, #F60 ${progress}%, white ${progress}%, white 100%)`; } else { dom.loadingBar.style.backgroundImage = 'linear-gradient(90deg, #F60 0%, #F60 100%)'; } }); // Hide loading bar once first frame is rendered events.on('firstFrame', () => { document.getElementById('loadingWrap').classList.add('hidden'); }); // Fullscreen support const hasFullscreenAPI = docRoot.requestFullscreen && document.exitFullscreen; const requestFullscreen = () => { if (hasFullscreenAPI) { docRoot.requestFullscreen(); } else { window.parent.postMessage('requestFullscreen', '*'); state.isFullscreen = true; } }; const exitFullscreen = () => { if (hasFullscreenAPI) { document.exitFullscreen(); } else { window.parent.postMessage('exitFullscreen', '*'); state.isFullscreen = false; } }; if (hasFullscreenAPI) { document.addEventListener('fullscreenchange', () => { state.isFullscreen = !!document.fullscreenElement; }); } dom.enterFullscreen.addEventListener('click', requestFullscreen); dom.exitFullscreen.addEventListener('click', exitFullscreen); // toggle fullscreen when user switches between landscape portrait // orientation screen?.orientation?.addEventListener('change', (event) => { if (['landscape-primary', 'landscape-secondary'].includes(screen.orientation.type)) { requestFullscreen(); } else { exitFullscreen(); } }); // update UI when fullscreen state changes events.on('isFullscreen:changed', (value) => { dom.enterFullscreen.classList[value ? 'add' : 'remove']('hidden'); dom.exitFullscreen.classList[value ? 'remove' : 'add']('hidden'); }); // HQ mode dom.hqOption.addEventListener('click', () => { state.hqMode = true; }); dom.lqOption.addEventListener('click', () => { state.hqMode = false; }); const updateHQ = () => { dom.hqCheck.classList[state.hqMode ? 'add' : 'remove']('active'); dom.lqCheck.classList[state.hqMode ? 'remove' : 'add']('active'); }; events.on('hqMode:changed', (value) => { updateHQ(); }); updateHQ(); // AR/VR const arChanged = () => dom.arMode.classList[state.hasAR ? 'remove' : 'add']('hidden'); const vrChanged = () => dom.vrMode.classList[state.hasVR ? 'remove' : 'add']('hidden'); dom.arMode.addEventListener('click', () => events.fire('startAR')); dom.vrMode.addEventListener('click', () => events.fire('startVR')); events.on('hasAR:changed', arChanged); events.on('hasVR:changed', vrChanged); arChanged(); vrChanged(); // Info panel const updateInfoTab = (tab) => { if (tab === 'desktop') { dom.desktopTab.classList.add('active'); dom.touchTab.classList.remove('active'); dom.desktopInfoPanel.classList.remove('hidden'); dom.touchInfoPanel.classList.add('hidden'); } else { dom.desktopTab.classList.remove('active'); dom.touchTab.classList.add('active'); dom.desktopInfoPanel.classList.add('hidden'); dom.touchInfoPanel.classList.remove('hidden'); } }; dom.desktopTab.addEventListener('click', () => { updateInfoTab('desktop'); }); dom.touchTab.addEventListener('click', () => { updateInfoTab('touch'); }); dom.info.addEventListener('click', () => { updateInfoTab(state.inputMode); dom.infoPanel.classList.toggle('hidden'); }); dom.infoPanel.addEventListener('pointerdown', () => { dom.infoPanel.classList.add('hidden'); }); events.on('inputEvent', (event) => { if (event === 'cancel') { // close info panel on cancel dom.infoPanel.classList.add('hidden'); dom.settingsPanel.classList.add('hidden'); // close fullscreen on cancel if (state.isFullscreen) { exitFullscreen(); } } else if (event === 'interrupt') { dom.settingsPanel.classList.add('hidden'); } }); // fade ui controls after 5 seconds of inactivity events.on('controlsHidden:changed', (value) => { dom.controlsWrap.className = value ? 'faded-out' : 'faded-in'; }); // show the ui and start a timer to hide it again let uiTimeout = null; const showUI = () => { if (uiTimeout) { clearTimeout(uiTimeout); } state.controlsHidden = false; uiTimeout = setTimeout(() => { uiTimeout = null; state.controlsHidden = true; }, 4000); }; showUI(); events.on('inputEvent', showUI); // Animation controls events.on('hasAnimation:changed', (value, prev) => { // Start and Stop animation dom.play.addEventListener('click', () => { state.cameraMode = 'anim'; state.animationPaused = false; }); dom.pause.addEventListener('click', () => { state.cameraMode = 'anim'; state.animationPaused = true; }); const updatePlayPause = () => { if (config.minimal || state.cameraMode !== 'anim' || state.animationPaused) { dom.play.classList.remove('hidden'); dom.pause.classList.add('hidden'); } else { dom.play.classList.add('hidden'); dom.pause.classList.remove('hidden'); } if (!config.minimal && state.cameraMode === 'anim') { dom.timelineContainer.classList.remove('hidden'); } else { dom.timelineContainer.classList.add('hidden'); } if (config.minimal) { dom.play.classList.add('hidden'); dom.pause.classList.add('hidden'); } }; // Update UI on animation changes events.on('cameraMode:changed', updatePlayPause); events.on('animationPaused:changed', updatePlayPause); const updateSlider = () => { dom.handle.style.left = `${state.animationTime / state.animationDuration * 100}%`; dom.time.style.left = `${state.animationTime / state.animationDuration * 100}%`; dom.time.innerText = `${state.animationTime.toFixed(1)}s`; }; events.on('animationTime:changed', updateSlider); events.on('animationLength:changed', updateSlider); const handleScrub = (event) => { const rect = dom.timelineContainer.getBoundingClientRect(); const t = Math.max(0, Math.min(rect.width - 1, event.clientX - rect.left)) / rect.width; events.fire('scrubAnim', state.animationDuration * t); showUI(); }; let paused = false; let captured = false; dom.timelineContainer.addEventListener('pointerdown', (event) => { if (!captured) { handleScrub(event); dom.timelineContainer.setPointerCapture(event.pointerId); dom.time.classList.remove('hidden'); paused = state.animationPaused; state.animationPaused = true; captured = true; } }); dom.timelineContainer.addEventListener('pointermove', (event) => { if (captured) { handleScrub(event); } }); dom.timelineContainer.addEventListener('pointerup', (event) => { if (captured) { dom.timelineContainer.releasePointerCapture(event.pointerId); dom.time.classList.add('hidden'); state.animationPaused = paused; captured = false; } }); }); // Camera mode UI const updateCameraMode = () => { dom.orbitCamera.classList[state.cameraMode === 'orbit' ? 'add' : 'remove']('active'); dom.flyCamera.classList[state.cameraMode === 'fly' ? 'add' : 'remove']('active'); }; events.on('cameraMode:changed', updateCameraMode); updateCameraMode(); // Initialize on load // Hide mode buttons if nomode config is set if (config.minimal) { dom.orbitCamera.classList.add('hidden'); dom.flyCamera.classList.add('hidden'); dom.play.classList.add('hidden'); dom.timelineContainer.classList.add('hidden'); } dom.settings.addEventListener('click', () => { dom.settingsPanel.classList.toggle('hidden'); }); dom.orbitCamera.addEventListener('click', () => { state.cameraMode = 'orbit'; }); dom.flyCamera.addEventListener('click', () => { state.cameraMode = 'fly'; }); dom.reset.addEventListener('click', (event) => { events.fire('inputEvent', 'reset', event); }); dom.frame.addEventListener('click', (event) => { events.fire('inputEvent', 'frame', event); }); dom.screenshot.addEventListener('click', () => { events.fire('captureImage'); }); // Background HSL const updateSliders = () => { dom.bgHue.value = state.bgHue.toString(); dom.bgSaturation.value = state.bgSaturation.toString(); dom.bgLightness.value = state.bgLightness.toString(); dom.fov.value = state.fov.toString(); dom.bgHueValue.textContent = `${state.bgHue}°`; dom.bgSaturationValue.textContent = `${state.bgSaturation}%`; dom.bgLightnessValue.textContent = `${state.bgLightness}%`; dom.fovValue.textContent = `${Math.round(state.fov)}°`; }; dom.bgHue.addEventListener('input', (event) => { state.bgHue = parseInt(event.target.value, 10); }); dom.bgSaturation.addEventListener('input', (event) => { state.bgSaturation = parseInt(event.target.value, 10); }); dom.bgLightness.addEventListener('input', (event) => { state.bgLightness = parseInt(event.target.value, 10); }); dom.fov.addEventListener('input', (event) => { state.fov = parseInt(event.target.value, 10); }); events.on('bgHue:changed', updateSliders); events.on('bgSaturation:changed', updateSliders); events.on('bgLightness:changed', updateSliders); events.on('fov:changed', updateSliders); updateSliders(); // update UI based on touch joystick updates events.on('touchJoystickUpdate', (base, stick) => { if (base === null) { dom.joystickBase.classList.add('hidden'); } else { v.set(stick[0], stick[1], 0).mulScalar(1 / 48); if (v.length() > 1) { v.normalize(); } v.mulScalar(48); dom.joystickBase.classList.remove('hidden'); dom.joystickBase.style.left = `${base[0]}px`; dom.joystickBase.style.top = `${base[1]}px`; dom.joystick.style.left = `${48 + v.x}px`; dom.joystick.style.top = `${48 + v.y}px`; } }); // Hide all UI (poster, loading bar, controls) if (config.noui) { dom.ui.classList.add('hidden'); } // tooltips const tooltip = new Tooltip(dom.tooltip); tooltip.register(dom.play, 'Play', 'top'); tooltip.register(dom.pause, 'Pause', 'top'); tooltip.register(dom.orbitCamera, 'Orbit Camera', 'top'); tooltip.register(dom.flyCamera, 'Fly Camera', 'top'); tooltip.register(dom.reset, 'Reset Camera', 'bottom'); tooltip.register(dom.frame, 'Frame Scene', 'bottom'); tooltip.register(dom.settings, 'Settings', 'top'); tooltip.register(dom.info, 'Help', 'top'); tooltip.register(dom.arMode, 'Enter AR', 'top'); tooltip.register(dom.vrMode, 'Enter VR', 'top'); tooltip.register(dom.enterFullscreen, 'Fullscreen', 'top'); tooltip.register(dom.exitFullscreen, 'Fullscreen', 'top'); }; // clamp the vertices of the hotspot so it is never clipped by the near or far plane const depthClamp = ` float f = gl_Position.z / gl_Position.w; if (f > 1.0) { gl_Position.z = gl_Position.w; } else if (f < -1.0) { gl_Position.z = -gl_Position.w; } `; const vec$2 = new Vec3(); /** * A script for creating interactive 3D annotations in a scene. Each annotation consists of: * * - A 3D hotspot that maintains constant screen-space size. The hotspot is rendered with muted * appearance when obstructed by geometry but is still clickable. The hotspot relies on an * invisible DOM element that matches the hotspot's size and position to detect clicks. * - An annotation panel that shows title and description text. */ class Annotation extends Script { static scriptName = 'annotation'; static hotspotSize = 25; static hotspotColor = new Color(0.8, 0.8, 0.8); static hoverColor = new Color(1.0, 0.4, 0.0); static parentDom = null; static styleSheet = null; static camera = null; static tooltipDom = null; static titleDom = null; static textDom = null; static layers = []; static mesh = null; static activeAnnotation = null; static hoverAnnotation = null; static opacity = 1.0; /** * @attribute */ label; /** * @attribute */ title; /** * @attribute */ text; /** * @private */ hotspotDom = null; /** * @private */ texture = null; /** * @private */ materials = []; /** * Injects required CSS styles into the document. * @param {number} size - The size of the hotspot in screen pixels. * @private */ static _injectStyles(size) { const css = ` .pc-annotation { display: block; position: absolute; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 8px; border-radius: 4px; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; pointer-events: none; max-width: 200px; word-wrap: break-word; overflow-x: visible; white-space: normal; width: fit-content; opacity: 0; transition: opacity 0.2s ease-in-out; visibility: hidden; transform: translate(25px, -50%); } .pc-annotation-title { font-weight: bold; margin-bottom: 4px; } /* Add a little triangular arrow on the left edge of the tooltip */ .pc-annotation::before { content: ""; position: absolute; left: -8px; top: 50%; transform: translateY(-50%); width: 0; height: 0; border-top: 8px solid transparent; border-bottom: 8px solid transparent; border-right: 8px solid rgba(0, 0, 0, 0.8); } .pc-annotation-hotspot { display: none; position: absolute; width: ${size + 5}px; height: ${size + 5}px; opacity: 0; cursor: pointer; transform: translate(-50%, -50%); } `; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); Annotation.styleSheet = style; } /** * Initialize static resources. * @param {AppBase} app - The application instance * @private */ static _initializeStatic(app) { if (Annotation.styleSheet) { return; } Annotation._injectStyles(Annotation.hotspotSize); if (Annotation.parentDom === null) { Annotation.parentDom = document.body; } const { layers } = app.scene; const worldLayer = layers.getLayerByName('World'); const createLayer = (name, semitrans) => { const layer = new Layer({ name: name }); const idx = semitrans ? layers.getTransparentIndex(worldLayer) : layers.getOpaqueIndex(worldLayer); layers.insert(layer, idx + 1); return layer; }; Annotation.layers = [ createLayer('HotspotBase', false), createLayer('HotspotOverlay', true) ]; if (Annotation.camera === null) { Annotation.camera = app.root.findComponent('camera').entity; } Annotation.camera.camera.layers = [ ...Annotation.camera.camera.layers, ...Annotation.layers.map(layer => layer.id) ]; Annotation.mesh = Mesh.fromGeometry(app.graphicsDevice, new PlaneGeometry({ widthSegments: 1, lengthSegments: 1 })); // Initialize tooltip dom Annotation.tooltipDom = document.createElement('div'); Annotation.tooltipDom.className = 'pc-annotation'; Annotation.titleDom = document.createElement('div'); Annotation.titleDom.className = 'pc-annotation-title'; Annotation.tooltipDom.appendChild(Annotation.titleDom); Annotation.textDom = document.createElement('div'); Annotation.textDom.className = 'pc-annotation-text'; Annotation.tooltipDom.appendChild(Annotation.textDom); Annotation.parentDom.appendChild(Annotation.tooltipDom); } /** * Creates a circular hotspot texture. * @param {AppBase} app - The PlayCanvas AppBase * @param {string} label - Label text to draw on the hotspot * @param {number} [size] - The texture size (should be power of 2) * @param {number} [borderWidth] - The border width in pixels * @returns {Texture} The hotspot texture * @private */ static _createHotspotTexture(app, label, size = 64, borderWidth = 6) { // Create canvas for hotspot texture const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); // First clear with stroke color at zero alpha ctx.fillStyle = 'white'; ctx.globalAlpha = 0; ctx.fillRect(0, 0, size, size); ctx.globalAlpha = 1.0; // Draw dark circle with light border const centerX = size / 2; const centerY = size / 2; const radius = (size / 2) - 4; // Leave space for border // Draw main circle ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.fillStyle = 'black'; ctx.fill(); // Draw border ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.lineWidth = borderWidth; ctx.strokeStyle = 'white'; ctx.stroke(); // Draw text ctx.font = 'bold 32px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'white'; ctx.fillText(label, Math.floor(canvas.width / 2), Math.floor(canvas.height / 2) + 1); // get pixel data const imageData = ctx.getImageData(0, 0, size, size); const data = imageData.data; // set the color channel of semitransparent pixels to white so the blending at // the edges is correct for (let i = 0; i < data.length; i += 4) { const a = data[i + 3]; if (a < 255) { data[i] = 255; data[i + 1] = 255; data[i + 2] = 255; } } const texture = new Texture(app.graphicsDevice, { width: size, height: size, format: PIXELFORMAT_RGBA8, magFilter: FILTER_LINEAR, minFilter: FILTER_LINEAR, mipmaps: false, levels: [new Uint8Array(data.buffer)] }); return texture; } /** * Creates a material for hotspot rendering. * @param {Texture} texture - The texture to use for emissive and opacity * @param {object} [options] - Material options * @param {number} [options.opacity] - Base opacity multiplier * @param {boolean} [options.depthTest] - Whether to perform depth testing * @param {boolean} [options.depthWrite] - Whether to write to depth buffer * @returns {StandardMaterial} The configured material * @private */ static _createHotspotMaterial(texture, { opacity = 1, depthTest = true, depthWrite = true } = {}) { const material = new StandardMaterial(); // Base properties material.diffuse = Color.BLACK; material.emissive.copy(Annotation.hotspotColor); material.emissiveMap = texture; material.opacityMap = texture; // Alpha properties material.opacity = opacity; material.alphaTest = 0.01; material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE); // Depth properties material.depthTest = depthTest; material.depthWrite = depthWrite; // Rendering properties material.cull = CULLFACE_NONE; material.useLighting = false; material.shaderChunks.glsl.add({ 'litUserMainEndVS': depthClamp }); material.update(); return material; } initialize() { // Ensure static resources are initialized Annotation._initializeStatic(this.app); // Create texture this.texture = Annotation._createHotspotTexture(this.app, this.label); // Create material the base and overlay material this.materials = [ Annotation._createHotspotMaterial(this.texture, { opacity: 1, depthTest: true, depthWrite: true }), Annotation._createHotspotMaterial(this.texture, { opacity: 0.25, depthTest: false, depthWrite: false }) ]; const base = new Entity('base'); const baseMi = new MeshInstance(Annotation.mesh, this.materials[0]); baseMi.cull = false; base.addComponent('render', { layers: [Annotation.layers[0].id], meshInstances: [baseMi] }); const overlay = new Entity('overlay'); const overlayMi = new MeshInstance(Annotation.mesh, this.materials[1]); overlayMi.cull = false; overlay.addComponent('render', { layers: [Annotation.layers[1].id], meshInstances: [overlayMi] }); this.entity.addChild(base); this.entity.addChild(overlay); // Create hotspot dom this.hotspotDom = document.createElement('div'); this.hotspotDom.className = 'pc-annotation-hotspot'; // Add click handlers this.hotspotDom.addEventListener('click', (e) => { e.stopPropagation(); this.showTooltip(); }); const leave = () => { if (Annotation.hoverAnnotation === this) { Annotation.hoverAnnotation = null; this.setHover(false); } }; const enter = () => { if (Annotation.hoverAnnotation !== null) { Annotation.hoverAnnotation.setHover(false); } Annotation.hoverAnnotation = this; this.setHover(true); }; this.hotspotDom.addEventListener('pointerenter', enter); this.hotspotDom.addEventListener('pointerleave', leave); document.addEventListener('click', () => { this.hideTooltip(); }); Annotation.parentDom.appendChild(this.hotspotDom); // Clean up on entity destruction this.on('destroy', () => { this.hotspotDom.remove(); if (Annotation.activeAnnotation === this) { this.hideTooltip(); } this.materials.forEach(mat => mat.destroy()); this.materials = []; this.texture.destroy(); this.texture = null; }); this.app.on('prerender', () => { if (!Annotation.camera) return; const position = this.entity.getPosition(); const screenPos = Annotation.camera.camera.worldToScreen(position); const { viewMatrix } = Annotation.camera.camera; viewMatrix.transformPoint(position, vec$2); if (vec$2.z >= 0) { this._hideElements(); return; } this._updatePositions(screenPos); this._updateRotationAndScale(); // update material opacity and also directly on the uniform so we // can avoid a full material update this.materials[0].opacity = Annotation.opacity; this.materials[1].opacity = 0.25 * Annotation.opacity; this.materials[0].setParameter('material_opacity', Annotation.opacity); this.materials[1].setParameter('material_opacity', 0.25 * Annotation.opacity); }); } /** * Set the hover state of the annotation. * @param hover - Whether the annotation is hovered * @private */ setHover(hover) { this.materials.forEach((material) => { material.emissive.copy(hover ? Annotation.hoverColor : Annotation.hotspotColor); material.update(); }); this.fire('hover', hover); } /** * @private */ showTooltip() { Annotation.activeAnnotation = this; Annotation.tooltipDom.style.visibility = 'visible'; Annotation.tooltipDom.style.opacity = '1'; Annotation.titleDom.textContent = this.title; Annotation.textDom.textContent = this.text; this.fire('show', this); } /** * @private */ hideTooltip() { Annotation.activeAnnotation = null; Annotation.tooltipDom.style.opacity = '0'; // Wait for fade out before hiding setTimeout(() => { if (Annotation.tooltipDom.style.opacity === '0') { Annotation.tooltipDom.style.visibility = 'hidden'; } this.fire('hide'); }, 200); // Match the transition duration } /** * Hide all elements when annotation is behind camera. * @private */ _hideElements() { this.hotspotDom.style.display = 'none'; if (Annotation.activeAnnotation === this) { if (Annotation.tooltipDom.style.visibility !== 'hidden') { this.hideTooltip(); } } } /** * Update screen-space positions of HTML elements. * @param {Vec3} screenPos - Screen coordinate * @private */ _updatePositions(screenPos) { // Show and position hotspot this.hotspotDom.style.display = 'block'; this.hotspotDom.style.left = `${screenPos.x}px`; this.hotspotDom.style.top = `${screenPos.y}px`; // Position tooltip if (Annotation.activeAnnotation === this) { Annotation.tooltipDom.style.left = `${screenPos.x}px`; Annotation.tooltipDom.style.top = `${screenPos.y}px`; } } /** * Update 3D rotation and scale of hotspot planes. * @private */ _updateRotationAndScale() { // Copy camera rotation to align with view plane const cameraRotation = Annotation.camera.getRotation(); this._updateHotspotTransform(this.entity, cameraRotation); // Calculate scale based on distance to maintain constant screen size const scale = this._calculateScreenSpaceScale(); this.entity.setLocalScale(scale, scale, scale); } /** * Update rotation of a single hotspot entity. * @param {Entity} hotspot - The hotspot entity to update * @param {Quat} cameraRotation - The camera's current rotation * @private */ _updateHotspotTransform(hotspot, cameraRotation) { hotspot.setRotation(cameraRotation); hotspot.rotateLocal(90, 0, 0); } /** * Calculate scale factor to maintain constant screen-space size. * @returns {number} The scale to apply to hotspot entities * @private */ _calculateScreenSpaceScale() { const cameraPos = Annotation.camera.getPosition(); const toAnnotation = this.entity.getPosition().sub(cameraPos); const distance = toAnnotation.length(); // Use the canvas's CSS/client height instead of graphics device height const canvas = this.app.graphicsDevice.canvas; const screenHeight = canvas.clientHeight; // Get the camera's projection matrix vertical scale factor const projMatrix = Annotation.camera.camera.projectionMatrix; const worldSize = (Annotation.hotspotSize / screenHeight) * (2 * distance / projMatrix.data[5]); return worldSize; } } class Annotations { annotations; parentDom; constructor(global, hasCameraFrame) { // create dom parent const parentDom = document.createElement('div'); parentDom.id = 'annotations'; Annotation.parentDom = parentDom; document.querySelector('#ui').appendChild(parentDom); global.events.on('controlsHidden:changed', (value) => { parentDom.style.display = value ? 'none' : 'block'; Annotation.opacity = value ? 0.0 : 1.0; if (this.annotations.length > 0) { global.app.renderNextFrame = true; } }); this.annotations = global.settings.annotations; this.parentDom = parentDom; if (hasCameraFrame) { Annotation.hotspotColor.gamma(); Annotation.hoverColor.gamma(); } // create annotation entities const parent = global.app.root; for (let i = 0; i < this.annotations.length; i++) { const ann = this.annotations[i]; const entity = new Entity(); entity.addComponent('script'); entity.script.create(Annotation); const script = entity.script; script.annotation.label = (i + 1).toString(); script.annotation.title = ann.title; script.annotation.text = ann.text; entity.setPosition(ann.position[0], ann.position[1], ann.position[2]); parent.addChild(entity); // handle an annotation being activated/shown script.annotation.on('show', () => { global.events.fire('annotation.activate', ann); }); // re-render if hover state changes script.annotation.on('hover', (hover) => { global.app.renderNextFrame = true; }); } } } /** * Creates a rotation animation track * * @param position - Starting location of the camera. * @param target - Target point around which to rotate * @param fov - The camera field of view. * @param keys - The number of keys in the animation. * @param duration - The duration of the animation in seconds. * @returns - The animation track object containing position and target keyframes. */ const createRotateTrack = (position, target, fov, keys = 12, duration = 20) => { const times = new Array(keys).fill(0).map((_, i) => i / keys * duration); const positions = []; const targets = []; const fovs = new Array(keys).fill(fov); const mat = new Mat4(); const vec = new Vec3(); const dif = new Vec3(position.x - target.x, position.y - target.y, position.z - target.z); for (let i = 0; i < keys; ++i) { mat.setFromEulerAngles(0, -i / keys * 360, 0); mat.transformPoint(dif, vec); positions.push(target.x + vec.x); positions.push(target.y + vec.y); positions.push(target.z + vec.z); targets.push(target.x); targets.push(target.y); targets.push(target.z); } return { name: 'rotate', duration, frameRate: 1, loopMode: 'repeat', interpolation: 'spline', smoothness: 1, keyframes: { times, values: { position: positions, target: targets, fov: fovs } } }; }; class CubicSpline { // control times times; // control data: in-tangent, point, out-tangent knots; // dimension of the knot points dim; constructor(times, knots) { this.times = times; this.knots = knots; this.dim = knots.length / times.length / 3; } evaluate(time, result) { const { times } = this; const last = times.length - 1; if (time <= times[0]) { this.getKnot(0, result); } else if (time >= times[last]) { this.getKnot(last, result); } else { let seg = 0; while (time >= times[seg + 1]) { seg++; } this.evaluateSegment(seg, (time - times[seg]) / (times[seg + 1] - times[seg]), result); } } getKnot(index, result) { const { knots, dim } = this; const idx = index * 3 * dim; for (let i = 0; i < dim; ++i) { result[i] = knots[idx + i * 3 + 1]; } } // evaluate the spline segment at the given normalized time t evaluateSegment(segment, t, result) { const { knots, dim } = this; const t2 = t * t; const twot = t + t; const omt = 1 - t; const omt2 = omt * omt; let idx = segment * dim * 3; // each knot has 3 values: tangent in, value, tangent out for (let i = 0; i < dim; ++i) { const p0 = knots[idx + 1]; // p0 const m0 = knots[idx + 2]; // outgoing tangent const m1 = knots[idx + dim * 3]; // incoming tangent const p1 = knots[idx + dim * 3 + 1]; // p1 idx += 3; result[i] = p0 * ((1 + twot) * omt2) + m0 * (t * omt2) + p1 * (t2 * (3 - twot)) + m1 * (t2 * (t - 1)); } } // calculate cubic spline knots from points // times: time values for each control point // points: control point values to be interpolated (n dimensional) // smoothness: 0 = linear, 1 = smooth static calcKnots(times, points, smoothness) { const n = times.length; const dim = points.length / n; const knots = new Array(n * dim * 3); for (let i = 0; i < n; i++) { const t = times[i]; for (let j = 0; j < dim; j++) { const idx = i * dim + j; const p = points[idx]; let tangent; if (i === 0) { tangent = (points[idx + dim] - p) / (times[i + 1] - t); } else if (i === n - 1) { tangent = (p - points[idx - dim]) / (t - times[i - 1]); } else { tangent = (points[idx + dim] - points[idx - dim]) / (times[i + 1] - times[i - 1]); } // convert to derivatives w.r.t normalized segment parameter const inScale = i > 0 ? (times[i] - times[i - 1]) : (times[1] - times[0]); const outScale = i < n - 1 ? (times[i + 1] - times[i]) : (times[i] - times[i - 1]); knots[idx * 3] = tangent * inScale * smoothness; knots[idx * 3 + 1] = p; knots[idx * 3 + 2] = tangent * outScale * smoothness; } } return knots; } static fromPoints(times, points, smoothness = 1) { return new CubicSpline(times, CubicSpline.calcKnots(times, points, smoothness)); } // create a looping spline by duplicating animation points at the end and beginning static fromPointsLooping(length, times, points, smoothness = 1) { if (times.length < 2) { return CubicSpline.fromPoints(times, points); } const dim = points.length / times.length; const newTimes = times.slice(); const newPoints = points.slice(); // append first two points newTimes.push(length + times[0], length + times[1]); newPoints.push(...points.slice(0, dim * 2)); // prepend last two points newTimes.splice(0, 0, times[times.length - 2] - length, times[times.length - 1] - length); newPoints.splice(0, 0, ...points.slice(points.length - dim * 2)); return CubicSpline.fromPoints(newTimes, newPoints, smoothness); } } /** * Damping function to smooth out transitions. * * @param damping - Damping factor (0 < damping < 1). * @param dt - Delta time in seconds. * @returns - Damping factor adjusted for the delta time. */ /** * Easing function for smooth transitions. * * @param x - Input value in the range [0, 1]. * @returns - Output value in the range [0, 1]. */ const easeOut = (x) => (1 - (2 ** (-10 * x))) / (1 - (2 ** -10)); /** * Modulus function that handles negative values correctly. * * @param n - The number to be modulated. * @param m - The modulus value. * @returns - The result of n mod m, adjusted to be non-negative. */ const mod = (n, m) => ((n % m) + m) % m; const nearlyEquals = (a, b, epsilon = 1e-4) => { return !a.some((v, i) => Math.abs(v - b[i]) >= epsilon); }; const vecToAngles = (result, vec) => { const radToDeg = 180 / Math.PI; result.x = Math.asin(vec.y) * radToDeg; result.y = Math.atan2(-vec.x, -vec.z) * radToDeg; result.z = 0; return result; }; // track an animation cursor with support for repeat and ping-pong loop modes class AnimCursor { duration = 0; loopMode = 'none'; timer = 0; cursor = 0; constructor(duration, loopMode) { this.reset(duration, loopMode); } update(deltaTime) { // update animation timer this.timer += deltaTime; // update the track cursor this.cursor += deltaTime; if (this.cursor >= this.duration) { switch (this.loopMode) { case 'none': this.cursor = this.duration; break; case 'repeat': this.cursor %= this.duration; break; case 'pingpong': this.cursor %= (this.duration * 2); break; } } } reset(duration, loopMode) { this.duration = duration; this.loopMode = loopMode; this.timer = 0; this.cursor = 0; } set value(value) { this.cursor = mod(value, this.duration); } get value() { return this.cursor > this.duration ? this.duration - this.cursor : this.cursor; } } // manage the state of a camera animation track class AnimState { spline; cursor = new AnimCursor(0, 'none'); frameRate; result = []; position = new Vec3(); target = new Vec3(); constructor(spline, duration, loopMode, frameRate) { this.spline = spline; this.cursor.reset(duration, loopMode); this.frameRate = frameRate; } // update given delta time update(dt) { const { cursor, result, spline, frameRate, position, target } = this; // update the animation cursor cursor.update(dt); // evaluate the spline spline.evaluate(cursor.value * frameRate, result); if (result.every(isFinite)) { position.set(result[0], result[1], result[2]); target.set(result[3], result[4], result[5]); } } // construct an animation from a settings track static fromTrack(track) { const { keyframes, duration, frameRate, loopMode, smoothness } = track; const { times, values } = keyframes; const { position, target } = values; // construct the points array containing position and target const points = []; for (let i = 0; i < times.length; i++) { points.push(position[i * 3], position[i * 3 + 1], position[i * 3 + 2]); points.push(target[i * 3], target[i * 3 + 1], target[i * 3 + 2]); } const extra = (duration === times[times.length - 1] / frameRate) ? 1 : 0; const spline = CubicSpline.fromPointsLooping((duration + extra) * frameRate, times, points, smoothness); return new AnimState(spline, duration, loopMode, frameRate); } } class AnimController { animState; constructor(animTrack) { this.animState = AnimState.fromTrack(animTrack); this.animState.update(0); } onEnter(camera) { // snap camera to start position camera.look(this.animState.position, this.animState.target); } update(deltaTime, inputFrame, camera) { this.animState.update(deltaTime); // update camera pose camera.look(this.animState.position, this.animState.target); // ignore input inputFrame.read(); } onExit(camera) { } } const rotation = new Quat(); const avec = new Vec3(); const bvec = new Vec3(); class Camera { position = new Vec3(); angles = new Vec3(); distance = 1; fov = 65; constructor(other) { if (other) { this.copy(other); } } copy(source) { this.position.copy(source.position); this.angles.copy(source.angles); this.distance = source.distance; this.fov = source.fov; } lerp(a, b, t) { a.calcFocusPoint(avec); b.calcFocusPoint(bvec); this.position.lerp(a.position, b.position, t); avec.lerp(avec, bvec, t).sub(this.position); this.distance = avec.length(); vecToAngles(this.angles, avec.mulScalar(1.0 / this.distance)); this.fov = math.lerp(a.fov, b.fov, t); } look(from, to) { this.position.copy(from); this.distance = from.distance(to); const dir = avec.sub2(to, from).normalize(); vecToAngles(this.angles, dir); } calcFocusPoint(result) { rotation.setFromEulerAngles(this.angles) .transformVector(Vec3.FORWARD, result) .mulScalar(this.distance) .add(this.position); } } const p$1 = new Pose(); class FlyController { controller; constructor() { this.controller = new FlyController$1(); this.controller.pitchRange = new Vec2(-90, 90); this.controller.rotateDamping = 0.97; this.controller.moveDamping = 0.97; } onEnter(camera) { p$1.position.copy(camera.position); p$1.angles.copy(camera.angles); p$1.distance = camera.distance; this.controller.attach(p$1, false); } update(deltaTime, inputFrame, camera) { const pose = this.controller.update(inputFrame, deltaTime); camera.position.copy(pose.position); camera.angles.copy(pose.angles); camera.distance = pose.distance; } onExit(camera) { } goto(pose) { this.controller.attach(pose, true); } } const p = new Pose(); class OrbitController { controller; constructor() { this.controller = new OrbitController$1(); this.controller.zoomRange = new Vec2(0.01, Infinity); this.controller.pitchRange = new Vec2(-90, 90); this.controller.rotateDamping = 0.97; this.controller.moveDamping = 0.97; this.controller.zoomDamping = 0.97; } onEnter(camera) { p.position.copy(camera.position); p.angles.copy(camera.angles); p.distance = camera.distance; this.controller.attach(p, false); } update(deltaTime, inputFrame, camera) { const pose = this.controller.update(inputFrame, deltaTime); camera.position.copy(pose.position); camera.angles.copy(pose.angles); camera.distance = pose.distance; } onExit(camera) { } goto(camera) { p.position.copy(camera.position); p.angles.copy(camera.angles); p.distance = camera.distance; this.controller.attach(p, true); } } const tmpCamera = new Camera(); const tmpv = new Vec3(); const createCamera = (position, target, fov) => { const result = new Camera(); result.look(position, target); result.fov = fov; return result; }; const createFrameCamera = (bbox, fov) => { const sceneSize = bbox.halfExtents.length(); const distance = sceneSize / Math.sin(fov / 180 * Math.PI * 0.5); return createCamera(new Vec3(2, 1, 2).normalize().mulScalar(distance).add(bbox.center), bbox.center, fov); }; class CameraManager { update; // holds the camera state camera = new Camera(); constructor(global, bbox) { const { events, settings, state } = global; const camera0 = settings.cameras[0].initial; const frameCamera = createFrameCamera(bbox, camera0.fov); const resetCamera = createCamera(new Vec3(camera0.position), new Vec3(camera0.target), camera0.fov); const getAnimTrack = (initial, isObjectExperience) => { const { animTracks } = settings; // extract the camera animation track from settings if (animTracks?.length > 0 && settings.startMode === 'animTrack') { // use the first animTrack return animTracks[0]; } else if (isObjectExperience) { // create basic rotation animation if no anim track is specified initial.calcFocusPoint(tmpv); return createRotateTrack(initial.position, tmpv, initial.fov); } return null; }; // object experience starts outside the bounding box const isObjectExperience = !bbox.containsPoint(resetCamera.position); const animTrack = getAnimTrack(settings.hasStartPose ? resetCamera : frameCamera, isObjectExperience); const controllers = { orbit: new OrbitController(), fly: new FlyController(), anim: animTrack ? new AnimController(animTrack) : null }; const getController = (cameraMode) => { return controllers[cameraMode]; }; // set the global animation flag state.hasAnimation = !!controllers.anim; state.animationDuration = controllers.anim ? controllers.anim.animState.cursor.duration : 0; // initialize camera mode and initial camera position - always start in fly mode state.cameraMode = 'fly'; this.camera.copy(resetCamera); const target = new Camera(this.camera); // the active controller updates this const from = new Camera(this.camera); // stores the previous camera state during transition let fromMode = 'fly'; // enter the initial controller getController(state.cameraMode).onEnter(this.camera); // transition time between cameras const transitionSpeed = 2.0; let transitionTimer = 1; // application update this.update = (deltaTime, frame) => { // use dt of 0 if animation is paused const dt = state.cameraMode === 'anim' && state.animationPaused ? 0 : deltaTime; // update transition timer transitionTimer = Math.min(1, transitionTimer + deltaTime * transitionSpeed); const controller = getController(state.cameraMode); controller.update(dt, frame, target); if (transitionTimer < 1) { // lerp away from previous camera during transition this.camera.lerp(from, target, easeOut(transitionTimer)); } else { this.camera.copy(target); } // update animation timeline if (state.cameraMode === 'anim') { state.animationTime = controllers.anim.animState.cursor.value; } // sync fov state if (state.fov !== this.camera.fov) { state.fov = Math.round(this.camera.fov); } }; // handle input events events.on('inputEvent', (eventName, event) => { switch (eventName) { case 'frame': state.cameraMode = 'orbit'; controllers.orbit.goto(frameCamera); break; case 'reset': state.cameraMode = 'orbit'; controllers.orbit.goto(resetCamera); break; case 'playPause': if (state.hasAnimation && !global.config.minimal && !global.config.noanim) { if (state.cameraMode === 'anim') { state.animationPaused = !state.animationPaused; } else { state.cameraMode = 'anim'; state.animationPaused = false; } } break; case 'cancel': case 'interrupt': if (state.cameraMode === 'anim') { state.cameraMode = fromMode; } break; } }); // handle external fov changes events.on('fov:changed', (value) => { this.camera.fov = value; target.fov = value; }); // handle camera mode switching events.on('cameraMode:changed', (value, prev) => { // store previous camera mode and pose target.copy(this.camera); from.copy(this.camera); fromMode = prev; // exit the old controller const prevController = getController(prev); prevController.onExit(this.camera); // enter new controller const newController = getController(value); newController.onEnter(this.camera); // reset camera transition timer transitionTimer = 0; }); // handle user scrubbing the animation timeline events.on('scrubAnim', (time) => { // switch to animation camera if we're not already there state.cameraMode = 'anim'; // set time controllers.anim.animState.cursor.value = time; }); // handle user picking in the scene events.on('pick', (position) => { // switch to orbit camera on pick state.cameraMode = 'orbit'; // construct camera tmpCamera.copy(this.camera); tmpCamera.look(this.camera.position, position); controllers.orbit.goto(tmpCamera); }); events.on('annotation.activate', (annotation) => { // switch to orbit camera on pick state.cameraMode = 'orbit'; const { initial } = annotation.camera; // construct camera tmpCamera.fov = initial.fov; tmpCamera.look(new Vec3(initial.position), new Vec3(initial.target)); controllers.orbit.goto(tmpCamera); }); } } const vec$1 = new Vec3(); const vecb = new Vec3(); const ray = new Ray(); const clearColor = new Color(0, 0, 0, 1); // Shared buffer for half-to-float conversion const float32 = new Float32Array(1); const uint32 = new Uint32Array(float32.buffer); // Convert 16-bit half-float to 32-bit float using bit manipulation const half2Float = (h) => { const sign = (h & 0x8000) << 16; // Move sign to bit 31 const exponent = (h & 0x7C00) >> 10; // Extract 5-bit exponent const mantissa = h & 0x03FF; // Extract 10-bit mantissa if (exponent === 0) { if (mantissa === 0) { // Zero uint32[0] = sign; } else { // Denormalized: convert to normalized float32 let e = -1; let m = mantissa; do { e++; m <<= 1; } while ((m & 0x0400) === 0); uint32[0] = sign | ((127 - 15 - e) << 23) | ((m & 0x03FF) << 13); } } else if (exponent === 31) { // Infinity or NaN uint32[0] = sign | 0x7F800000 | (mantissa << 13); } else { // Normalized: adjust exponent bias from 15 to 127 uint32[0] = sign | ((exponent + 127 - 15) << 23) | (mantissa << 13); } return float32[0]; }; // get the normalized world-space ray starting at the camera position // facing the supplied screen position // works for both perspective and orthographic cameras const getRay = (camera, screenX, screenY, ray) => { const cameraPos = camera.getPosition(); // create the pick ray in world space if (camera.camera.projection === PROJECTION_ORTHOGRAPHIC) { camera.camera.screenToWorld(screenX, screenY, -1, vec$1); camera.camera.screenToWorld(screenX, screenY, 1.0, vecb); vecb.sub(vec$1).normalize(); ray.set(vec$1, vecb); } else { camera.camera.screenToWorld(screenX, screenY, 1.0, vec$1); vec$1.sub(cameraPos).normalize(); ray.set(cameraPos, vec$1); } }; class Picker { pick; release; constructor(app, camera) { const { graphicsDevice } = app; let colorBuffer; let renderTarget; let renderPass; const emptyMap = new Map(); const init = (width, height) => { colorBuffer = new Texture(graphicsDevice, { format: PIXELFORMAT_RGBA16F, width: width, height: height, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, name: 'picker' }); renderTarget = new RenderTarget({ colorBuffer, depth: false // not needed - gaussians are rendered back to front }); renderPass = new RenderPassPicker(graphicsDevice, app.renderer); // RGB: additive depth accumulation (ONE, ONE_MINUS_SRC_ALPHA) // Alpha: multiplicative transmittance (ZERO, ONE_MINUS_SRC_ALPHA) -> T = T * (1 - alpha) renderPass.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA, // RGB blend BLENDEQUATION_ADD, BLENDMODE_ZERO, BLENDMODE_ONE_MINUS_SRC_ALPHA // Alpha blend (transmittance) ); }; this.pick = async (x, y) => { const width = Math.floor(graphicsDevice.width); const height = Math.floor(graphicsDevice.height); // convert from [0,1] to pixel coordinates const screenX = Math.floor(x * graphicsDevice.width); const screenY = Math.floor(y * graphicsDevice.height); // flip Y for texture read on WebGL (texture origin is bottom-left) const texX = screenX; const texY = graphicsDevice.isWebGL2 ? height - screenY - 1 : screenY; // construct picker on demand if (!renderPass) { init(width, height); } else { renderTarget.resize(width, height); } // render scene renderPass.init(renderTarget); renderPass.setClearColor(clearColor); renderPass.update(camera.camera, app.scene, [app.scene.layers.getLayerByName('World')], emptyMap, false); renderPass.render(); // read pixel using texture coordinates const pixels = await colorBuffer.read(texX, texY, 1, 1, { renderTarget }); // convert half-float values to floats // R channel: accumulated depth * alpha // A channel: transmittance (1 - alpha), values near 0 have better half-float precision const r = half2Float(pixels[0]); const transmittance = half2Float(pixels[3]); const alpha = 1 - transmittance; // check alpha first (transmittance close to 1 means nothing visible) if (alpha < 1e-6) { return null; } // get camera near/far for denormalization const near = camera.camera.nearClip; const far = camera.camera.farClip; // divide by alpha to get normalized depth, then denormalize to linear depth const normalizedDepth = r / alpha; const depth = normalizedDepth * (far - near) + near; // get the ray from camera through the screen point (using pixel coords) getRay(camera, Math.floor(x * graphicsDevice.canvas.offsetWidth), Math.floor(y * graphicsDevice.canvas.offsetHeight), ray); // convert linear depth (view-space z distance) to ray distance const forward = camera.forward; const t = depth / ray.direction.dot(forward); // world position = ray origin + ray direction * t return ray.origin.clone().add(ray.direction.clone().mulScalar(t)); }; this.release = () => { renderPass?.destroy(); renderTarget?.destroy(); colorBuffer?.destroy(); }; } } /* Vec initialisation to avoid recurrent memory allocation */ const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); const mouseRotate = new Vec3(); const flyMove = new Vec3(); const pinchMove = new Vec3(); const orbitRotate = new Vec3(); const flyRotate = new Vec3(); const stickMove = new Vec3(); const stickRotate = new Vec3(); /** * Converts screen space mouse deltas to world space pan vector. * * @param camera - The camera component. * @param dx - The mouse delta x value. * @param dy - The mouse delta y value. * @param dz - The world space zoom delta value. * @param out - The output vector to store the pan result. * @returns - The pan vector in world space. * @private */ const screenToWorld = (camera, dx, dy, dz, out = new Vec3()) => { const { system, fov, aspectRatio, horizontalFov, projection, orthoHeight } = camera; const { width, height } = system.app.graphicsDevice.clientRect; // normalize deltas to device coord space out.set(-(dx / width) * 2, (dy / height) * 2, 0); // calculate half size of the view frustum at the current distance const halfSize = tmpV2.set(0, 0, 0); if (projection === PROJECTION_PERSPECTIVE) { const halfSlice = dz * Math.tan(0.5 * fov * math.DEG_TO_RAD); if (horizontalFov) { halfSize.set(halfSlice, halfSlice / aspectRatio, 0); } else { halfSize.set(halfSlice * aspectRatio, halfSlice, 0); } } else { halfSize.set(orthoHeight * aspectRatio, orthoHeight, 0); } // scale by device coord space out.mul(halfSize); return out; }; class InputController { _state = { axis: new Vec3(), mouse: [0, 0, 0], shift: 0, ctrl: 0, touches: 0 }; _desktopInput = new KeyboardMouseSource(); _orbitInput = new MultiTouchSource(); _flyInput = new DualGestureSource(); _gamepadInput = new GamepadSource(); global; frame = new InputFrame({ move: [0, 0, 0], rotate: [0, 0, 0] }); joystick = { base: null, stick: null }; // this gets overridden by the viewer based on scene size moveSpeed = 4; orbitSpeed = 18; pinchSpeed = 0.4; wheelSpeed = 0.06; constructor(global) { const { app, camera, events, state } = global; const canvas = app.graphicsDevice.canvas; this._desktopInput.attach(canvas); this._orbitInput.attach(canvas); this._flyInput.attach(canvas); // convert events to joystick state this._flyInput.on('joystick:position:left', ([bx, by, sx, sy]) => { if (bx < 0 || by < 0 || sx < 0 || sy < 0) { this.joystick.base = null; this.joystick.stick = null; return; } this.joystick.base = [bx, by]; this.joystick.stick = [sx - bx, sy - by]; }); this.global = global; // Generate input events ['wheel', 'pointerdown', 'contextmenu', 'keydown'].forEach((eventName) => { canvas.addEventListener(eventName, (event) => { events.fire('inputEvent', 'interrupt', event); }); }); canvas.addEventListener('pointermove', (event) => { events.fire('inputEvent', 'interact', event); }); // Detect double taps manually because iOS doesn't send dblclick events const lastTap = { time: 0, x: 0, y: 0 }; canvas.addEventListener('pointerdown', (event) => { const now = Date.now(); const delay = Math.max(0, now - lastTap.time); if (delay < 300 && Math.abs(event.clientX - lastTap.x) < 8 && Math.abs(event.clientY - lastTap.y) < 8) { events.fire('inputEvent', 'dblclick', event); lastTap.time = 0; } else { lastTap.time = now; lastTap.x = event.clientX; lastTap.y = event.clientY; } }); // Calculate pick location on double click let picker = null; events.on('inputEvent', async (eventName, event) => { switch (eventName) { case 'dblclick': { if (!picker) { picker = new Picker(app, camera); } const result = await picker.pick(event.offsetX / canvas.clientWidth, event.offsetY / canvas.clientHeight); if (result) { events.fire('pick', result); } break; } } }); // update input mode based on pointer event ['pointerdown', 'pointermove'].forEach((eventName) => { window.addEventListener(eventName, (event) => { state.inputMode = event.pointerType === 'touch' ? 'touch' : 'desktop'; }); }); // handle keyboard events window.addEventListener('keydown', (event) => { if (event.key === 'Escape') { events.fire('inputEvent', 'cancel', event); } else if (!event.ctrlKey && !event.altKey && !event.metaKey) { switch (event.key) { case 'f': events.fire('inputEvent', 'frame', event); break; case 'r': events.fire('inputEvent', 'reset', event); break; case ' ': events.fire('inputEvent', 'playPause', event); break; } } }); } /** * @param dt - delta time in seconds * @param state - the current state of the app * @param state.cameraMode - the current camera mode * @param distance - the distance to the camera target */ update(dt, distance) { const { keyCode } = KeyboardMouseSource; const { key, button, mouse, wheel } = this._desktopInput.read(); const { touch, pinch, count } = this._orbitInput.read(); const { leftInput, rightInput } = this._flyInput.read(); const { leftStick, rightStick } = this._gamepadInput.read(); const { events, state } = this.global; const { camera } = this.global.camera; // update state this._state.axis.add(tmpV1.set((key[keyCode.D] - key[keyCode.A]) + (key[keyCode.RIGHT] - key[keyCode.LEFT]), (key[keyCode.E] - key[keyCode.Q]), (key[keyCode.W] - key[keyCode.S]) + (key[keyCode.UP] - key[keyCode.DOWN]))); this._state.touches += count[0]; for (let i = 0; i < button.length; i++) { this._state.mouse[i] += button[i]; } this._state.shift += key[keyCode.SHIFT]; this._state.ctrl += key[keyCode.CTRL]; // Switch to fly mode when using keyboard axis or zooming with wheel const hasWheelZoom = wheel[0] !== 0; const hasPinchZoom = pinch[0] !== 0; if (state.cameraMode !== 'fly' && (this._state.axis.length() > 0 || hasWheelZoom || hasPinchZoom)) { state.cameraMode = 'fly'; } const orbit = +(state.cameraMode === 'orbit'); const fly = +(state.cameraMode === 'fly'); const double = +(this._state.touches > 1); const pan = this._state.mouse[2] || +(button[2] === -1) || double; const orbitFactor = fly ? camera.fov / 120 : 1; // Speed multiplier for consistent movement (keyboard uses moveSpeed) const speedMultiplier = this._state.shift ? 4 : this._state.ctrl ? 0.25 : 1; const { deltas } = this.frame; // desktop move const v = tmpV1.set(0, 0, 0); const keyMove = this._state.axis.clone().normalize(); v.add(keyMove.mulScalar(fly * this.moveSpeed * speedMultiplier * dt)); // Pan movement - use minimum distance of 1 to ensure panning works in fly mode const panDistance = Math.max(distance, 1); const panMove = screenToWorld(camera, mouse[0], mouse[1], panDistance); v.add(panMove.mulScalar(pan * this.moveSpeed * 0.15)); const wheelMove = new Vec3(0, 0, -wheel[0]); v.add(wheelMove.mulScalar(this.wheelSpeed * dt)); // FIXME: need to flip z axis for orbit camera deltas.move.append([v.x, v.y, orbit ? -v.z : v.z]); // desktop rotate v.set(0, 0, 0); mouseRotate.set(mouse[0], mouse[1], 0); v.add(mouseRotate.mulScalar((1 - pan) * this.orbitSpeed * orbitFactor * dt)); deltas.rotate.append([v.x, v.y, v.z]); // mobile move v.set(0, 0, 0); // Touch pan movement - use minimum distance for fly mode compatibility const touchPanDistance = Math.max(distance, 1); const orbitMove = screenToWorld(camera, touch[0], touch[1], touchPanDistance); v.add(orbitMove.mulScalar(orbit * pan * this.moveSpeed * 0.15)); flyMove.set(leftInput[0], 0, -leftInput[1]); v.add(flyMove.mulScalar(fly * this.moveSpeed * dt)); pinchMove.set(0, 0, pinch[0]); v.add(pinchMove.mulScalar(orbit * double * this.pinchSpeed * dt)); deltas.move.append([v.x, v.y, v.z]); // mobile rotate v.set(0, 0, 0); orbitRotate.set(touch[0], touch[1], 0); v.add(orbitRotate.mulScalar(orbit * (1 - pan) * this.orbitSpeed * dt)); flyRotate.set(rightInput[0], rightInput[1], 0); v.add(flyRotate.mulScalar(fly * this.orbitSpeed * orbitFactor * dt)); deltas.rotate.append([v.x, v.y, v.z]); // gamepad move v.set(0, 0, 0); stickMove.set(leftStick[0], 0, -leftStick[1]); v.add(stickMove.mulScalar(this.moveSpeed * dt)); deltas.move.append([v.x, v.y, v.z]); // gamepad rotate v.set(0, 0, 0); stickRotate.set(rightStick[0], rightStick[1], 0); v.add(stickRotate.mulScalar(this.orbitSpeed * orbitFactor * dt)); deltas.rotate.append([v.x, v.y, v.z]); // update touch joystick UI if (state.cameraMode === 'fly') { events.fire('touchJoystickUpdate', this.joystick.base, this.joystick.stick); } } } // override global pick to pack depth instead of meshInstance id const pickDepthGlsl = /* glsl */ ` uniform vec4 camera_params; // 1/far, far, near, isOrtho vec4 getPickOutput() { float linearDepth = 1.0 / gl_FragCoord.w; float normalizedDepth = (linearDepth - camera_params.z) / (camera_params.y - camera_params.z); return vec4(gaussianColor.a * normalizedDepth, 0.0, 0.0, gaussianColor.a); } `; const gammaChunk = ` vec3 prepareOutputFromGamma(vec3 gammaColor) { return gammaColor; } `; const pickDepthWgsl = /* wgsl */ ` uniform camera_params: vec4f; // 1/far, far, near, isOrtho fn getPickOutput() -> vec4f { let linearDepth = 1.0 / pcPosition.w; let normalizedDepth = (linearDepth - uniform.camera_params.z) / (uniform.camera_params.y - uniform.camera_params.z); return vec4f(gaussianColor.a * normalizedDepth, 0.0, 0.0, gaussianColor.a); } `; const tonemapTable = { none: TONEMAP_NONE, linear: TONEMAP_LINEAR, filmic: TONEMAP_FILMIC, hejl: TONEMAP_HEJL, aces: TONEMAP_ACES, aces2: TONEMAP_ACES2, neutral: TONEMAP_NEUTRAL }; const applyPostEffectSettings = (cameraFrame, settings) => { if (settings.sharpness.enabled) { cameraFrame.rendering.sharpness = settings.sharpness.amount; } else { cameraFrame.rendering.sharpness = 0; } const { bloom } = cameraFrame; if (settings.bloom.enabled) { bloom.intensity = settings.bloom.intensity; bloom.blurLevel = settings.bloom.blurLevel; } else { bloom.intensity = 0; } const { grading } = cameraFrame; if (settings.grading.enabled) { grading.enabled = true; grading.brightness = settings.grading.brightness; grading.contrast = settings.grading.contrast; grading.saturation = settings.grading.saturation; grading.tint = new Color().fromArray(settings.grading.tint); } else { grading.enabled = false; } const { vignette } = cameraFrame; if (settings.vignette.enabled) { vignette.intensity = settings.vignette.intensity; vignette.inner = settings.vignette.inner; vignette.outer = settings.vignette.outer; vignette.curvature = settings.vignette.curvature; } else { vignette.intensity = 0; } const { fringing } = cameraFrame; if (settings.fringing.enabled) { fringing.intensity = settings.fringing.intensity; } else { fringing.intensity = 0; } }; const anyPostEffectEnabled = (settings) => { return (settings.sharpness.enabled && settings.sharpness.amount > 0) || (settings.bloom.enabled && settings.bloom.intensity > 0) || (settings.grading.enabled) || (settings.vignette.enabled && settings.vignette.intensity > 0) || (settings.fringing.enabled && settings.fringing.intensity > 0); }; const vec = new Vec3(); class Viewer { global; cameraFrame; inputController; cameraManager; annotations; forceRenderNextFrame = false; constructor(global, gsplatLoad, skyboxLoad) { this.global = global; const { app, settings, config, events, state, camera } = global; const { graphicsDevice } = app; // enable anonymous CORS for image loading in safari app.loader.getHandler('texture').imgParser.crossOrigin = 'anonymous'; // render skybox as plain equirect const glsl = ShaderChunks.get(graphicsDevice, 'glsl'); glsl.set('skyboxPS', glsl.get('skyboxPS').replace('mapRoughnessUv(uv, mipLevel)', 'uv')); glsl.set('pickPS', pickDepthGlsl); const wgsl = ShaderChunks.get(graphicsDevice, 'wgsl'); wgsl.set('skyboxPS', wgsl.get('skyboxPS').replace('mapRoughnessUv(uv, uniform.mipLevel)', 'uv')); wgsl.set('pickPS', pickDepthWgsl); // disable auto render, we'll render only when camera changes app.autoRender = false; // apply camera animation settings camera.camera.aspectRatio = graphicsDevice.width / graphicsDevice.height; // configure the camera this.configureCamera(settings); // reconfigure camera when entering/exiting XR app.xr.on('start', () => this.configureCamera(settings)); app.xr.on('end', () => this.configureCamera(settings)); // handle horizontal fov on canvas resize const updateHorizontalFov = () => { camera.camera.horizontalFov = graphicsDevice.width > graphicsDevice.height; app.renderNextFrame = true; }; graphicsDevice.on('resizecanvas', updateHorizontalFov); updateHorizontalFov(); // handle HQ mode changes const updateHqMode = () => { // limit the backbuffer to 4k on desktop and HD on mobile // we use the shorter dimension so ultra-wide (or high) monitors still work correctly. const maxRatio = (platform.mobile ? 1080 : 2160) / Math.min(screen.width, screen.height); // half pixel resolution with hq mode disabled graphicsDevice.maxPixelRatio = (state.hqMode ? 1.0 : 0.5) * Math.min(maxRatio, window.devicePixelRatio); app.renderNextFrame = true; }; events.on('hqMode:changed', updateHqMode); updateHqMode(); // handle background HSL changes const hslToRgb = (h, s, l) => { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; // achromatic } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return new Color(r, g, b, 1); }; const updateBackgroundColor = () => { const color = hslToRgb(state.bgHue, state.bgSaturation, state.bgLightness); camera.camera.clearColor = color; app.renderNextFrame = true; }; events.on('bgHue:changed', updateBackgroundColor); events.on('bgSaturation:changed', updateBackgroundColor); events.on('bgLightness:changed', updateBackgroundColor); updateBackgroundColor(); // handle screenshot capture events.on('captureImage', () => { app.renderNextFrame = true; app.once('frameend', () => { const canvas = app.graphicsDevice.canvas; const url = canvas.toDataURL('image/png'); const link = document.createElement('a'); // derive filename from contentUrl const contentUrl = global.config.contentUrl || 'screenshot.png'; const filename = contentUrl.split('/').pop().split('?')[0].split('#')[0]; const basename = filename.includes('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename; link.download = `${basename || 'screenshot'}.png`; link.href = url; link.click(); }); }); // construct debug ministats if (config.ministats) { const options = MiniStats.getDefaultOptions(); options.cpu.enabled = false; options.stats = options.stats.filter((s) => s.name !== 'DrawCalls'); options.stats.push({ name: 'VRAM', stats: ['vram.tex'], decimalPlaces: 1, multiplier: 1 / (1024 * 1024), unitsName: 'MB', watermark: 1024 }, { name: 'Splats', stats: ['frame.gsplats'], decimalPlaces: 3, multiplier: 1 / 1000000, unitsName: 'M', watermark: 5 }); // eslint-disable-next-line no-new new MiniStats(app, options); } const prevProj = new Mat4(); const prevWorld = new Mat4(); const sceneBound = new BoundingBox(); // track the camera state and trigger a render when it changes app.on('framerender', () => { const world = camera.getWorldTransform(); const proj = camera.camera.projectionMatrix; if (!app.renderNextFrame) { if (config.ministats || !nearlyEquals(world.data, prevWorld.data) || !nearlyEquals(proj.data, prevProj.data)) { app.renderNextFrame = true; } } // suppress rendering till we're ready if (!state.readyToRender) { app.renderNextFrame = false; } if (this.forceRenderNextFrame) { app.renderNextFrame = true; } if (app.renderNextFrame) { prevWorld.copy(world); prevProj.copy(proj); } }); const applyCamera = (camera) => { const cameraEntity = global.camera; cameraEntity.setPosition(camera.position); cameraEntity.setEulerAngles(camera.angles); cameraEntity.camera.fov = camera.fov; // fit clipping planes to bounding box const boundRadius = sceneBound.halfExtents.length(); // calculate the forward distance between the camera to the bound center vec.sub2(sceneBound.center, camera.position); const dist = vec.dot(cameraEntity.forward); const far = Math.max(dist + boundRadius, 1e-2); const near = Math.max(dist - boundRadius, far / (1024 * 16)); cameraEntity.camera.farClip = far; cameraEntity.camera.nearClip = near; }; // handle application update app.on('update', (deltaTime) => { // in xr mode we leave the camera alone if (app.xr.active) { return; } if (this.inputController && this.cameraManager) { // update inputs this.inputController.update(deltaTime, this.cameraManager.camera.distance); // update cameras this.cameraManager.update(deltaTime, this.inputController.frame); // apply to the camera entity applyCamera(this.cameraManager.camera); } }); // unpause the animation on first frame events.on('firstFrame', () => { state.animationPaused = !!config.noanim; }); // wait for the model to load Promise.all([gsplatLoad, skyboxLoad]).then((results) => { const gsplat = results[0].gsplat; // get scene bounding box const gsplatBbox = gsplat.customAabb; if (gsplatBbox) { sceneBound.setFromTransformedAabb(gsplatBbox, results[0].getWorldTransform()); } if (!config.noui) { this.annotations = new Annotations(global, this.cameraFrame != null); } this.inputController = new InputController(global); this.cameraManager = new CameraManager(global, sceneBound); applyCamera(this.cameraManager.camera); const { instance } = gsplat; if (instance) { // kick off gsplat sorting immediately now that camera is in position instance.sort(camera); // listen for sorting updates to trigger first frame events instance.sorter?.on('updated', () => { // request frame render when sorting changes app.renderNextFrame = true; if (!state.readyToRender) { // we're ready to render once the first sort has completed state.readyToRender = true; // wait for the first valid frame to complete rendering app.once('frameend', () => { events.fire('firstFrame'); // emit first frame event on window window.firstFrame?.(); }); } }); } else { const { gsplat } = app.scene; // quality ranges const ranges = { mobile: { low: { range: [2, 8], splatBudget: 1 }, high: { range: [1, 8], splatBudget: 2 } }, desktop: { low: { range: [1, 8], splatBudget: 3 }, high: { range: [0, 8], splatBudget: 6 } } }; const quality = platform.mobile ? ranges.mobile : ranges.desktop; // start in low quality mode so we can get user interacting asap gsplat.lodRangeMin = quality.low.range[0]; gsplat.lodRangeMax = quality.low.range[1]; results[0].gsplat.splatBudget = quality.low.splatBudget * 1000000; // these two allow LOD behind camera to drop, saves lots of splats gsplat.lodUpdateAngle = 90; gsplat.lodBehindPenalty = 5; // same performance, but rotating on slow devices does not give us unsorted splats on sides gsplat.radialSorting = true; const eventHandler = app.systems.gsplat; // we must force continuous rendering with streaming & lod system this.forceRenderNextFrame = true; let current = 0; let watermark = 1; const readyHandler = (camera, layer, ready, loading) => { if (ready && loading === 0) { // scene is done loading eventHandler.off('frame:ready', readyHandler); state.readyToRender = true; // handle quality mode changes const updateLod = () => { const settings = state.hqMode ? quality.high : quality.low; gsplat.lodRangeMin = settings.range[0]; gsplat.lodRangeMax = settings.range[1]; results[0].gsplat.splatBudget = settings.splatBudget * 1000000; }; events.on('hqMode:changed', updateLod); updateLod(); // debug colorize lods gsplat.colorizeLod = config.colorize; // wait for the first valid frame to complete rendering app.once('frameend', () => { events.fire('firstFrame'); // emit first frame event on window window.firstFrame?.(); }); } // update loading status if (loading !== current) { watermark = Math.max(watermark, loading); current = watermark - loading; state.progress = Math.trunc(current / watermark * 100); } }; eventHandler.on('frame:ready', readyHandler); } }); } // configure camera based on application mode and post process settings configureCamera(settings) { const { global } = this; const { app, camera } = global; const { postEffectSettings } = settings; const { background } = settings; const enableCameraFrame = !app.xr.active && (anyPostEffectEnabled(postEffectSettings) || settings.highPrecisionRendering); if (enableCameraFrame) { // create instance if (!this.cameraFrame) { this.cameraFrame = new CameraFrame(app, camera.camera); } const { cameraFrame } = this; cameraFrame.enabled = true; cameraFrame.rendering.toneMapping = tonemapTable[settings.tonemapping]; cameraFrame.rendering.renderFormats = settings.highPrecisionRendering ? [PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F] : []; applyPostEffectSettings(cameraFrame, postEffectSettings); cameraFrame.update(); // force gsplat shader to write gamma-space colors ShaderChunks.get(app.graphicsDevice, 'glsl').set('gsplatOutputVS', gammaChunk); // ensure the final blit doesn't perform linear->gamma conversion RenderTarget.prototype.isColorBufferSrgb = function () { return true; }; camera.camera.clearColor = new Color(background.color); } else { // no post effects needed, destroy camera frame if it exists if (this.cameraFrame) { this.cameraFrame.destroy(); this.cameraFrame = null; } if (!app.xr.active) { camera.camera.toneMapping = tonemapTable[settings.tonemapping]; camera.camera.clearColor = new Color(background.color); } } } } class XrControllers extends Script { static scriptName = 'xrControllers'; /** * The base URL for fetching the WebXR input profiles. * * @attribute * @type {string} */ basePath = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets/dist/profiles'; controllers = new Map(); initialize() { if (!this.app.xr) { console.error('XrControllers script requires XR to be enabled on the application'); return; } this.app.xr.input.on('add', async (inputSource) => { if (!inputSource.profiles?.length) { console.warn('No profiles available for input source'); return; } // Process all profiles concurrently const profilePromises = inputSource.profiles.map(async (profileId) => { const profileUrl = `${this.basePath}/${profileId}/profile.json`; try { const response = await fetch(profileUrl); if (!response.ok) { return null; } const profile = await response.json(); const layoutPath = profile.layouts[inputSource.handedness]?.assetPath || ''; const assetPath = `${this.basePath}/${profile.profileId}/${inputSource.handedness}${layoutPath.replace(/^\/?(left|right)/, '')}`; // Load the model const asset = await new Promise((resolve, reject) => { this.app.assets.loadFromUrl(assetPath, 'container', (err, asset) => { if (err) reject(err); else resolve(asset); }); }); return { profileId, asset }; } catch (error) { console.warn(`Failed to process profile ${profileId}`); return null; } }); // Wait for all profile attempts to complete const results = await Promise.all(profilePromises); const successfulResult = results.find(result => result !== null); if (successfulResult) { const { asset } = successfulResult; const container = asset.resource; const entity = container.instantiateRenderEntity(); this.app.root.addChild(entity); const jointMap = new Map(); if (inputSource.hand) { for (const joint of inputSource.hand.joints) { const jointEntity = entity.findByName(joint.id); if (jointEntity) { jointMap.set(joint, jointEntity); } } } this.controllers.set(inputSource, { entity, jointMap }); } else { console.warn('No compatible profiles found'); } }); this.app.xr.input.on('remove', (inputSource) => { const controller = this.controllers.get(inputSource); if (controller) { controller.entity.destroy(); this.controllers.delete(inputSource); } }); } update(dt) { if (this.app.xr?.active) { for (const [inputSource, { entity, jointMap }] of this.controllers) { if (inputSource.hand) { for (const [joint, jointEntity] of jointMap) { jointEntity.setPosition(joint.getPosition()); jointEntity.setRotation(joint.getRotation()); } } else { entity.setPosition(inputSource.getPosition()); entity.setRotation(inputSource.getRotation()); } } } } } /** @import { XrInputSource } from 'playcanvas' */ /** * Handles VR navigation with support for both teleportation and smooth locomotion. * Both methods can be enabled simultaneously, allowing users to choose their preferred * navigation method on the fly. * * Teleportation: Point and teleport using trigger/pinch gestures * Smooth Locomotion: Use left thumbstick for movement and right thumbstick for snap turning * * This script should be attached to a parent entity of the camera entity used for the XR * session. The entity hierarchy should be: XrNavigationEntity > CameraEntity for proper * locomotion handling. Use it in conjunction with the `XrControllers` script. */ class XrNavigation extends Script { static scriptName = 'xrNavigation'; /** * Enable teleportation navigation using trigger/pinch gestures. * @attribute */ enableTeleport = true; /** * Enable smooth locomotion using thumbsticks. * @attribute */ enableMove = true; /** * Speed of smooth locomotion movement in meters per second. * @attribute * @range [0.1, 10] * @enabledif {enableMove} */ movementSpeed = 1.5; /** * Angle in degrees for each snap turn. * @attribute * @range [15, 180] * @enabledif {enableMove} */ rotateSpeed = 45; /** * Thumbstick deadzone threshold for movement. * @attribute * @range [0, 0.5] * @precision 0.01 * @enabledif {enableMove} */ movementThreshold = 0.1; /** * Thumbstick threshold to trigger snap turning. * @attribute * @range [0.1, 1] * @precision 0.01 * @enabledif {enableMove} */ rotateThreshold = 0.5; /** * Thumbstick threshold to reset snap turn state. * @attribute * @range [0.05, 0.5] * @precision 0.01 * @enabledif {enableMove} */ rotateResetThreshold = 0.25; /** * Maximum distance for teleportation in meters. * @attribute * @range [1, 50] * @enabledif {enableTeleport} */ maxTeleportDistance = 10; /** * Radius of the teleport target indicator circle. * @attribute * @range [0.1, 2] * @precision 0.1 * @enabledif {enableTeleport} */ teleportIndicatorRadius = 0.2; /** * Number of segments for the teleport indicator circle. * @attribute * @range [8, 64] * @enabledif {enableTeleport} */ teleportIndicatorSegments = 16; /** * Color for valid teleportation areas. * @attribute * @enabledif {enableTeleport} */ validTeleportColor = new Color(0, 1, 0); /** * Color for invalid teleportation areas. * @attribute * @enabledif {enableTeleport} */ invalidTeleportColor = new Color(1, 0, 0); /** * Color for controller rays. * @attribute * @enabledif {enableMove} */ controllerRayColor = new Color(1, 1, 1); /** @type {Set} */ inputSources = new Set(); /** @type {Map} */ activePointers = new Map(); /** @type {Map} */ inputHandlers = new Map(); // Rotation state for snap turning lastRotateValue = 0; // Pre-allocated objects for performance (object pooling) tmpVec2A = new Vec2(); tmpVec2B = new Vec2(); tmpVec3A = new Vec3(); tmpVec3B = new Vec3(); // Color objects validColor = new Color(); invalidColor = new Color(); rayColor = new Color(); // Camera reference for movement calculations /** @type {import('playcanvas').Entity | null} */ cameraEntity = null; initialize() { if (!this.app.xr) { console.error('XrNavigation script requires XR to be enabled on the application'); return; } // Log enabled navigation methods const methods = []; if (this.enableTeleport) methods.push('teleportation'); if (this.enableMove) methods.push('smooth movement'); console.log(`XrNavigation: Enabled methods - ${methods.join(', ')}`); if (!this.enableTeleport && !this.enableMove) { console.warn('XrNavigation: Both teleportation and movement are disabled. Navigation will not work.'); } // Initialize color objects from Color attributes this.validColor.copy(this.validTeleportColor); this.invalidColor.copy(this.invalidTeleportColor); this.rayColor.copy(this.controllerRayColor); // Find camera entity - should be a child of this entity const cameraComponent = this.entity.findComponent('camera'); this.cameraEntity = cameraComponent ? cameraComponent.entity : null; if (!this.cameraEntity) { console.warn('XrNavigation: Camera entity not found. Looking for camera in children...'); // First try to find by name - cast to Entity since we know it should be one const foundByName = this.entity.findByName('camera'); this.cameraEntity = /** @type {import('playcanvas').Entity | null} */ (foundByName); // If not found, search children for entity with camera component if (!this.cameraEntity) { for (const child of this.entity.children) { const childEntity = /** @type {import('playcanvas').Entity} */ (child); if (childEntity.camera) { this.cameraEntity = childEntity; break; } } } if (!this.cameraEntity) { console.error('XrNavigation: No camera entity found. Movement calculations may not work correctly.'); } } this.app.xr.input.on('add', (inputSource) => { const handleSelectStart = () => { this.activePointers.set(inputSource, true); }; const handleSelectEnd = () => { this.activePointers.set(inputSource, false); this.tryTeleport(inputSource); }; // Attach the handlers inputSource.on('selectstart', handleSelectStart); inputSource.on('selectend', handleSelectEnd); // Store the handlers in the map this.inputHandlers.set(inputSource, { handleSelectStart, handleSelectEnd }); this.inputSources.add(inputSource); }); this.app.xr.input.on('remove', (inputSource) => { const handlers = this.inputHandlers.get(inputSource); if (handlers) { inputSource.off('selectstart', handlers.handleSelectStart); inputSource.off('selectend', handlers.handleSelectEnd); this.inputHandlers.delete(inputSource); } this.activePointers.delete(inputSource); this.inputSources.delete(inputSource); }); } findPlaneIntersection(origin, direction) { // Find intersection with y=0 plane if (Math.abs(direction.y) < 0.00001) return null; // Ray is parallel to plane const t = -origin.y / direction.y; if (t < 0) return null; // Intersection is behind the ray return new Vec3( origin.x + direction.x * t, 0, origin.z + direction.z * t ); } tryTeleport(inputSource) { const origin = inputSource.getOrigin(); const direction = inputSource.getDirection(); const hitPoint = this.findPlaneIntersection(origin, direction); if (hitPoint) { const cameraY = this.entity.getPosition().y; hitPoint.y = cameraY; this.entity.setPosition(hitPoint); } } update(dt) { // Handle smooth locomotion and snap turning if (this.enableMove) { this.handleSmoothLocomotion(dt); } // Handle teleportation if (this.enableTeleport) { this.handleTeleportation(); } // Always show controller rays for debugging/visualization this.renderControllerRays(); } handleSmoothLocomotion(dt) { if (!this.cameraEntity) return; for (const inputSource of this.inputSources) { // Only process controllers with gamepads if (!inputSource.gamepad) continue; // Left controller - movement if (inputSource.handedness === 'left') { // Get thumbstick input (axes[2] = X, axes[3] = Y) this.tmpVec2A.set(inputSource.gamepad.axes[2], inputSource.gamepad.axes[3]); // Check if input exceeds deadzone if (this.tmpVec2A.length() > this.movementThreshold) { this.tmpVec2A.normalize(); // Calculate camera-relative movement direction const forward = this.cameraEntity.forward; this.tmpVec2B.x = forward.x; this.tmpVec2B.y = forward.z; this.tmpVec2B.normalize(); // Calculate rotation angle based on camera yaw const rad = Math.atan2(this.tmpVec2B.x, this.tmpVec2B.y) - Math.PI / 2; // Apply rotation to movement vector const t = this.tmpVec2A.x * Math.sin(rad) - this.tmpVec2A.y * Math.cos(rad); this.tmpVec2A.y = this.tmpVec2A.y * Math.sin(rad) + this.tmpVec2A.x * Math.cos(rad); this.tmpVec2A.x = t; // Scale by movement speed and delta time this.tmpVec2A.mulScalar(this.movementSpeed * dt); // Apply movement to camera parent (this entity) this.entity.translate(this.tmpVec2A.x, 0, this.tmpVec2A.y); } } else if (inputSource.handedness === 'right') { // Right controller - snap turning this.handleSnapTurning(inputSource); } } } handleSnapTurning(inputSource) { // Get rotation input from right thumbstick X-axis const rotate = -inputSource.gamepad.axes[2]; // Hysteresis system to prevent multiple rotations from single gesture if (this.lastRotateValue > 0 && rotate < this.rotateResetThreshold) { this.lastRotateValue = 0; } else if (this.lastRotateValue < 0 && rotate > -this.rotateResetThreshold) { this.lastRotateValue = 0; } // Only rotate when thumbstick crosses threshold from neutral position if (this.lastRotateValue === 0 && Math.abs(rotate) > this.rotateThreshold) { this.lastRotateValue = Math.sign(rotate); if (this.cameraEntity) { // Rotate around camera position, not entity origin this.tmpVec3A.copy(this.cameraEntity.getLocalPosition()); this.entity.translateLocal(this.tmpVec3A); this.entity.rotateLocal(0, Math.sign(rotate) * this.rotateSpeed, 0); this.entity.translateLocal(this.tmpVec3A.mulScalar(-1)); } } } handleTeleportation() { for (const inputSource of this.inputSources) { // Only show teleportation ray when trigger/select is pressed if (!this.activePointers.get(inputSource)) continue; const start = inputSource.getOrigin(); const direction = inputSource.getDirection(); const hitPoint = this.findPlaneIntersection(start, direction); if (hitPoint && this.isValidTeleportDistance(hitPoint)) { // Draw line to intersection point this.app.drawLine(start, hitPoint, this.validColor); this.drawTeleportIndicator(hitPoint); } else { // Draw full length ray if no intersection or invalid distance this.tmpVec3B.copy(direction).mulScalar(this.maxTeleportDistance).add(start); this.app.drawLine(start, this.tmpVec3B, this.invalidColor); } } } renderControllerRays() { // Only render controller rays when smooth movement is enabled // (teleport rays are handled separately in handleTeleportation) if (!this.enableMove) return; for (const inputSource of this.inputSources) { // Skip if currently teleporting (handled by handleTeleportation) if (this.activePointers.get(inputSource)) continue; const start = inputSource.getOrigin(); this.tmpVec3B.copy(inputSource.getDirection()).mulScalar(2).add(start); this.app.drawLine(start, this.tmpVec3B, this.rayColor); } } isValidTeleportDistance(hitPoint) { const distance = hitPoint.distance(this.entity.getPosition()); return distance <= this.maxTeleportDistance; } drawTeleportIndicator(point) { // Draw a circle at the teleport point using configurable attributes const segments = this.teleportIndicatorSegments; const radius = this.teleportIndicatorRadius; for (let i = 0; i < segments; i++) { const angle1 = (i / segments) * Math.PI * 2; const angle2 = ((i + 1) / segments) * Math.PI * 2; const x1 = point.x + Math.cos(angle1) * radius; const z1 = point.z + Math.sin(angle1) * radius; const x2 = point.x + Math.cos(angle2) * radius; const z2 = point.z + Math.sin(angle2) * radius; // Use pre-allocated vectors to avoid garbage collection this.tmpVec3A.set(x1, 0.01, z1); // Slightly above ground to avoid z-fighting this.tmpVec3B.set(x2, 0.01, z2); this.app.drawLine(this.tmpVec3A, this.tmpVec3B, this.validColor); } } } // On entering/exiting AR, we need to set the camera clear color to transparent black const initXr = (global) => { const { app, events, state, camera } = global; state.hasAR = app.xr.isAvailable('immersive-ar'); state.hasVR = app.xr.isAvailable('immersive-vr'); // initialize ar/vr app.xr.on('available:immersive-ar', (available) => { state.hasAR = available; }); app.xr.on('available:immersive-vr', (available) => { state.hasVR = available; }); const parent = camera.parent; const clearColor = new Color(); const parentPosition = new Vec3(); const parentRotation = new Quat(); const cameraPosition = new Vec3(); const cameraRotation = new Quat(); const angles = new Vec3(); parent.addComponent('script'); parent.script.create(XrControllers); parent.script.create(XrNavigation); app.xr.on('start', () => { app.autoRender = true; // cache original camera rig positions and rotations parentPosition.copy(parent.getPosition()); parentRotation.copy(parent.getRotation()); cameraPosition.copy(camera.getPosition()); cameraRotation.copy(camera.getRotation()); cameraRotation.getEulerAngles(angles); // copy transform to parent to XR/VR mode starts in the right place parent.setPosition(cameraPosition.x, 0, cameraPosition.z); parent.setEulerAngles(0, angles.y, 0); if (app.xr.type === 'immersive-ar') { clearColor.copy(camera.camera.clearColor); camera.camera.clearColor = new Color(0, 0, 0, 0); } }); app.xr.on('end', () => { app.autoRender = false; // restore camera to pre-XR state parent.setPosition(parentPosition); parent.setRotation(parentRotation); camera.setPosition(cameraPosition); camera.setRotation(cameraRotation); if (app.xr.type === 'immersive-ar') { camera.camera.clearColor = clearColor; } }); events.on('startAR', () => { app.xr.start(app.root.findComponent('camera'), 'immersive-ar', 'local-floor'); }); events.on('startVR', () => { app.xr.start(app.root.findComponent('camera'), 'immersive-vr', 'local-floor'); }); events.on('inputEvent', (event) => { if (event === 'cancel' && app.xr.active) { app.xr.end(); } }); }; var version = "1.11.0"; const loadGsplat = async (app, config, progressCallback) => { const { contents, contentUrl, unified, aa } = config; const c = contents; const filename = new URL(contentUrl, location.href).pathname.split('/').pop(); const data = filename.toLowerCase() === 'meta.json' ? await (await contents).json() : undefined; const asset = new Asset(filename, 'gsplat', { url: contentUrl, filename, contents: c }, data); return new Promise((resolve, reject) => { asset.on('load', () => { const entity = new Entity('gsplat'); entity.setLocalEulerAngles(0, 0, 180); entity.addComponent('gsplat', { unified: unified || filename.toLowerCase().endsWith('lod-meta.json'), asset }); const material = entity.gsplat.unified ? app.scene.gsplat.material : entity.gsplat.material; material.setDefine('GSPLAT_AA', aa); material.setParameter('alphaClip', 1 / 255); app.root.addChild(entity); resolve(entity); }); let watermark = 0; asset.on('progress', (received, length) => { const progress = Math.min(1, received / length) * 100; if (progress > watermark) { watermark = progress; progressCallback(Math.trunc(watermark)); } }); asset.on('error', (err) => { console.log(err); reject(err); }); app.assets.add(asset); app.assets.load(asset); }); }; const loadSkybox = (app, url) => { return new Promise((resolve, reject) => { const asset = new Asset('skybox', 'texture', { url }, { type: 'rgbp', mipmaps: false, addressu: 'repeat', addressv: 'clamp' }); asset.on('load', () => { resolve(asset); }); asset.on('error', (err) => { console.log(err); reject(err); }); app.assets.add(asset); app.assets.load(asset); }); }; const main = (app, camera, settingsJson, config) => { const events = new EventHandler(); const state = observe(events, { readyToRender: false, hqMode: true, progress: 0, inputMode: 'desktop', cameraMode: 'fly', hasAnimation: false, animationDuration: 0, animationTime: 0, animationPaused: true, hasAR: false, hasVR: false, isFullscreen: false, controlsHidden: false, bgHue: 0, bgSaturation: 0, bgLightness: 0, fov: 0 }); const global = { app, settings: importSettings(settingsJson), config, state, events, camera }; // initialize HSL from background color const color = global.settings.background.color; const r = color[0] / 255; const g = color[1] / 255; const b = color[2] / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h, s; const l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } state.bgHue = Math.round(h * 360); state.bgSaturation = Math.round(s * 100); state.bgLightness = Math.round(l * 100); state.fov = global.settings.cameras[0].initial.fov; // Initialize the load-time poster if (config.poster) { initPoster(events); } camera.addComponent('camera'); // Initialize XR support initXr(global); // Initialize user interface initUI(global); // Load model const gsplatLoad = loadGsplat(app, config, (progress) => { state.progress = progress; }); // Load skybox const skyboxLoad = config.skyboxUrl && loadSkybox(app, config.skyboxUrl).then((asset) => { app.scene.envAtlas = asset.resource; }); // Load and play sound if (global.settings.soundUrl) { const sound = new Audio(global.settings.soundUrl); sound.crossOrigin = 'anonymous'; document.body.addEventListener('click', () => { if (sound) { sound.play(); } }, { capture: true, once: true }); } // Create the viewer return new Viewer(global, gsplatLoad, skyboxLoad); }; console.log(`SuperSplat Viewer v${version} | Engine v${version$1} (${revision})`); export { main }; //# sourceMappingURL=index.js.map