Files
filament/web/filament-js/utilities.js
Philip Rideout 9aeec3a759 Add Ktx2Reader and BasisEncoder and use them in samples.
mipgen can now emit basis-encoded KTX2 files. Both the desktop and
web "suzanne" samples use this as a test for compressed textures.

This PR does not add KTX2 support to glTF, but it's on the way.

`BasisEncoder` has a builder style API that calls the basis encoder to
create KTX2 files. This hides some low-level BasisU features that we are
not using, like file I/O and mipmap generation.

`Ktx2Reader` is an easy-to-use API for creating Filament textures from
KTX2 files. Its API primarily consists of these two methods:

    bool requestFormat(Texture::InternalFormat format);
    Filament::Texture* load(const uint8_t* data, size_t size);

The first method is used to build an ordered list of formats that are
supported by your hardware. The second method consumes the contents of a
basis-encoded KTX2 file and attempts to produce a Filament texture with
a preferred format.

IMPORTANT: Our tools still let you use KTX1 for non-compressed images
because it is useful for HDR, but you can no longer use KTX1 for
block-compressed data.

Partial fix for #4771.
2022-04-15 10:48:48 -07:00

355 lines
14 KiB
JavaScript

/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ---------------
// Buffer Wrappers
// ---------------
// These wrappers make it easy for JavaScript clients to pass large swaths of data to Filament. They
// copy the contents of the given typed array into the WASM heap, then return a low-level buffer
// descriptor object. If the given array was taken from the WASM heap, then they create a temporary
// copy because the input pointer becomes invalidated after allocating heap memory for the buffer
// descriptor.
/// Buffer ::function:: Constructs a [BufferDescriptor] by copying a typed array into the WASM heap.
/// typedarray ::argument:: Data to consume (e.g. Uint8Array, Uint16Array, Float32Array)
/// ::retval:: [BufferDescriptor]
Filament.Buffer = function(typedarray) {
console.assert(typedarray.buffer instanceof ArrayBuffer);
console.assert(typedarray.byteLength > 0);
// The only reason we need to create a copy here is that emscripten might "grow" its entire heap
// (i.e. destroy and recreate) during the allocation of the BufferDescriptor, which would cause
// detachment if the source array happens to be view into the old emscripten heap.
const ta = typedarray.slice();
const bd = new Filament.driver$BufferDescriptor(ta.byteLength);
const uint8array = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
// getBytes() returns a view into the emscripten heap, this just does a memcpy into it.
bd.getBytes().set(uint8array);
return bd;
};
/// PixelBuffer ::function:: Constructs a [PixelBufferDescriptor] by copying a typed array into \
/// the WASM heap.
/// typedarray ::argument:: Data to consume (e.g. Uint8Array, Uint16Array, Float32Array)
/// format ::argument:: [PixelDataFormat]
/// datatype ::argument:: [PixelDataType]
/// ::retval:: [PixelBufferDescriptor]
Filament.PixelBuffer = function(typedarray, format, datatype) {
console.assert(typedarray.buffer instanceof ArrayBuffer);
console.assert(typedarray.byteLength > 0);
const ta = typedarray.slice();
const bd = new Filament.driver$PixelBufferDescriptor(ta.byteLength, format, datatype);
const uint8array = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
bd.getBytes().set(uint8array);
return bd;
};
/// CompressedPixelBuffer ::function:: Constructs a [PixelBufferDescriptor] for compressed texture
/// data by copying a typed array into the WASM heap.
/// typedarray ::argument:: Data to consume (e.g. Uint8Array, Uint16Array, Float32Array)
/// cdatatype ::argument:: [CompressedPixelDataType]
/// faceSize ::argument:: Number of bytes in each face (cubemaps only)
/// ::retval:: [PixelBufferDescriptor]
Filament.CompressedPixelBuffer = function(typedarray, cdatatype, faceSize) {
console.assert(typedarray.buffer instanceof ArrayBuffer);
console.assert(typedarray.byteLength > 0);
faceSize = faceSize || typedarray.byteLength;
const ta = typedarray.slice();
const bd = new Filament.driver$PixelBufferDescriptor(ta.byteLength, cdatatype, faceSize, true);
const uint8array = new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
bd.getBytes().set(uint8array);
return bd;
};
Filament._loadFilamesh = function(engine, buffer, definstance, matinstances) {
matinstances = matinstances || {};
const registry = new Filament.MeshReader$MaterialRegistry();
for (let key in matinstances) {
registry.set(key, matinstances[key]);
}
if (definstance) {
registry.set("DefaultMaterial", definstance);
}
const mesh = Filament.MeshReader.loadMeshFromBuffer(engine, buffer, registry);
const keys = registry.keys();
for (let i = 0; i < keys.size(); i++) {
const key = keys.get(i);
const minstance = registry.get(key);
matinstances[key] = minstance;
}
return {
"renderable": mesh.renderable(),
"vertexBuffer": mesh.vertexBuffer(),
"indexBuffer": mesh.indexBuffer(),
}
}
// ------------------
// Geometry Utilities
// ------------------
/// IcoSphere ::class:: Utility class for constructing spheres (requires glMatrix).
///
/// The constructor takes an integer subdivision level, with 0 being an icosahedron.
///
/// Exposes three arrays as properties:
///
/// - `icosphere.vertices` Float32Array of XYZ coordinates.
/// - `icosphere.tangents` Uint16Array (interpreted as half-floats) encoding the surface orientation
/// as quaternions.
/// - `icosphere.triangles` Uint16Array with triangle indices.
///
Filament.IcoSphere = function(nsubdivs) {
const X = .525731112119133606;
const Z = .850650808352039932;
const N = 0.;
this.vertices = new Float32Array([
-X, +N, +Z, +X, +N, +Z, -X, +N, -Z, +X, +N, -Z ,
+N, +Z, +X, +N, +Z, -X, +N, -Z, +X, +N, -Z, -X ,
+Z, +X, +N, -Z, +X, +N, +Z, -X, +N, -Z, -X, +N ,
]);
this.triangles = new Uint16Array([
1, 4, 0, 4, 9, 0, 4, 5, 9, 8, 5, 4 , 1, 8, 4 ,
1, 10, 8, 10, 3, 8, 8, 3, 5, 3, 2, 5 , 3, 7, 2 ,
3, 10, 7, 10, 6, 7, 6, 11, 7, 6, 0, 11 , 6, 1, 0 ,
10, 1, 6, 11, 0, 9, 2, 11, 9, 5, 2, 9 , 11, 2, 7 ,
]);
nsubdivs = nsubdivs || 0;
while (nsubdivs-- > 0) {
this.subdivide();
}
const nverts = this.vertices.length / 3;
// This is a unit sphere, so normals = positions.
const normals = this.vertices;
// Perform computations.
const sob = new Filament.SurfaceOrientation$Builder();
sob.vertexCount(nverts);
sob.normals(normals, 0)
const orientation = sob.build();
// Copy the results out of the helper.
this.tangents = orientation.getQuats(nverts);
// Free up the surface orientation helper now that we're done with it.
orientation.delete();
}
Filament.IcoSphere.prototype.subdivide = function() {
const srctris = this.triangles;
const srcverts = this.vertices;
const nsrctris = srctris.length / 3;
const ndsttris = nsrctris * 4;
const nsrcverts = srcverts.length / 3;
const ndstverts = nsrcverts + nsrctris * 3;
const dsttris = new Uint16Array(ndsttris * 3);
const dstverts = new Float32Array(ndstverts * 3);
dstverts.set(srcverts);
let srcind = 0, dstind = 0, i3 = nsrcverts * 3, i4 = i3 + 3, i5 = i4 + 3;
for (let tri = 0; tri < nsrctris; tri++, i3 += 9, i4 += 9, i5 += 9) {
const i0 = srctris[srcind++] * 3;
const i1 = srctris[srcind++] * 3;
const i2 = srctris[srcind++] * 3;
const v0 = srcverts.subarray(i0, i0 + 3);
const v1 = srcverts.subarray(i1, i1 + 3);
const v2 = srcverts.subarray(i2, i2 + 3);
const v3 = dstverts.subarray(i3, i3 + 3);
const v4 = dstverts.subarray(i4, i4 + 3);
const v5 = dstverts.subarray(i5, i5 + 3);
vec3.normalize(v3, vec3.add(v3, v0, v1));
vec3.normalize(v4, vec3.add(v4, v1, v2));
vec3.normalize(v5, vec3.add(v5, v2, v0));
dsttris[dstind++] = i0 / 3;
dsttris[dstind++] = i3 / 3;
dsttris[dstind++] = i5 / 3;
dsttris[dstind++] = i3 / 3;
dsttris[dstind++] = i1 / 3;
dsttris[dstind++] = i4 / 3;
dsttris[dstind++] = i5 / 3;
dsttris[dstind++] = i3 / 3;
dsttris[dstind++] = i4 / 3;
dsttris[dstind++] = i2 / 3;
dsttris[dstind++] = i5 / 3;
dsttris[dstind++] = i4 / 3;
}
this.triangles = dsttris;
this.vertices = dstverts;
}
// ---------------
// Math Extensions
// ---------------
function clamp(v, least, most) {
return Math.max(Math.min(most, v), least);
}
/// packSnorm16 ::function:: Converts a float in [-1, +1] into a half-float.
/// value ::argument:: float
/// ::retval:: half-float
Filament.packSnorm16 = function(value) {
return Math.round(clamp(value, -1.0, 1.0) * 32767.0);
}
/// loadMathExtensions ::function:: Extends the [glMatrix](http://glmatrix.net/) math library.
/// Filament does not require its clients to use glMatrix, but if its usage is detected then
/// the [init] function will automatically call `loadMathExtensions`.
/// This defines the following functions:
/// - **vec4.packSnorm16** can be used to create half-floats (see [packSnorm16])
/// - **mat3.fromRotation** now takes an arbitrary axis
Filament.loadMathExtensions = function() {
vec4.packSnorm16 = function(out, src) {
out[0] = Filament.packSnorm16(src[0]);
out[1] = Filament.packSnorm16(src[1]);
out[2] = Filament.packSnorm16(src[2]);
out[3] = Filament.packSnorm16(src[3]);
return out;
}
// In gl-matrix, mat3 rotation assumes rotation about the Z axis, so here we add a function
// to allow an arbitrary axis.
const fromRotationZ = mat3.fromRotation;
mat3.fromRotation = function(out, radians, axis) {
if (axis) {
return mat3.fromMat4(out, mat4.fromRotation(mat4.create(), radians, axis));
}
return fromRotationZ(out, radians);
};
};
// ---------------
// Texture helpers
// ---------------
Filament._createTextureFromKtx1 = function(ktxdata, engine, options) {
options = options || {};
const ktx = options['ktx'] || new Filament.Ktx1Bundle(ktxdata);
const srgb = !!options['srgb'];
return Filament.ktx1reader$createTexture(engine, ktx, srgb);
};
Filament._createIblFromKtx1 = function(ktxdata, engine, options) {
options = options || {};
const iblktx = options['ktx'] = new Filament.Ktx1Bundle(ktxdata);
const format = iblktx.info().glInternalFormat;
//if (format != this.ctx.R11F_G11F_B10F && format != this.ctx.RGB16F && format != this.ctx.RGB32F) {
if (format != 35898 && format != 33327 && format != 34837) {
console.warn('IBL texture format is 0x' + format.toString(16) +
' which is not an expected floating-point format. Please use cmgen to generate IBL.');
}
const ibltex = Filament._createTextureFromKtx1(ktxdata, engine, options);
const shstring = iblktx.getMetadata("sh");
const ibl = Filament.IndirectLight.Builder()
.reflections(ibltex)
.build(engine);
ibl.shfloats = shstring.split(/\s/, 9 * 3).map(parseFloat);
return ibl;
};
Filament._createTextureFromImageFile = function(fileContents, engine, options) {
const Sampler = Filament.Texture$Sampler;
const TextureFormat = Filament.Texture$InternalFormat;
const PixelDataFormat = Filament.PixelDataFormat;
options = options || {};
const srgb = !!options['srgb'];
const noalpha = !!options['noalpha'];
const nomips = !!options['nomips'];
const decodedImage = Filament.decodeImage(fileContents, noalpha ? 3 : 4);
let texformat, pbformat, pbtype;
if (noalpha) {
texformat = srgb ? TextureFormat.SRGB8 : TextureFormat.RGB8;
pbformat = PixelDataFormat.RGB;
pbtype = Filament.PixelDataType.UBYTE;
} else {
texformat = srgb ? TextureFormat.SRGB8_A8 : TextureFormat.RGBA8;
pbformat = PixelDataFormat.RGBA;
pbtype = Filament.PixelDataType.UBYTE;
}
const tex = Filament.Texture.Builder()
.width(decodedImage.width)
.height(decodedImage.height)
.levels(nomips ? 1 : 0xff)
.sampler(Sampler.SAMPLER_2D)
.format(texformat)
.build(engine);
const pixelbuffer = Filament.PixelBuffer(decodedImage.data.getBytes(), pbformat, pbtype);
tex.setImage(engine, 0, pixelbuffer);
if (!nomips) {
tex.generateMipmaps(engine);
}
return tex;
};
/// getSupportedFormats ::function:: Queries WebGL to check which compressed formats are supported.
/// ::retval:: object with boolean values and the following keys: s3tc, astc, etc
Filament.getSupportedFormats = function() {
if (Filament.supportedFormats) {
return Filament.supportedFormats;
}
const options = { majorVersion: 2, minorVersion: 0 };
let ctx = document.createElement('canvas').getContext('webgl2', options);
const result = {
s3tc: false,
s3tc_srgb: false,
astc: false,
etc: false,
}
let exts = ctx.getSupportedExtensions(), nexts = exts.length, i;
for (i = 0; i < nexts; i++) {
let ext = exts[i];
if (ext == "WEBGL_compressed_texture_s3tc") {
result.s3tc = true;
} else if (ext == "WEBGL_compressed_texture_s3tc_srgb") {
result.s3tc_srgb = true;
} else if (ext == "WEBGL_compressed_texture_astc") {
result.astc = true;
} else if (ext == "WEBGL_compressed_texture_etc") {
result.etc = true;
}
}
return Filament.supportedFormats = result;
}
/// getSupportedFormatSuffix ::function:: Generate a file suffix according to the texture format.
/// Consumes a string describing desired formats and produces a file suffix depending on
/// which (if any) of the formats are actually supported by the WebGL implementation. This is
/// useful for compressed textures. For example, some platforms accept ETC and others accept S3TC.
/// desiredFormats ::argument:: space-delimited string of desired formats
/// ::retval:: empty string if there is no intersection of supported and desired formats.
Filament.getSupportedFormatSuffix = function(desiredFormats) {
desiredFormats = desiredFormats.split(' ');
let exts = Filament.getSupportedFormats();
for (let key in exts) {
if (exts[key] && desiredFormats.includes(key)) {
return '_' + key;
}
}
return '';
}