diff --git a/app_web/src/main.js b/app_web/src/main.js index 13113ef7..503d202a 100644 --- a/app_web/src/main.js +++ b/app_web/src/main.js @@ -7,8 +7,14 @@ import { Observable, Subject, from, merge } from 'rxjs'; import { mergeMap, filter, map, multicast } from 'rxjs/operators'; import { gltfModelPathProvider, fillEnvironmentWithPaths } from './model_path_provider.js'; -async function main() -{ + +import mikktspaceWasm from '../../source/libs/mikktspace_module.js'; + +let mikktspace = null; + +async function main() { + mikktspace = await mikktspaceWasm("libs/mikktspace_module_bg.wasm"); + const canvas = document.getElementById("canvas"); const context = canvas.getContext("webgl2", { alpha: false, antialias: true }); const ui = document.getElementById("app"); @@ -363,4 +369,4 @@ async function main() window.requestAnimationFrame(update); } -export { main }; +export { main, mikktspace }; diff --git a/source/gltf/primitive.js b/source/gltf/primitive.js index dd37cc71..6cf0ea5b 100644 --- a/source/gltf/primitive.js +++ b/source/gltf/primitive.js @@ -1,6 +1,7 @@ import { initGlForMembers } from './utils.js'; import { GltfObject } from './gltf_object.js'; import { gltfBuffer } from './buffer.js'; +import { gltfAccessor } from './accessor.js'; import { gltfImage } from './image.js'; import { ImageMimeType } from './image_mime_type.js'; import { gltfTexture } from './texture.js'; @@ -9,13 +10,14 @@ import { gltfSampler } from './sampler.js'; import { gltfBufferView } from './buffer_view.js'; import { DracoDecoder } from '../ResourceLoader/draco.js'; import { GL } from '../Renderer/webgl.js'; +import { mikktspace } from '../../app_web/src/main.js'; class gltfPrimitive extends GltfObject { constructor() { super(); - this.attributes = []; + this.attributes = {}; this.targets = []; this.indices = undefined; this.material = undefined; @@ -53,6 +55,7 @@ class gltfPrimitive extends GltfObject if (this.extensions !== undefined) { + // Decode Draco compressed mesh: if (this.extensions.KHR_draco_mesh_compression !== undefined) { const dracoDecoder = new DracoDecoder(); @@ -69,6 +72,15 @@ class gltfPrimitive extends GltfObject } } + if (this.attributes.TANGENT === undefined) + { + console.info("Generating tangents using the MikkTSpace algorithm."); + console.time("Tangent generation"); + this.unweld(gltf); + this.generateTangents(gltf); + console.timeEnd("Tangent generation"); + } + // VERTEX ATTRIBUTES for (const attribute of Object.keys(this.attributes)) { @@ -722,6 +734,131 @@ class gltfPrimitive extends GltfObject }; } + + /** + * Unwelds this primitive, i.e. applies the index mapping. + * This is required for generating tangents using the MikkTSpace algorithm, + * because the same vertex might be mapped to different tangents. + * @param {*} gltf The glTF document. + */ + unweld(gltf) { + // Unwelding is an idempotent operation. + if (this.indices === undefined) { + return; + } + + const indices = gltf.accessors[this.indices].getTypedView(gltf); + + // Unweld attributes: + for (const [attribute, accessorIndex] of Object.entries(this.attributes)) { + this.attributes[attribute] = this.unweldAccessor(gltf, gltf.accessors[accessorIndex], indices); + } + + // Unweld morph targets: + for (const target of this.targets) { + for (const [attribute, accessorIndex] of Object.entries(target)) { + target[attribute] = this.unweldAccessor(gltf, gltf.accessors[accessorIndex], indices); + } + } + + // Dipose the indices: + this.indices = undefined; + } + + /** + * Unwelds a single accessor. Used by {@link unweld}. + * @param {*} gltf The glTF document. + * @param {*} accessor The accessor to unweld. + * @param {*} typedIndexView A typed view of the indices. + * @returns A new accessor index containing the unwelded attribute. + */ + unweldAccessor(gltf, accessor, typedIndexView) { + const stride = accessor.getComponentCount(accessor.type); + + const weldedAttribute = accessor.getTypedView(gltf); + const unweldedAttribute = new Float32Array(gltf.accessors[this.indices].count * stride); + + // Apply the index mapping. + for (let i = 0; i < typedIndexView.length; i++) { + for (let j = 0; j < stride; j++) { + unweldedAttribute[i * stride + j] = weldedAttribute[typedIndexView[i] * stride + j]; + } + } + + // Create a new buffer and buffer view for the unwelded attribute: + const unweldedBuffer = new gltfBuffer(); + unweldedBuffer.byteLength = unweldedAttribute.byteLength; + unweldedBuffer.buffer = unweldedAttribute.buffer; + gltf.buffers.push(unweldedBuffer); + + const unweldedBufferView = new gltfBufferView(); + unweldedBufferView.buffer = gltf.buffers.length - 1; + unweldedBufferView.byteLength = unweldedAttribute.byteLength; + unweldedBufferView.target = GL.ARRAY_BUFFER; + gltf.bufferViews.push(unweldedBufferView); + + // Create a new accessor for the unwelded attribute: + const unweldedAccessor = new gltfAccessor(); + unweldedAccessor.bufferView = gltf.bufferViews.length - 1; + unweldedAccessor.byteOffset = 0; + unweldedAccessor.count = typedIndexView.length; + unweldedAccessor.type = accessor.type; + unweldedAccessor.componentType = accessor.componentType; + unweldedAccessor.min = gltf.accessors[this.attributes.POSITION].min; + unweldedAccessor.max = gltf.accessors[this.attributes.POSITION].max; + gltf.accessors.push(unweldedAccessor); + + // Update the primitive to use the unwelded attribute: + return gltf.accessors.length - 1; + } + + generateTangents(gltf) { + const positions = gltf.accessors[this.attributes.POSITION].getTypedView(gltf); + const normals = gltf.accessors[this.attributes.NORMAL].getTypedView(gltf); + const texcoords = gltf.accessors[this.attributes.TEXCOORD_0].getTypedView(gltf); + for (let i = 0; i < positions.length; i += 3) { + mikktspace.writeFace( + positions[i], positions[i + 1], positions[i + 2], + normals[i], normals[i + 1], normals[i + 2], + texcoords[i], texcoords[i + 1] + ); + } + + mikktspace.generateTangents(); + + const tangents = new Float32Array(4 * positions.length / 3); + for (let i = 0; i < tangents.length; i++) { + let t = mikktspace.readTangent(i); + tangents[i] = t; + } + + // Create a new buffer and buffer view for the tangents: + const tangentBuffer = new gltfBuffer(); + tangentBuffer.byteLength = tangents.byteLength; + tangentBuffer.buffer = tangents.buffer; + gltf.buffers.push(tangentBuffer); + + const tangentBufferView = new gltfBufferView(); + tangentBufferView.buffer = gltf.buffers.length - 1; + tangentBufferView.byteLength = tangents.byteLength; + tangentBufferView.target = GL.ARRAY_BUFFER; + gltf.bufferViews.push(tangentBufferView); + + // Create a new accessor for the tangents: + const tangentAccessor = new gltfAccessor(); + tangentAccessor.bufferView = gltf.bufferViews.length - 1; + tangentAccessor.byteOffset = 0; + tangentAccessor.count = tangents.length / 4; + tangentAccessor.type = "VEC4"; + tangentAccessor.componentType = GL.FLOAT; + + // Update the primitive to use the tangents: + this.attributes.TANGENT = gltf.accessors.length; + gltf.accessors.push(tangentAccessor); + + // Update the primitive to use the tangents: + this.attributes.TANGENT = gltf.accessors.length - 1; + } } export { gltfPrimitive }; diff --git a/source/libs/mikktspace_module.js b/source/libs/mikktspace_module.js new file mode 100644 index 00000000..e18d20dd --- /dev/null +++ b/source/libs/mikktspace_module.js @@ -0,0 +1,360 @@ + +let wasm; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachegetUint8Memory0 = null; +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +let WASM_VECTOR_LEN = 0; + +let cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} +/** +*/ +export function init_panic_hook() { + wasm.init_panic_hook(); +} + +/** +* @param {number} a +* @param {number} b +* @returns {number} +*/ +export function add(a, b) { + var ret = wasm.add(a, b); + return ret >>> 0; +} + +/** +* @param {string} a +* @returns {string} +*/ +export function foo(a) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + var ptr0 = passStringToWasm0(a, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + wasm.foo(retptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } +} + +let cachegetUint32Memory0 = null; +function getUint32Memory0() { + if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) { + cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer); + } + return cachegetUint32Memory0; +} + +function passArrayJsValueToWasm0(array, malloc) { + const ptr = malloc(array.length * 4); + const mem = getUint32Memory0(); + for (let i = 0; i < array.length; i++) { + mem[ptr / 4 + i] = addHeapObject(array[i]); + } + WASM_VECTOR_LEN = array.length; + return ptr; +} +/** +* @param {any[]} a +* @returns {number} +*/ +export function length(a) { + var ptr0 = passArrayJsValueToWasm0(a, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.length(ptr0, len0); + return ret >>> 0; +} + +/** +* @param {number} p0 +* @param {number} p1 +* @param {number} p2 +* @param {number} n0 +* @param {number} n1 +* @param {number} n2 +* @param {number} t0 +* @param {number} t1 +*/ +export function writeFace(p0, p1, p2, n0, n1, n2, t0, t1) { + wasm.writeFace(p0, p1, p2, n0, n1, n2, t0, t1); +} + +/** +* @param {number} i +* @returns {number} +*/ +export function readTangent(i) { + var ret = wasm.readTangent(i); + return ret; +} + +/** +* Generates vertex tangents for the given position/normal/texcoord attributes. +*/ +export function generateTangents() { + wasm.generateTangents(); +} + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +async function init(input) { + if (typeof input === 'undefined') { + input = new URL('mikktspace_module_bg.wasm', import.meta.url); + } + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbg_log_19b305dd6bb2d393 = function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + var ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_59cb74e423758ede = function() { + var ret = new Error(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_stack_558ba5917b466edd = function(arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_error_4bb6c2a97407129a = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } + }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + var ret = debugString(getObject(arg1)); + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_rethrow = function(arg0) { + throw takeObject(arg0); + }; + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + + + const { instance, module } = await load(await input, imports); + + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + + return wasm; +} + +export default init; + diff --git a/source/libs/mikktspace_module_bg.wasm b/source/libs/mikktspace_module_bg.wasm new file mode 100644 index 00000000..253f2de1 Binary files /dev/null and b/source/libs/mikktspace_module_bg.wasm differ