From cebc83e8faccd372d4a583942d8dce017f7f755d Mon Sep 17 00:00:00 2001 From: Romain Guy Date: Tue, 15 Jan 2019 16:11:34 -0800 Subject: [PATCH] Fix the Android filamesh file loader (#709) * Fix the Android filamesh file loader The loader was not updated to support the SNORM16 format now sometimes used to encode UV sets in filamesh files. Fixes #708 * Update android/samples/image-based-lighting/app/src/main/java/com/google/android/filament/ibl/MeshLoader.kt --- .../google/android/filament/ibl/MeshLoader.kt | 35 +++++++++++++++---- .../android/filament/textured/MeshLoader.kt | 35 +++++++++++++++---- tools/filamesh/README.md | 19 ++++++++-- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/android/samples/image-based-lighting/app/src/main/java/com/google/android/filament/ibl/MeshLoader.kt b/android/samples/image-based-lighting/app/src/main/java/com/google/android/filament/ibl/MeshLoader.kt index 7012286205..165bc2b9ac 100644 --- a/android/samples/image-based-lighting/app/src/main/java/com/google/android/filament/ibl/MeshLoader.kt +++ b/android/samples/image-based-lighting/app/src/main/java/com/google/android/filament/ibl/MeshLoader.kt @@ -69,12 +69,16 @@ fun loadMesh(assets: AssetManager, name: String, private const val FILAMESH_FILE_IDENTIFIER = "FILAMESH" private const val MAX_UINT32 = 4294967295 +private const val HEADER_FLAG_INTERLEAVED = 0x1L +private const val HEADER_FLAG_SNORM16_UV = 0x2L +private const val HEADER_FLAG_COMPRESSED = 0x4L + private class Header { var valid = false var versionNumber = 0L var parts = 0L var aabb = Box() - var interleaved = 0L + var flags = 0L var posOffset = 0L var positionStride = 0L var tangentOffset = 0L @@ -121,7 +125,7 @@ private fun readHeader(input: InputStream): Header { header.aabb = Box( readFloat32LE(input), readFloat32LE(input), readFloat32LE(input), readFloat32LE(input), readFloat32LE(input), readFloat32LE(input)) - header.interleaved = readUIntLE(input) + header.flags = readUIntLE(input) header.posOffset = readUIntLE(input) header.positionStride = readUIntLE(input) header.tangentOffset = readUIntLE(input) @@ -142,7 +146,6 @@ private fun readHeader(input: InputStream): Header { return header } - private fun readSizedData(channel: ReadableByteChannel, sizeInBytes: Long): ByteBuffer { val buffer = ByteBuffer.allocateDirect(sizeInBytes.toInt()) buffer.order(ByteOrder.LITTLE_ENDIAN) @@ -190,7 +193,15 @@ private fun createIndexBuffer(engine: Engine, header: Header, data: ByteBuffer): .apply { setBuffer(engine, data) } } +private fun uvNormalized(header: Header) = header.flags and HEADER_FLAG_SNORM16_UV != 0L + private fun createVertexBuffer(engine: Engine, header: Header, data: ByteBuffer): VertexBuffer { + val uvType = if (!uvNormalized(header)) { + HALF2 + } else { + SHORT2 + } + val vertexBufferBuilder = VertexBuffer.Builder() .bufferCount(1) .vertexCount(header.totalVertices.toInt()) @@ -203,11 +214,23 @@ private fun createVertexBuffer(engine: Engine, header: Header, data: ByteBuffer) .attribute(POSITION, 0, HALF4, header.posOffset.toInt(), header.positionStride.toInt()) .attribute(TANGENTS, 0, SHORT4, header.tangentOffset.toInt(), header.tangentStride.toInt()) .attribute(COLOR, 0, UBYTE4, header.colorOffset.toInt(), header.colorStride.toInt()) - .attribute(UV0, 0, HALF2, header.uv0Offset.toInt(), header.uv0Stride.toInt()) + // UV coordinates are stored as normalized 16-bit integers or half-floats depending on + // the range they span. When stored as half-float, there is only enough precision for + // sub-pixel addressing in textures that are <= 1024x1024 + .attribute(UV0, 0, uvType, header.uv0Offset.toInt(), header.uv0Stride.toInt()) + + // When UV coordinates are stored as 16-bit integers we must normalize them (we want values + // in the range -1..1) + if (uvNormalized(header)) { + vertexBufferBuilder.normalized(UV0) + } if (header.uv1Offset != MAX_UINT32 && header.uv1Stride != MAX_UINT32) { vertexBufferBuilder - .attribute(UV1, 0, HALF2, header.uv1Offset.toInt(), header.uv1Stride.toInt()) + .attribute(UV1, 0, uvType, header.uv1Offset.toInt(), header.uv1Stride.toInt()) + if (uvNormalized(header)) { + vertexBufferBuilder.normalized(UV1) + } } return vertexBufferBuilder.build(engine).apply { setBufferAt(engine, 0, data) } @@ -224,7 +247,7 @@ private fun createRenderable( val builder = RenderableManager.Builder(header.parts.toInt()).boundingBox(header.aabb) - (0 until header.parts.toInt()).forEach { i -> + repeat(header.parts.toInt()) { i -> builder.geometry(i, RenderableManager.PrimitiveType.TRIANGLES, vertexBuffer, diff --git a/android/samples/textured-object/app/src/main/java/com/google/android/filament/textured/MeshLoader.kt b/android/samples/textured-object/app/src/main/java/com/google/android/filament/textured/MeshLoader.kt index a2fdfd1019..e412179c78 100644 --- a/android/samples/textured-object/app/src/main/java/com/google/android/filament/textured/MeshLoader.kt +++ b/android/samples/textured-object/app/src/main/java/com/google/android/filament/textured/MeshLoader.kt @@ -69,12 +69,16 @@ fun loadMesh(assets: AssetManager, name: String, private const val FILAMESH_FILE_IDENTIFIER = "FILAMESH" private const val MAX_UINT32 = 4294967295 +private const val HEADER_FLAG_INTERLEAVED = 0x1L +private const val HEADER_FLAG_SNORM16_UV = 0x2L +private const val HEADER_FLAG_COMPRESSED = 0x4L + private class Header { var valid = false var versionNumber = 0L var parts = 0L var aabb = Box() - var interleaved = 0L + var flags = 0L var posOffset = 0L var positionStride = 0L var tangentOffset = 0L @@ -121,7 +125,7 @@ private fun readHeader(input: InputStream): Header { header.aabb = Box( readFloat32LE(input), readFloat32LE(input), readFloat32LE(input), readFloat32LE(input), readFloat32LE(input), readFloat32LE(input)) - header.interleaved = readUIntLE(input) + header.flags = readUIntLE(input) header.posOffset = readUIntLE(input) header.positionStride = readUIntLE(input) header.tangentOffset = readUIntLE(input) @@ -142,7 +146,6 @@ private fun readHeader(input: InputStream): Header { return header } - private fun readSizedData(channel: ReadableByteChannel, sizeInBytes: Long): ByteBuffer { val buffer = ByteBuffer.allocateDirect(sizeInBytes.toInt()) buffer.order(ByteOrder.LITTLE_ENDIAN) @@ -190,7 +193,15 @@ private fun createIndexBuffer(engine: Engine, header: Header, data: ByteBuffer): .apply { setBuffer(engine, data) } } +private fun uvNormalized(header: Header) = header.flags and HEADER_FLAG_SNORM16_UV != 0L + private fun createVertexBuffer(engine: Engine, header: Header, data: ByteBuffer): VertexBuffer { + val uvType = if (!uvNormalized(header)) { + HALF2 + } else { + SHORT2 + } + val vertexBufferBuilder = VertexBuffer.Builder() .bufferCount(1) .vertexCount(header.totalVertices.toInt()) @@ -203,13 +214,23 @@ private fun createVertexBuffer(engine: Engine, header: Header, data: ByteBuffer) .attribute(POSITION, 0, HALF4, header.posOffset.toInt(), header.positionStride.toInt()) .attribute(TANGENTS, 0, SHORT4, header.tangentOffset.toInt(), header.tangentStride.toInt()) .attribute(COLOR, 0, UBYTE4, header.colorOffset.toInt(), header.colorStride.toInt()) - // UV coordinates are stored in fp16, which gives sub-pixel precision only - // for textures up to 1024x1024 - .attribute(UV0, 0, HALF2, header.uv0Offset.toInt(), header.uv0Stride.toInt()) + // UV coordinates are stored as normalized 16-bit integers or half-floats depending on + // the range they span. When stored as half-float, there is only enough precision for + // sub-pixel addressing in textures that are <= 1024x1024 + .attribute(UV0, 0, uvType, header.uv0Offset.toInt(), header.uv0Stride.toInt()) + + // When UV coordinates are stored as 16-bit integeres we must normalize them (we want values + // in the range -1..1) + if (uvNormalized(header)) { + vertexBufferBuilder.normalized(UV0) + } if (header.uv1Offset != MAX_UINT32 && header.uv1Stride != MAX_UINT32) { vertexBufferBuilder - .attribute(UV1, 0, HALF2, header.uv1Offset.toInt(), header.uv1Stride.toInt()) + .attribute(UV1, 0, uvType, header.uv1Offset.toInt(), header.uv1Stride.toInt()) + if (uvNormalized(header)) { + vertexBufferBuilder.normalized(UV1) + } } return vertexBufferBuilder.build(engine).apply { setBufferAt(engine, 0, data) } diff --git a/tools/filamesh/README.md b/tools/filamesh/README.md index 87802b76b8..0d103298bd 100644 --- a/tools/filamesh/README.md +++ b/tools/filamesh/README.md @@ -198,6 +198,14 @@ Mesh loadMeshFromFile(filament::Engine* engine, const utils::Path& path, mesh.indexBuffer->setBuffer(*engine, IndexBuffer::BufferDescriptor(indices, header->indexSize)); + const uint32_t FLAG_SNORM16_UV = 0x2; + + VertexBuffer::AttributeType::HALF2 uvType = VertexBuffer::AttributeType::HALF2; + if (header->flags & FLAG_SNORM16_UV) { + uvType = VertexBuffer::AttributeType::SHORT2; + } + bool uvNormalized = header->flags & FLAG_SNORM16_UV; + VertexBuffer::Builder vbb; vbb.vertexCount(header->vertexCount) .bufferCount(1) @@ -209,13 +217,20 @@ Mesh loadMeshFromFile(filament::Engine* engine, const utils::Path& path, header->offsetTangents, uint8_t(header->strideTangents)) .attribute(VertexAttribute::COLOR, 0, VertexBuffer::AttributeType::UBYTE4, header->offsetColor, uint8_t(header->strideColor)) - .attribute(VertexAttribute::UV0, 0, VertexBuffer::AttributeType::HALF2, + .attribute(VertexAttribute::UV0, 0, uvType, header->offsetUV0, uint8_t(header->strideUV0)); + if (uvNormalized) { + vbb.normalized(VertexAttribute::UV0); + } + if (header->offsetUV1 != std::numeric_limits::max() && header->strideUV1 != std::numeric_limits::max()) { - vbb.attribute(VertexAttribute::UV1, 0, VertexBuffer::AttributeType::HALF2, + vbb.attribute(VertexAttribute::UV1, 0, uvType, header->offsetUV1, uint8_t(header->strideUV1)); + if (uvNormalized) { + vbb.normalized(VertexAttribute::UV1); + } } mesh.vertexBuffer = vbb.build(*engine);