diff --git a/app_web/package.json b/app_web/package.json index 815a54e1..0899e9fa 100644 --- a/app_web/package.json +++ b/app_web/package.json @@ -27,30 +27,31 @@ "@khronosgroup/gltf-viewer": "..", "axios": "^0.21.1", "buefy": "^0.9.4", + "fast-png": "^5.0.3", "gl-matrix": "^3.2.1", "gltf-viewer-source": "../source", + "jpeg-js": "^0.4.3", "normalize-wheel": "^1.0.1", "path": "^0.12.7", "rxjs": "^6.6.3", "simple-dropzone": "^0.8.0", "vue": "^2.6.12", - "vue-rx": "^6.2.0", - "fast-png": "^5.0.3", - "jpeg-js": "^0.4.3" + "vue-rx": "^6.2.0" }, "devDependencies": { - "rollup": "^2.79.1", "@rollup/plugin-alias": "^3.1.9", - "@rollup/plugin-replace": "^4.0.0", + "@rollup/plugin-commonjs": "22.0.2", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^14.1.0", - "@rollup/plugin-commonjs": "22.0.2", + "@rollup/plugin-replace": "^4.0.0", + "@rollup/plugin-wasm": "^6.1.3", + "concurrently": "^7.4.0", + "eslint": "^8.24.0", + "rollup": "^2.79.1", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-glslify": "^1.3.1", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-scss": "^3.0.0", - "concurrently": "^7.4.0", - "eslint": "^8.24.0", "sass": "^1.55.0", "serve": "^14.0.1" }, diff --git a/app_web/rollup.config.js b/app_web/rollup.config.js index e93ce17d..678fb865 100644 --- a/app_web/rollup.config.js +++ b/app_web/rollup.config.js @@ -7,6 +7,7 @@ import copy from 'rollup-plugin-copy'; import alias from '@rollup/plugin-alias'; import replace from '@rollup/plugin-replace'; import json from '@rollup/plugin-json'; +import {wasm} from "@rollup/plugin-wasm"; export default { input: 'src/main.js', @@ -19,6 +20,7 @@ export default { } ], plugins: [ + wasm(), json(), glslify({ include: ['../source/Renderer/shaders/*', '../source/shaders/*'], diff --git a/app_web/src/main.js b/app_web/src/main.js index 13113ef7..33d5cb18 100644 --- a/app_web/src/main.js +++ b/app_web/src/main.js @@ -7,8 +7,7 @@ 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() -{ +async function main() { const canvas = document.getElementById("canvas"); const context = canvas.getContext("webgl2", { alpha: false, antialias: true }); const ui = document.getElementById("app"); diff --git a/package.json b/package.json index cb4e2d21..ca578563 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "rollup": "^2.79.1", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-glslify": "^1.3.1", + "@rollup/plugin-wasm": "^6.1.3", "@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-node-resolve": "^14.1.0", "concurrently": "^7.4.0", diff --git a/rollup.config.js b/rollup.config.js index 1ae7c775..33e457b5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,6 +2,7 @@ import commonjs from '@rollup/plugin-commonjs'; import glslify from 'rollup-plugin-glslify'; import resolve from '@rollup/plugin-node-resolve'; import copy from "rollup-plugin-copy"; +import {wasm} from "@rollup/plugin-wasm"; export default { @@ -19,6 +20,7 @@ export default { } ], plugins: [ + wasm(), glslify(), resolve({ browser: false, diff --git a/source/ResourceLoader/resource_loader.js b/source/ResourceLoader/resource_loader.js index 690db2cd..bbbd00b7 100644 --- a/source/ResourceLoader/resource_loader.js +++ b/source/ResourceLoader/resource_loader.js @@ -9,6 +9,8 @@ import { gltfTexture, gltfTextureInfo } from '../gltf/texture.js'; import { gltfSampler } from '../gltf/sampler.js'; import { GL } from '../Renderer/webgl.js'; import { iblSampler } from '../ibl_sampler.js'; +import init from '../libs/mikktspace.js'; +import mikktspace from '../libs/mikktspace_bg.wasm'; import { AsyncFileReader } from './async_file_reader.js'; @@ -110,6 +112,7 @@ class ResourceLoader image.resolveRelativePath(getContainingFolder(gltf.path)); } + await init(await mikktspace()); await gltfLoader.load(gltf, this.view.context, buffers); return gltf; diff --git a/source/gltf/primitive.js b/source/gltf/primitive.js index dd37cc71..e28b80a9 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,15 @@ import { gltfSampler } from './sampler.js'; import { gltfBufferView } from './buffer_view.js'; import { DracoDecoder } from '../ResourceLoader/draco.js'; import { GL } from '../Renderer/webgl.js'; +import { generateTangents } from '../libs/mikktspace.js'; + class gltfPrimitive extends GltfObject { constructor() { super(); - this.attributes = []; + this.attributes = {}; this.targets = []; this.indices = undefined; this.material = undefined; @@ -53,6 +56,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 +73,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 +735,121 @@ 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 componentCount = accessor.getComponentCount(accessor.type); + + const weldedAttribute = accessor.getTypedView(gltf); + const unweldedAttribute = new Float32Array(gltf.accessors[this.indices].count * componentCount); + + // Apply the index mapping. + for (let i = 0; i < typedIndexView.length; i++) { + for (let j = 0; j < componentCount; j++) { + unweldedAttribute[i * componentCount + j] = weldedAttribute[typedIndexView[i] * componentCount + 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 = accessor.min; + unweldedAccessor.max = accessor.max; + gltf.accessors.push(unweldedAccessor); + + // Update the primitive to use the unwelded attribute: + return gltf.accessors.length - 1; + } + + generateTangents(gltf) { + if(this.attributes.NORMAL === undefined || this.attributes.TEXCOORD_0 === undefined) + { + return; + } + + 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); + + const tangents = generateTangents(positions, normals, texcoords); + + // 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); + + } } export { gltfPrimitive }; diff --git a/source/libs/mikktspace.js b/source/libs/mikktspace.js new file mode 100644 index 00000000..f6ff6f67 --- /dev/null +++ b/source/libs/mikktspace.js @@ -0,0 +1,164 @@ + +let wasm; + +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)); +} + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +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 getObject(idx) { return heap[idx]; } + +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 cachegetFloat32Memory0 = null; +function getFloat32Memory0() { + if (cachegetFloat32Memory0 === null || cachegetFloat32Memory0.buffer !== wasm.memory.buffer) { + cachegetFloat32Memory0 = new Float32Array(wasm.memory.buffer); + } + return cachegetFloat32Memory0; +} + +let WASM_VECTOR_LEN = 0; + +function passArrayF32ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 4); + getFloat32Memory0().set(arg, ptr / 4); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +function getArrayF32FromWasm0(ptr, len) { + return getFloat32Memory0().subarray(ptr / 4, ptr / 4 + len); +} +/** +* Generates vertex tangents for the given position/normal/texcoord attributes. +* @param {Float32Array} position +* @param {Float32Array} normal +* @param {Float32Array} texcoord +* @returns {Float32Array} +*/ +export function generateTangents(position, normal, texcoord) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + var ptr0 = passArrayF32ToWasm0(position, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ptr1 = passArrayF32ToWasm0(normal, wasm.__wbindgen_malloc); + var len1 = WASM_VECTOR_LEN; + var ptr2 = passArrayF32ToWasm0(texcoord, wasm.__wbindgen_malloc); + var len2 = WASM_VECTOR_LEN; + wasm.generateTangents(retptr, ptr0, len0, ptr1, len1, ptr2, len2); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v3 = getArrayF32FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 4); + return v3; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +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_bg.wasm', import.meta.url); + } + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + var ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + 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_bg.wasm b/source/libs/mikktspace_bg.wasm new file mode 100644 index 00000000..b49bf3f7 Binary files /dev/null and b/source/libs/mikktspace_bg.wasm differ