Comparison with blank skybox.

This commit is contained in:
Matt Hoffman
2025-06-26 14:35:16 -05:00
committed by Juan Caldas
parent 433fded025
commit bbea03224d
23 changed files with 1202 additions and 2 deletions

View File

@@ -287,6 +287,9 @@ set(FILAMENT ${CMAKE_CURRENT_SOURCE_DIR})
# Where our tools are
set(TOOLS ${CMAKE_CURRENT_SOURCE_DIR}/tools)
# Where our tools are
set(TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test)
# ==================================================================================================
# Compiler check
# ==================================================================================================
@@ -896,6 +899,8 @@ if (IS_HOST_PLATFORM)
add_subdirectory(${TOOLS}/roughness-prefilter)
add_subdirectory(${TOOLS}/specular-color)
add_subdirectory(${TOOLS}/uberz)
add_subdirectory(${TESTS}/gltf-comparison)
endif()
# Generate exported executables for cross-compiled builds (Android, WebGL, and iOS)

View File

@@ -21,9 +21,9 @@
#include "utils/Hash.h"
#include <fstream>
#include "BackendTest.h"
//#include "BackendTest.h"
#include "backend/PixelBufferDescriptor.h"
#include "private/backend/DriverApi.h"
//#include "private/backend/DriverApi.h"
#ifndef FILAMENT_IOS

View File

@@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.19)
project(filament C ASM)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(GLTF_COMPARISON_SOURCES
src/test_CompareGLTF.cpp
src/GLTFViewer.cpp
src/ImageExpectations.cpp
src/main.cpp
)
file(COPY glTF_cases DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/glTF_cases)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(COPY expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
enable_testing()
add_executable(gltf_comparison ${GLTF_COMPARISON_SOURCES})
target_include_directories(gltf_comparison PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(gltf_comparison
gtest
filamentapp
gltfio
absl::str_format
)
include(GoogleTest)
gtest_discover_tests(gltf_comparison)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,15 @@
# LICENSE file for the model: Box
All files in this directory tree are licensed as indicated below.
* All files directly associated with the model including all text, image and binary files:
* [CC BY 4.0 International]("https://creativecommons.org/licenses/by/4.0/legalcode") [SPDX license identifier: "CC-BY-4.0"]
* This file and all other metadocumentation files including "metadata.json":
* [Creative Commons Attribtution 4.0 International]("https://creativecommons.org/licenses/by/4.0/legalcode") [SPDX license identifier: "CC-BY-4.0"]
Full license text of these licenses are available at the links above
#### Generated by modelmetadata

View File

@@ -0,0 +1,7 @@
## Screenshot
![screenshot](screenshot/screenshot.png)
## Description
Simple cube model.

View File

@@ -0,0 +1,31 @@
# Box
## Tags
[core](../../Models-core.md), [testing](../../Models-testing.md)
## Summary
One mesh and one material. Start with this.
## Operations
* [Display](https://github.khronos.org/glTF-Sample-Viewer-Release/?model=https://raw.GithubUserContent.com/KhronosGroup/glTF-Sample-Assets/main/./Models/Box/glTF-Binary/Box.glb) in SampleViewer
* [Download GLB](https://raw.GithubUserContent.com/KhronosGroup/glTF-Sample-Assets/main/./Models/Box/glTF-Binary/Box.glb)
* [Model Directory](./)
## Screenshot
![screenshot](screenshot/screenshot.png)
## Description
Simple cube model.
## Legal
&copy; 2017, Cesium. [CC BY 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode)
- Cesium for Everything
#### Assembled by modelmetadata

View File

@@ -0,0 +1,152 @@
{
"asset": {
"generator": "COLLADA2GLTF",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1,
0,
0,
0,
0,
0,
-1,
0,
0,
1,
0,
0,
0,
0,
0,
1
]
},
{
"mesh": 0
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 2
},
"indices": 0,
"mode": 4,
"material": 0,
"extensions": {
"KHR_draco_mesh_compression": {
"bufferView": 0,
"attributes": {
"NORMAL": 0,
"POSITION": 1
}
}
}
}
],
"name": "Mesh"
}
],
"accessors": [
{
"componentType": 5123,
"count": 36,
"max": [
23
],
"min": [
0
],
"type": "SCALAR"
},
{
"componentType": 5126,
"count": 24,
"max": [
1.007843137254902,
1.007843137254902,
1.007843137254902
],
"min": [
-1.007843137254902,
-1.007843137254902,
-1.007843137254902
],
"type": "VEC3"
},
{
"componentType": 5126,
"count": 24,
"max": [
0.5004885197850513,
0.5004885197850513,
0.5004885197850513
],
"min": [
-0.5004885197850513,
-0.5004885197850513,
-0.5004885197850513
],
"type": "VEC3"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorFactor": [
0.800000011920929,
0,
0,
1
],
"metallicFactor": 0,
"roughnessFactor": 1
},
"name": "Red",
"emissiveFactor": [
0,
0,
0
],
"alphaMode": "OPAQUE",
"doubleSided": false
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 118
}
],
"buffers": [
{
"name": "Box",
"byteLength": 120,
"uri": "Box.bin"
}
],
"extensionsRequired": [
"KHR_draco_mesh_compression"
],
"extensionsUsed": [
"KHR_draco_mesh_compression"
]
}

View File

@@ -0,0 +1,142 @@
{
"asset": {
"generator": "COLLADA2GLTF",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
-1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0
]
},
{
"mesh": 0
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 2
},
"indices": 0,
"mode": 4,
"material": 0
}
],
"name": "Mesh"
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5123,
"count": 36,
"max": [
23
],
"min": [
0
],
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 24,
"max": [
1.0,
1.0,
1.0
],
"min": [
-1.0,
-1.0,
-1.0
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 288,
"componentType": 5126,
"count": 24,
"max": [
0.5,
0.5,
0.5
],
"min": [
-0.5,
-0.5,
-0.5
],
"type": "VEC3"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorFactor": [
0.800000011920929,
0.0,
0.0,
1.0
],
"metallicFactor": 0.0
},
"name": "Red"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 576,
"byteLength": 72,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 576,
"byteStride": 12,
"target": 34962
}
],
"buffers": [
{
"byteLength": 648,
"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAABAAIAAwACAAEABAAFAAYABwAGAAUACAAJAAoACwAKAAkADAANAA4ADwAOAA0AEAARABIAEwASABEAFAAVABYAFwAWABUA"
}
]
}

View File

@@ -0,0 +1,142 @@
{
"asset": {
"generator": "COLLADA2GLTF",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
-1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0
]
},
{
"mesh": 0
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 2
},
"indices": 0,
"mode": 4,
"material": 0
}
],
"name": "Mesh"
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5123,
"count": 36,
"max": [
23
],
"min": [
0
],
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 24,
"max": [
1.0,
1.0,
1.0
],
"min": [
-1.0,
-1.0,
-1.0
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 288,
"componentType": 5126,
"count": 24,
"max": [
0.5,
0.5,
0.5
],
"min": [
-0.5,
-0.5,
-0.5
],
"type": "VEC3"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorFactor": [
0.800000011920929,
0.0,
0.0,
1.0
],
"metallicFactor": 0.0
},
"name": "Red"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 576,
"byteLength": 72,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 576,
"byteStride": 12,
"target": 34962
}
],
"buffers": [
{
"byteLength": 648,
"uri": "Box0.bin"
}
]
}

View File

@@ -0,0 +1,25 @@
{
"version": 2,
"legal": [
{
"license": "CC-BY 4.0",
"licenseUrl": "https://creativecommons.org/licenses/by/4.0/legalcode",
"artist": "Cesium",
"year": "2017",
"owner": "Cesium",
"what": "Everything",
"text": "CC BY 4.0 International",
"spdx": "CC-BY-4.0",
"icon": "https://licensebuttons.net/l/by/3.0/88x31.png"
}
],
"tags": [
"core",
"testing"
],
"screenshot": "screenshot/screenshot.png",
"name": "Box",
"path": "./Models/Box",
"summary": "One mesh and one material. Start with this.",
"createReadme": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,3 @@
Files here are copied directly from https://github.com/KhronosGroup/glTF-Sample-Assets
Each glTF model includes its own license and attribution in LICENSE.md and metadata.json.

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2025 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.
*/
#ifndef TNT_GLTFVIEWER_H
#define TNT_GLTFVIEWER_H
#endif // TNT_GLTFVIEWER_H

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2025 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.
*/
#ifndef TNT_IMAGE_EXPECTATIONS_H
#define TNT_IMAGE_EXPECTATIONS_H
#include <vector>
#include <backend/PixelBufferDescriptor.h>
#include <filament/Renderer.h>
#include "gtest/gtest.h"
// Arguments are (Renderer* renderer, ImageExpectations& expectations,
// ScreenshotParams screenshotParams)
#define EXPECT_IMAGE(renderer, expectations, screenshotParams) \
do { \
expectations.addExpectation( \
__FILE__, \
__LINE__, \
renderer, \
screenshotParams); \
} while (0)
namespace test {
/**
* Stores user-provided configuration values for an image expectation
*/
class ScreenshotParams {
public:
// TODO(b/422804941): Add a set of environments where this test should use a different golden.
ScreenshotParams(int width, int height, std::string fileName,
bool isSrgb = false);
int width() const;
int height() const;
bool isSrgb() const;
static std::string actualDirectoryPath();
std::string actualFileName() const;
std::string actualFilePath() const;
static std::string expectedDirectoryPath();
std::string expectedFileName() const;
std::string expectedFilePath() const;
const std::string filePrefix() const;
private:
int mWidth;
int mHeight;
bool mIsSrgb;
std::string mFileName;
};
/**
* When created adds a command to the GPU pipeline to copy the render target into a buffer and
* stores the result.
* If this object is destroyed before the GPU pipeline is flushed it will leak memory in order to
* avoid the GPU pipeline callback being a use-after-free.
*/
class RenderTargetDump {
public:
using ReadPixels = std::function<void(uint32_t, uint32_t, uint32_t, uint32_t,
filament::backend::PixelBufferDescriptor)>;
static ReadPixels readFromRenderer(filament::Renderer* renderer);
RenderTargetDump(const ReadPixels& readPixels, const ScreenshotParams& params);
RenderTargetDump(RenderTargetDump&& other) = default;
RenderTargetDump& operator=(RenderTargetDump&& other) = default;
~RenderTargetDump();
/**
* Should only bue used if BytesFilled returns true.
* @return The hash of the stored bytes.
*/
uint32_t hash() const;
/**
* Gets the bytes of the render target. The hash should usually be preferable for comparisons
* but this is available for debugging.
* @return The stored bytes.
*/
const std::vector<unsigned char>& bytes() const;
/**
* Thread safe as this is backed by an atomic.
* Once this returns true it will never return false.
* @return Whether the bytes have actually been copied from the GPU to the buffer.
*/
bool bytesFilled() const;
private:
struct Internal {
explicit Internal(const ScreenshotParams& params);
ScreenshotParams params;
std::atomic<bool> bytesFilled = false;
std::vector<unsigned char> bytes;
uint32_t hash() const;
};
// We need a memory location that won't be invalidated to pass to GPU callbacks as they can't
// be canceled during the destructor.
std::unique_ptr<Internal> mInternal;
};
class LoadedPng {
public:
explicit LoadedPng(std::string filePath);
uint32_t hash() const;
const std::vector<unsigned char>& bytes() const;
private:
std::string mFilePath;
std::vector<unsigned char> mBytes;
};
class ImageExpectation {
public:
ImageExpectation(const char* fileName, int lineNumber, RenderTargetDump::ReadPixels readPixels,
ScreenshotParams params);
void evaluate();
private:
void compareImage() const;
bool mEvaluated = false;
const char* mFileName;
int mLineNumber;
ScreenshotParams mParams;
RenderTargetDump mResult;
};
class ImageExpectations {
public:
explicit ImageExpectations() = default;
~ImageExpectations();
/**
* Not meant to be called directly, use EXPECT_IMAGE to get the file name and line number
*/
void addExpectation(const char* fileName, int lineNumber,
filament::Renderer* renderer, ScreenshotParams params);
void evaluate();
static void markImageAsFailure(const std::string& imagePrefix);
private:
// Store expectations in unique pointers because they are self referential.
std::vector<std::unique_ptr<ImageExpectation>> mExpectations;
};
} // namespace test
#endif //TNT_IMAGE_EXPECTATIONS_H

View File

@@ -0,0 +1,17 @@
/*
* Copyright (C) 2025 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.
*/
#include "GLTFViewer.h"

View File

@@ -0,0 +1,248 @@
/*
* Copyright (C) 2025 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.
*/
#include "ImageExpectations.h"
#include "gmock/gmock.h"
#include "absl/strings/str_format.h"
#include "utils/Hash.h"
#include <fstream>
#include <imageio/ImageEncoder.h>
#include <imageio/ImageDecoder.h>
#include <image/ColorTransform.h>
namespace test {
ScreenshotParams::ScreenshotParams(int width, int height, std::string fileName, bool isSrgb)
: mWidth(width),
mHeight(height),
mIsSrgb(isSrgb),
mFileName(std::move(fileName)) {}
int ScreenshotParams::width() const {
return mWidth;
}
int ScreenshotParams::height() const {
return mHeight;
}
bool ScreenshotParams::isSrgb() const {
return mIsSrgb;
}
std::string ScreenshotParams::actualDirectoryPath() {
return "images/actual_images";
}
std::string ScreenshotParams::actualFileName() const {
return absl::StrFormat("%s_actual.png", mFileName);
}
std::string ScreenshotParams::actualFilePath() const {
return absl::StrFormat("%s/%s", actualDirectoryPath(), actualFileName());
}
std::string ScreenshotParams::expectedDirectoryPath() {
return "images/expected_images";
}
std::string ScreenshotParams::expectedFileName() const {
return absl::StrFormat("%s.png", mFileName);
}
std::string ScreenshotParams::expectedFilePath() const {
return absl::StrFormat("%s/%s", expectedDirectoryPath(), expectedFileName());
}
const std::string ScreenshotParams::filePrefix() const {
// TODO(b/422804941): If there are platform specific goldens, when on those platforms append a
// unique platform identifying string to this.
return mFileName;
}
ImageExpectation::ImageExpectation(const char* fileName, int lineNumber,
RenderTargetDump::ReadPixels readPixels, ScreenshotParams params)
: mFileName(fileName),
mLineNumber(lineNumber),
mParams(std::move(params)),
mResult(readPixels, mParams) {}
void ImageExpectation::evaluate() {
// Ensure this is only evaluated once.
if (mEvaluated) {
return;
}
mEvaluated = true;
// Do the actual image comparison inside a scoped trace with the stored file and line.
{
testing::ScopedTrace trace(mFileName, mLineNumber, "");
compareImage();
}
}
void ImageExpectation::compareImage() const {
bool bytesFilled = mResult.bytesFilled();
// If this fails, it likely means that BackendTest::flushAndWait needs to be called before
// ImageExpectations is evaluated or destroyed.
EXPECT_THAT(bytesFilled, testing::IsTrue())
<< "Render target wasn't copied to the buffer for " << mFileName;
if (bytesFilled) {
// Rather than directly compare the two images compare their hashes because comparing very
// large arrays generates way too much debug output to be useful.
uint32_t actualHash = mResult.hash();
#ifndef FILAMENT_IOS
LoadedPng loadedImage(mParams.expectedFilePath());
uint32_t loadedImageHash = loadedImage.hash();
auto compareToImageMatcher = testing::Eq(loadedImageHash);
if (!testing::Matches(compareToImageMatcher)(actualHash)) {
ImageExpectations::markImageAsFailure(mParams.filePrefix());
}
EXPECT_THAT(actualHash, compareToImageMatcher) << mParams.expectedFileName();
#endif
}
}
ImageExpectations::~ImageExpectations() {
// Guarantee that all expectations are evaluated when this leaves scope even if the caller
// forgot to manually evaluate them.
evaluate();
}
void ImageExpectations::addExpectation(const char* fileName, int lineNumber,
filament::Renderer* renderer, ScreenshotParams params) {
mExpectations.emplace_back(std::make_unique<ImageExpectation>(
fileName, lineNumber,
[renderer](uint32_t x, uint32_t y, uint32_t width, uint32_t height,
filament::backend::PixelBufferDescriptor&& pb) {
renderer->readPixels(x, y, width, height, std::move(pb));
},
std::move(params)));
}
void ImageExpectations::evaluate() {
for (auto& expectation: mExpectations) {
expectation->evaluate();
}
mExpectations.clear();
}
void ImageExpectations::markImageAsFailure(const std::string& imagePrefix) {
}
RenderTargetDump::RenderTargetDump(const RenderTargetDump::ReadPixels& readPixels,
const ScreenshotParams& params)
: mInternal(std::make_unique<RenderTargetDump::Internal>(params)) {
const size_t size = mInternal->params.width() * mInternal->params.height() * 4;
mInternal->bytes.resize(size);
auto cb = [](void* buffer, size_t size, void* user) {
auto* internal = static_cast<RenderTargetDump::Internal*>(user);
internal->bytesFilled = true;
#ifndef FILAMENT_IOS
image::LinearImage image;
if (internal->params.isSrgb()) {
image = image::toLinearWithAlpha<uint8_t>(internal->params.width(),
internal->params.height(),
internal->params.width() * 4, (uint8_t*)buffer);
} else {
// The image data is already linear, so pass in transforms that simply go from uint8_t
// to float. toLinearWithAlpha divides the float values by uint8_t max so there's no
// need to scale it to [0, 1]
image = image::toLinearWithAlpha<uint8_t>(
internal->params.width(), internal->params.height(),
internal->params.width() * 4, (uint8_t*) buffer,
[](uint8_t value) -> float { return value; },
[](filament::math::float4 rgba) -> filament::math::float4 { return rgba; });
}
std::string filePath = internal->params.actualFilePath();
std::ofstream pngStream(filePath, std::ios::binary | std::ios::trunc);
// To avoid going from linear -> sRGB -> linear save the PNG as linear.
image::ImageEncoder::encode(pngStream, image::ImageEncoder::Format::PNG_LINEAR, image, "",
filePath);
#endif
};
filament::backend::PixelBufferDescriptor pb(mInternal->bytes.data(), size,
filament::backend::PixelDataFormat::RGBA, filament::backend::PixelDataType::UBYTE, cb,
(void*)mInternal.get());
readPixels(0, 0, mInternal->params.width(), mInternal->params.height(), std::move(pb));
}
RenderTargetDump::~RenderTargetDump() {
// If the GPU callback hasn't been made yet then there's a callback elsewhere that has a copy of
// the internal pointer. But there's no guarantee that the callback will be ever made if the GPU
// pipeline wasn't run for some reason. So it is necessary to leak the memory.
// It would be possible to try to coordinate with the callback to have it clean up the memory,
// but if this condition happens there's already an issue with the test case so there's no need.
if (!bytesFilled()) {
mInternal.release();
}
}
uint32_t RenderTargetDump::Internal::hash() const {
return utils::hash::murmur3((uint32_t*)bytes.data(), bytes.size() / 4, 0);
}
uint32_t RenderTargetDump::hash() const {
return mInternal->hash();
}
const std::vector<unsigned char>& RenderTargetDump::bytes() const {
return mInternal->bytes;
}
bool RenderTargetDump::bytesFilled() const {
return mInternal->bytesFilled;
}
RenderTargetDump::Internal::Internal(const ScreenshotParams& params) : params(params) {}
LoadedPng::LoadedPng(std::string filePath) : mFilePath(std::move(filePath)) {
#ifndef FILAMENT_IOS
std::ifstream pngStream(mFilePath, std::ios::binary);
image::LinearImage loadedImage = image::ImageDecoder::decode(pngStream, filePath,
image::ImageDecoder::ColorSpace::LINEAR);
size_t valuesInImage = loadedImage.getWidth() * loadedImage.getHeight() *
loadedImage.getChannels();
// The linear image is loaded with each component as [0.0, 1.0] but should be [0, 255], so
// convert them.
mBytes = std::vector<unsigned char>(valuesInImage);
for (int i = 0; i < valuesInImage; ++i) {
mBytes[i] = static_cast<uint8_t>(loadedImage.get<float>()[i] * 255.0f);
}
#endif
// For platforms that don't support the image loading library, leave the loaded data blank.
}
uint32_t LoadedPng::hash() const {
EXPECT_THAT(mBytes, testing::Not(testing::IsEmpty()))
<< "Failed to load expected test result: " << mFilePath << ".\n"
<< "Did you forget to sync CMake after updating the expected image in the source "
"directory?";
if (mBytes.empty()) {
return 0;
}
return utils::hash::murmur3((uint32_t*)mBytes.data(), mBytes.size() / 4, 0);
}
const std::vector<unsigned char>& LoadedPng::bytes() const {
return mBytes;
}
} // namespace test

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2025 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.
*/
#include <gtest/gtest.h>
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2025 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.
*/
#include <gtest/gtest.h>
#include <filamentapp/Config.h>
#include <filamentapp/FilamentApp.h>
#include <filamentapp/IBL.h>
#include <filament/Camera.h>
#include <filament/Engine.h>
#include <filament/IndexBuffer.h>
#include <filament/Material.h>
#include <filament/MaterialInstance.h>
#include <filament/RenderableManager.h>
#include <filament/Scene.h>
#include <filament/Skybox.h>
#include <filament/TransformManager.h>
#include <filament/VertexBuffer.h>
#include <filament/View.h>
#include <utils/EntityManager.h>
#include "ImageExpectations.h"
using namespace filament;
using utils::Entity;
using utils::EntityManager;
struct App {
Config config;
VertexBuffer* vb;
IndexBuffer* ib;
Material* mat;
Camera* cam;
Entity camera;
Skybox* skybox;
Entity renderable;
};
struct Vertex {
filament::math::float2 position;
uint32_t color;
};
static const Vertex TRIANGLE_VERTICES[3] = {
{{1, 0}, 0xffff0000u},
{{cos(M_PI * 2 / 3), sin(M_PI * 2 / 3)}, 0xff00ff00u},
{{cos(M_PI * 4 / 3), sin(M_PI * 4 / 3)}, 0xff0000ffu},
};
static constexpr uint16_t TRIANGLE_INDICES[3] = { 0, 1, 2 };
const std::string material = R"(material {
name : BakedColor,
requires : [
color
],
shadingModel : unlit,
culling : none,
featureLevel : 0
}
fragment {
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.baseColor = getColor();
}
}
)";
class CompareGLTFTest : public testing::Test {
public:
test::ImageExpectations mExpectations;
void postRender(Engine*, View* view, Scene*, Renderer* renderer);
};
void CompareGLTFTest::postRender(Engine*, View* view, Scene*, Renderer* renderer) {
EXPECT_IMAGE(renderer, mExpectations, test::ScreenshotParams(512, 512, "GLTF"));
FilamentApp::get().close();
}
TEST_F(CompareGLTFTest, Compare) {
App app;
auto setup = [&app](Engine* engine, View* view, Scene* scene) {
app.skybox = Skybox::Builder().color({0.1, 0.125, 0.25, 1.0}).build(*engine);
scene->setSkybox(app.skybox);
view->setPostProcessingEnabled(false);
static_assert(sizeof(Vertex) == 12, "Strange vertex size.");
/*app.vb = VertexBuffer::Builder()
.vertexCount(3)
.bufferCount(1)
.attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 12)
.attribute(VertexAttribute::COLOR, 0, VertexBuffer::AttributeType::UBYTE4, 8, 12)
.normalized(VertexAttribute::COLOR)
.build(*engine);
app.vb->setBufferAt(*engine, 0,
VertexBuffer::BufferDescriptor(TRIANGLE_VERTICES, 36, nullptr));
app.ib = IndexBuffer::Builder()
.indexCount(3)
.bufferType(IndexBuffer::IndexType::USHORT)
.build(*engine);
app.ib->setBuffer(*engine,
IndexBuffer::BufferDescriptor(TRIANGLE_INDICES, 6, nullptr));
app.mat = Material::Builder()
.package(material.c_str(), material.size())
.build(*engine);
app.renderable = EntityManager::get().create();
RenderableManager::Builder(1)
.boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }})
.material(0, app.mat->getDefaultInstance())
.geometry(0, RenderableManager::PrimitiveType::TRIANGLES, app.vb, app.ib, 0, 3)
.culling(false)
.receiveShadows(false)
.castShadows(false)
.build(*engine, app.renderable);
scene->addEntity(app.renderable);*/
app.camera = utils::EntityManager::get().create();
app.cam = engine->createCamera(app.camera);
view->setCamera(app.cam);
};
auto cleanup = [&app](Engine* engine, View*, Scene*) {
engine->destroy(app.skybox);
//engine->destroy(app.renderable);
//engine->destroy(app.mat);
//engine->destroy(app.vb);
//engine->destroy(app.ib);
engine->destroyCameraComponent(app.camera);
utils::EntityManager::get().destroy(app.camera);
};
FilamentApp::get().animate([&app](Engine* engine, View* view, double now) {
constexpr float ZOOM = 1.5f;
const uint32_t w = view->getViewport().width;
const uint32_t h = view->getViewport().height;
const float aspect = (float) w / h;
app.cam->setProjection(Camera::Projection::ORTHO,
-aspect * ZOOM, aspect * ZOOM,
-ZOOM, ZOOM, 0, 1);
auto& tcm = engine->getTransformManager();
tcm.setTransform(tcm.getInstance(app.renderable),
filament::math::mat4f::rotation(now, filament::math::float3{ 0, 0, 1 }));
});
app.config.backend = filament::Engine::Backend::METAL;
FilamentApp::get().run(app.config, setup, cleanup, FilamentApp::ImGuiCallback(),
FilamentApp::PreRenderCallback(),
[this](Engine* engine, View* view, Scene* scene, Renderer* renderer) {
postRender(engine, view, scene, renderer);
});
}