Files
filament/filament/test/filament_test.cpp
Mathias Agopian f7fd6a9eab Implement grid-based world origin snapping in View (#9917)
* Implement grid-based world origin snapping in View

Implement a grid-based world origin snapping system in View to avoid
per-frame transform updates in the future. This will allow to
improve CPU performance and to enable future caching of acceleration
structures like BVH.

The feature is protected by the 'view.enable_grid_based_world_origin'
feature flag. The grid size can be set
manually via View::setGridSize or calculated automatically as 10%
of the camera's far plane distance.

A 50% hysteresis ratio is applied to prevent rapid origin flipping
near grid edges.

Exposed the new API to Java and JavaScript bindings and added
unit tests in filament_test.cpp.

BUGS=[504726278]

* Refine grid-based world origin snapping implementation

Refine the grid-based world origin snapping in View with several
improvements:

1. Support Orthographic Projections:
   Calculate automatic grid size using projection matrix elements,
   working for both perspective and ortho without assuming positive
   near plane.

2. Stable Automatic Grid Size:
   Only update effective grid size when a position snap occurs.
   This prevents instability when frustum scale changes smoothly.

3. Immediate Manual Override:
   Force an immediate snap when user manually changes grid size.

Added test cases for ortho and auto-grid size in filament_test.cpp.
2026-04-23 14:19:44 -07:00

888 lines
33 KiB
C++

/*
* Copyright (C) 2015 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 <iostream>
#include <random>
#include <gtest/gtest.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <math/mat3.h>
#include <math/mat4.h>
#include <math/scalar.h>
#include <filament/Box.h>
#include <filament/Camera.h>
#include <filament/Color.h>
#include <filament/Frustum.h>
#include <filament/Material.h>
#include <filament/Engine.h>
#include <private/filament/BufferInterfaceBlock.h>
#include <private/filament/UibStructs.h>
#include <private/backend/BackendUtils.h>
#include "Allocators.h"
#include "details/Material.h"
#include "details/Camera.h"
#include "Froxelizer.h"
#include "details/Engine.h"
#include "details/View.h"
#include "components/RenderableManager.h"
#include "components/TransformManager.h"
#include "UniformBuffer.h"
using namespace filament;
using namespace filament::math;
using namespace utils;
static bool isGray(float3 v) {
return v.r == v.g && v.g == v.b;
}
static bool almostEqualUlps(float a, float b, int maxUlps) {
if (a == b) return true;
int intDiff = abs(*reinterpret_cast<int32_t*>(&a) - *reinterpret_cast<int32_t*>(&b));
return intDiff <= maxUlps;
}
static bool vec3eq(float3 a, float3 b) {
return almostEqualUlps(a.x, b.x, 1) &&
almostEqualUlps(a.y, b.y, 1) &&
almostEqualUlps(a.z, b.z, 1);
}
TEST(FilamentTest, AabbMath) {
constexpr Aabb aabb = {{4, 5, 6}, {12, 14, 11}};
const mat4f m(mat3f::rotation(F_PI_2, float3 {0, 0, 1}), float3 {-4, -5, -6});
const Aabb result = aabb.transform(m);
// Compare Arvo's method (above) with the naive method (below).
const float3 a = (m * float4(aabb.min.x, aabb.min.y, aabb.min.z, 1.0)).xyz;
const float3 b = (m * float4(aabb.min.x, aabb.min.y, aabb.max.z, 1.0)).xyz;
const float3 c = (m * float4(aabb.min.x, aabb.max.y, aabb.min.z, 1.0)).xyz;
const float3 d = (m * float4(aabb.min.x, aabb.max.y, aabb.max.z, 1.0)).xyz;
const float3 e = (m * float4(aabb.max.x, aabb.min.y, aabb.min.z, 1.0)).xyz;
const float3 f = (m * float4(aabb.max.x, aabb.min.y, aabb.max.z, 1.0)).xyz;
const float3 g = (m * float4(aabb.max.x, aabb.max.y, aabb.min.z, 1.0)).xyz;
const float3 h = (m * float4(aabb.max.x, aabb.max.y, aabb.max.z, 1.0)).xyz;
const Aabb expected {
.min = min(min(min(min(min(min(min(a, b), c), d), e), f), g), h),
.max = max(max(max(max(max(max(max(a, b), c), d), e), f), g), h),
};
EXPECT_PRED2(vec3eq, result.min, expected.min);
EXPECT_PRED2(vec3eq, result.max, expected.max);
}
TEST(FilamentTest, SkinningMath) {
struct Bone {
quatf q;
float4 t;
float4 s;
};
auto makeBone = [&](mat4f m) -> Bone {
// figure out the scales
float4 s = { length(m[0]), length(m[1]), length(m[2]), 0.0f };
if (dot(cross(m[0].xyz, m[1].xyz), m[2].xyz) < 0) {
s[2] = -s[2];
}
// compute the inverse scales
float4 is = { 1.0f/s.x, 1.0f/s.y, 1.0f/s.z, 0.0f };
// normalize the matrix
m[0] *= is[0];
m[1] *= is[1];
m[2] *= is[2];
Bone bone;
bone.s = s;
bone.q = m.toQuaternion();
bone.t = m[3];
return bone;
};
auto applyBone = [](Bone const& bone, float3 v) -> float3 {
float4 q = bone.q.xyzw;
float3 t = bone.t.xyz;
float3 s = bone.s.xyz;
// apply the non-uniform scales
v *= s;
// apply the rigid transform (valid only for unit quaternions)
v += 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
// apply the translation
v += t;
return v;
};
auto check = [&](mat4f m, float3 v) {
float3 expect = (m * v).xyz;
float3 actual = applyBone(makeBone(m), v);
static constexpr float value_eps = 40 * std::numeric_limits<float>::epsilon();
EXPECT_NEAR(expect.x, actual.x, value_eps);
EXPECT_NEAR(expect.y, actual.y, value_eps);
EXPECT_NEAR(expect.z, actual.z, value_eps);
};
mat4f m;
float3 v = {1, 2, 3};
m = mat4f::translation(float3{1, 2, 3});
check(m, v);
m = mat4f::scaling(float3{1, 2, 3});
check(m, v);
m = mat4f::scaling(float3{1, 2, 3}) * mat4f::translation(float3{1, 2, 3});
check(m, v);
m = mat4f::translation(float3{1, 2, 3}) * mat4f::scaling(float3{1, 2, 3});
check(m, v);
m = mat4f::translation(float3{1, 2, 3}) * mat4f::scaling(float3{1, -4, 1});
check(m, v);
std::default_random_engine generator(82828); // NOLINT
std::uniform_real_distribution<float> distribution(-4, 4);
std::uniform_real_distribution<float> dangle(-f::TAU, f::TAU);
auto rand_gen = std::bind(distribution, generator);
for (size_t i = 0; i < 100; ++i) {
m =
mat4f::translation(float3{rand_gen(), rand_gen(), rand_gen()}) *
mat4f::rotation(dangle(generator), normalize(float3{rand_gen(), rand_gen(), rand_gen()})) *
mat4f::scaling(float3{rand_gen(), rand_gen(), rand_gen()});
check(m, v);
}
}
TEST(FilamentTest, TransformManagerSimple) {
filament::FTransformManager tcm;
EntityManager& em = EntityManager::get();
Entity root = em.create();
tcm.create(root);
auto ti = tcm.getInstance(root);
auto t = mat4f::translation(float3{ 1, 2, 3 });
auto prev = tcm.getWorldTransform(ti);
tcm.setTransform(ti, t);
auto updated = tcm.getWorldTransform(ti);
EXPECT_NE(prev, t);
EXPECT_EQ(updated, t);
}
TEST(FilamentTest, TransformManager) {
filament::FTransformManager tcm;
tcm.setAccurateTranslationsEnabled(true);
EntityManager& em = EntityManager::get();
std::array<Entity, 3> entities;
em.create(entities.size(), entities.data());
// test component creation
tcm.create(entities[0]);
EXPECT_TRUE(tcm.hasComponent(entities[0]));
TransformManager::Instance parent = tcm.getInstance(entities[0]);
EXPECT_TRUE(bool(parent));
// test component creation with parent
tcm.create(entities[1], parent, mat4f{});
EXPECT_TRUE(tcm.hasComponent(entities[1]));
TransformManager::Instance child = tcm.getInstance(entities[1]);
EXPECT_TRUE(bool(child));
// test default values
EXPECT_EQ(tcm.getTransform(parent), mat4f{ float4{ 1 }});
EXPECT_EQ(tcm.getWorldTransform(parent), mat4f{ float4{ 1 }});
EXPECT_EQ(tcm.getTransform(child), mat4f{ float4{ 1 }});
EXPECT_EQ(tcm.getWorldTransform(child), mat4f{ float4{ 1 }});
// test setting a transform
tcm.setTransform(parent, mat4f{ float4{ 2 }});
// test local and world transform propagation
EXPECT_EQ(tcm.getTransform(parent), mat4f{ float4{ 2 }});
EXPECT_EQ(tcm.getWorldTransform(parent), mat4f{ float4{ 2 }});
EXPECT_EQ(tcm.getTransform(child), mat4f{ float4{ 1 }});
EXPECT_EQ(tcm.getWorldTransform(child), mat4f{ float4{ 2 }});
// test local transaction
tcm.openLocalTransformTransaction();
tcm.setTransform(parent, mat4f{ float4{ 4 }});
// check the transforms ARE NOT propagated
EXPECT_EQ(tcm.getTransform(parent), mat4f{ float4{ 4 }});
EXPECT_EQ(tcm.getWorldTransform(parent), mat4f{ float4{ 2 }});
EXPECT_EQ(tcm.getTransform(child), mat4f{ float4{ 1 }});
EXPECT_EQ(tcm.getWorldTransform(child), mat4f{ float4{ 2 }});
tcm.commitLocalTransformTransaction();
// test propagation after closing the transaction
EXPECT_EQ(tcm.getTransform(parent), mat4f{ float4{ 4 }});
EXPECT_EQ(tcm.getWorldTransform(parent), mat4f{ float4{ 4 }});
EXPECT_EQ(tcm.getTransform(child), mat4f{ float4{ 1 }});
EXPECT_EQ(tcm.getWorldTransform(child), mat4f{ float4{ 4 }});
//
// test out-of-order parent/child
//
tcm.create(entities[2]);
EXPECT_TRUE(tcm.hasComponent(entities[2]));
TransformManager::Instance newParent = tcm.getInstance(entities[2]);
ASSERT_LT(child, newParent);
// test reparenting
tcm.setParent(child, newParent);
// make sure child/parent are out of order (i.e.: setParent() doesn't invalidate instances)
EXPECT_LT(tcm.getInstance(entities[1]), tcm.getInstance(entities[2]));
// local transaction reorders parent/child
auto const t = mat4::translation(double3(1.0 / 3.0));
tcm.openLocalTransformTransaction();
tcm.setTransform(newParent, t);
tcm.commitLocalTransformTransaction();
// local transaction invalidates Instances
parent = tcm.getInstance(entities[0]);
child = tcm.getInstance(entities[1]);
newParent = tcm.getInstance(entities[2]);
// check parent / child order is correct
EXPECT_GT(child, newParent);
// check transform propagation
// our "high precision" mode only preserves 48 bits of the double mantissa (out of 53).
// This constant loses 5 bits out of 1/3
const mat4 PRECISION_KILLER_5BITS = mat4::translation(double3(16.0));
EXPECT_EQ(tcm.getTransformAccurate(newParent) + PRECISION_KILLER_5BITS, t + PRECISION_KILLER_5BITS);
EXPECT_EQ(tcm.getWorldTransformAccurate(newParent) + PRECISION_KILLER_5BITS, t + PRECISION_KILLER_5BITS);
EXPECT_EQ(tcm.getWorldTransformAccurate(child) + PRECISION_KILLER_5BITS, t + PRECISION_KILLER_5BITS);
EXPECT_EQ(tcm.getTransformAccurate(child), mat4{ 1.0 });
// check children iterators
size_t c = 0;
auto first = tcm.getChildrenBegin(newParent);
auto last = tcm.getChildrenEnd(newParent);
while (first != last) {
++first;
c++;
}
EXPECT_EQ(tcm.getChildrenEnd(parent)++, tcm.getChildrenEnd(parent));
EXPECT_EQ(tcm.getChildrenBegin(parent), tcm.getChildrenEnd(parent));
EXPECT_EQ(c, tcm.getChildCount(newParent));
}
TEST(FilamentTest, UniformInterfaceBlock) {
BufferInterfaceBlock::Builder b;
b.name("TestUniformInterfaceBlock");
b.add({
{ "a_float_0", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_1", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_2", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_3", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_vec4_0", 0, BufferInterfaceBlock::Type::FLOAT4 },
{ "a_float_4", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_5", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_6", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_vec3_0", 0, BufferInterfaceBlock::Type::FLOAT3 },
{ "a_float_7", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float[3]", 3, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_8", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_mat3_0", 0, BufferInterfaceBlock::Type::MAT3 },
{ "a_mat4_0", 0, BufferInterfaceBlock::Type::MAT4 },
{ "a_mat3[3]", 3, BufferInterfaceBlock::Type::MAT3 }
});
BufferInterfaceBlock ib(b.build());
auto const& info = ib.getFieldInfoList();
// test that 4 floats are packed together
EXPECT_EQ(0, info[0].offset);
EXPECT_EQ(1, info[1].offset);
EXPECT_EQ(2, info[2].offset);
EXPECT_EQ(3, info[3].offset);
// test the double4 is where it should be
EXPECT_EQ(4, info[4].offset);
// check 3 following floats are packed right after the double4
EXPECT_EQ(8, info[5].offset);
EXPECT_EQ(9, info[6].offset);
EXPECT_EQ(10, info[7].offset);
// check that the following double3 is aligned to the next double4 boundary
EXPECT_EQ(12, info[8].offset);
// check that the following float is just behind the double3
EXPECT_EQ(15, info[9].offset);
// check that arrays are aligned on double4 and have a stride of double4
EXPECT_EQ(16, info[10].offset);
EXPECT_EQ(4, info[10].stride);
EXPECT_EQ(3, info[10].size);
// check the base offset of the member following the array is rounded up to the next multiple of the base alignment.
EXPECT_EQ(28, info[11].offset);
// check mat3 alignment is double4
EXPECT_EQ(32, info[12].offset);
EXPECT_EQ(12, info[12].stride);
// check following mat4 is 3*double4 away
EXPECT_EQ(44, info[13].offset);
EXPECT_EQ(16, info[13].stride);
// arrays of matrices
EXPECT_EQ(60, info[14].offset);
EXPECT_EQ(12, info[14].stride);
EXPECT_EQ(3, info[14].size);
}
TEST(FilamentTest, UniformBuffer) {
struct ubo {
float f0;
float f1;
float f2;
float f3;
alignas(16) float4 v0;
float f4;
float f5;
float f6;
alignas(16) float3 v1; // double3 are aligned to 4 floats
float f7;
struct {
alignas(16) float v; // arrays entries are always aligned to 4 floats
} u[3];
float f8;
alignas(16) float4 m0[3]; // mat3 are like vec4f[3]
alignas(16) mat4f m1;
};
auto CHECK = [](ubo const* data) {
EXPECT_EQ(1.0f, data->f0);
EXPECT_EQ(3.0f, data->f1);
EXPECT_EQ(5.0f, data->f2);
EXPECT_EQ(7.0f, data->f3);
EXPECT_EQ((float4{ -1.1f, -1.2f, 3.14f, sqrtf(2)}), data->v0);
EXPECT_EQ(11.0f, data->f4);
EXPECT_EQ(13.0f, data->f5);
EXPECT_EQ(17.0f, data->f6);
EXPECT_EQ((float3{ 1, 2, 3}), data->v1);
EXPECT_EQ(19.0f, data->f7);
EXPECT_EQ(-3.0f, data->u[0].v);
EXPECT_EQ(-5.0f, data->u[1].v);
EXPECT_EQ(-7.0f, data->u[2].v);
EXPECT_EQ(23.0f, data->f8);
EXPECT_EQ((mat4f{100, 200, 300, 0, 400, 500, 600, 0, 700, 800, 900, 0, 0, 0, 0, 1}), data->m1);
};
auto CHECK2 = [](auto const& info) {
EXPECT_EQ(offsetof(ubo, f0)/4, info[0].offset);
EXPECT_EQ(offsetof(ubo, f1)/4, info[1].offset);
EXPECT_EQ(offsetof(ubo, f2)/4, info[2].offset);
EXPECT_EQ(offsetof(ubo, f3)/4, info[3].offset);
EXPECT_EQ(offsetof(ubo, v0)/4, info[4].offset);
EXPECT_EQ(offsetof(ubo, f4)/4, info[5].offset);
EXPECT_EQ(offsetof(ubo, f5)/4, info[6].offset);
EXPECT_EQ(offsetof(ubo, f6)/4, info[7].offset);
EXPECT_EQ(offsetof(ubo, v1)/4, info[8].offset);
EXPECT_EQ(offsetof(ubo, f7)/4, info[9].offset);
EXPECT_EQ(offsetof(ubo, u)/4, info[10].offset);
EXPECT_EQ(offsetof(ubo, f8)/4, info[11].offset);
EXPECT_EQ(offsetof(ubo, m0)/4, info[12].offset);
EXPECT_EQ(offsetof(ubo, m1)/4, info[13].offset);
};
BufferInterfaceBlock::Builder b;
b.name("TestUniformBuffer");
b.add({
{ "a_float_0", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_1", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_2", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_3", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_vec4_0", 0, BufferInterfaceBlock::Type::FLOAT4 },
{ "a_float_4", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_5", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_6", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_vec3_0", 0, BufferInterfaceBlock::Type::FLOAT3 },
{ "a_float_7", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_float[3]",3, BufferInterfaceBlock::Type::FLOAT },
{ "a_float_8", 0, BufferInterfaceBlock::Type::FLOAT },
{ "a_mat3_0", 0, BufferInterfaceBlock::Type::MAT3 },
{ "a_mat4_0", 0, BufferInterfaceBlock::Type::MAT4 },
});
BufferInterfaceBlock ib(b.build());
CHECK2(ib.getFieldInfoList());
EXPECT_EQ(sizeof(ubo), ib.getSize());
UniformBuffer buffer(sizeof(ubo));
ubo const* data = static_cast<ubo const*>(buffer.getBuffer());
buffer.setUniform(offsetof(ubo, f0), 1.0f);
buffer.setUniform(offsetof(ubo, f1), 3.0f);
buffer.setUniform(offsetof(ubo, f2), 5.0f);
buffer.setUniform(offsetof(ubo, f3), 7.0f);
buffer.setUniform(offsetof(ubo, v0), float4{ -1.1f, -1.2f, 3.14f, sqrtf(2) });
buffer.setUniform(offsetof(ubo, f4), 11.0f);
buffer.setUniform(offsetof(ubo, f5), 13.0f);
buffer.setUniform(offsetof(ubo, f6), 17.0f);
buffer.setUniform(offsetof(ubo, v1), float3{ 1, 2, 3 });
buffer.setUniform(offsetof(ubo, f7), 19.0f);
buffer.setUniform(offsetof(ubo, u[0].v), -3.0f);
buffer.setUniform(offsetof(ubo, u[1].v), -5.0f);
buffer.setUniform(offsetof(ubo, u[2].v), -7.0f);
buffer.setUniform(offsetof(ubo, f8), 23.0f);
buffer.setUniform(offsetof(ubo, m0), mat3f{10,20,30, 40,50,60, 70,80,90 });
buffer.setUniform(offsetof(ubo, m1), mat4f{100,200,300,0, 400,500,600,0, 700,800,900,0, 0,0,0,1 });
CHECK(data);
ubo copy(*data);
CHECK(data);
CHECK(&copy);
ubo move(copy);
CHECK(&move);
copy = *data;
CHECK(data);
CHECK(&copy);
move = copy;
CHECK(&move);
}
TEST(FilamentTest, UniformBufferSize1) {
BufferInterfaceBlock::Builder b;
b.name("UniformBufferSize1");
b.add({
{ "f4a", 0, BufferInterfaceBlock::Type::FLOAT4 }, // offset = 0
{ "f4b", 0, BufferInterfaceBlock::Type::FLOAT4 }, // offset = 16
{ "f1a", 0, BufferInterfaceBlock::Type::FLOAT }, // offset = 32
{ "f1b", 0, BufferInterfaceBlock::Type::FLOAT }, // offset = 36
});
BufferInterfaceBlock uib(b.build());
UniformBuffer buffer(uib.getSize());
float4 f4(1.0f);
ssize_t f4_offset = uib.getFieldOffset("f4a", 0);
buffer.setUniformArray(f4_offset, &f4, 1);
float f1(1.0f);
ssize_t f1_offset = uib.getFieldOffset("f1b", 0);
buffer.setUniformArray(f1_offset, &f1, 1);
buffer.invalidate();
}
TEST(FilamentTest, UniformBufferSize2) {
BufferInterfaceBlock::Builder b;
b.name("UniformBufferSize2");
b.add({
{ "f4a", 0, BufferInterfaceBlock::Type::FLOAT4 }, // offset = 0
{ "f4b", 0, BufferInterfaceBlock::Type::FLOAT4 }, // offset = 16
{ "f1a", 0, BufferInterfaceBlock::Type::FLOAT }, // offset = 32
{ "f2a", 0, BufferInterfaceBlock::Type::FLOAT2 }, // offset = 36
});
BufferInterfaceBlock uib(b.build());
UniformBuffer buffer(uib.getSize());
float4 f4(1.0f);
ssize_t f4_offset = uib.getFieldOffset("f4a", 0);
buffer.setUniformArray(f4_offset, &f4, 1);
float2 f2(1.0f);
ssize_t f2_offset = uib.getFieldOffset("f2a", 0);
buffer.setUniformArray(f2_offset, &f2, 1);
buffer.invalidate();
}
TEST(FilamentTest, BoxCulling) {
Frustum frustum(mat4f::frustum(-1, 1, -1, 1, 1, 100));
// a cube centered in 0 of size 1
Box box = { 0, 0.5f };
// box fully inside
EXPECT_TRUE( frustum.intersects(box.translateTo({ 0, 0, -10})) );
// box clipped by the near or far plane
EXPECT_TRUE( frustum.intersects(box.translateTo({ 0, 0, -1})) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 0, 0, -100})) );
// box clipped by one or several planes of the frustum for any z, but still visible
EXPECT_TRUE( frustum.intersects(box.translateTo({ -10, 0, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 10, 0, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 0, -10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 0, 10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ -10, -10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 10, 10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 10, -10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ -10, 10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ -10, 10, -10 })) );
EXPECT_TRUE( frustum.intersects(box.translateTo({ 10, -10, -10 })) );
// box outside frustum planes
EXPECT_FALSE( frustum.intersects(box.translateTo({ 0, 0, 0})) );
EXPECT_FALSE( frustum.intersects(box.translateTo({ 0, 0, -101})) );
EXPECT_FALSE( frustum.intersects(box.translateTo({-1.51, 0, -0.5})) );
// slightly inside the frustum
EXPECT_TRUE( frustum.intersects(box.translateTo({-1.49, 0, -0.5})) );
EXPECT_TRUE( frustum.intersects(box.translateTo({-100, 0, -100})) );
// expected false classification (the box is not visible, but its classified as visible)
EXPECT_TRUE( frustum.intersects(box.translateTo({-100.51, 0, -100})) );
EXPECT_TRUE( frustum.intersects(box.translateTo({-100.99, 0, -100})) );
EXPECT_FALSE(frustum.intersects(box.translateTo({-101.01, 0, -100})) ); // good again
// A box that entirely contain the frustum
EXPECT_TRUE( frustum.intersects( { 0, 200 }) );
}
TEST(FilamentTest, SphereCulling) {
Frustum frustum(mat4f::frustum(-1, 1, -1, 1, 1, 100));
// a sphere centered in 0 of size 1
float4 sphere = { 0, 0, 0, 0.5f };
// sphere fully inside
EXPECT_TRUE( frustum.intersects(sphere + float4{ 0, 0, -10, 0}) );
// sphere clipped by the near or far plane
EXPECT_TRUE( frustum.intersects(sphere + float4{ 0, 0, -1, 0}) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 0, 0, -100, 0}) );
// sphere clipped by one or several planes of the frustum for any z, but still visible
EXPECT_TRUE( frustum.intersects(sphere + float4{ -10, 0, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 10, 0, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 0, -10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 0, 10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ -10, -10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 10, 10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 10, -10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ -10, 10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ -10, 10, -10, 0 }) );
EXPECT_TRUE( frustum.intersects(sphere + float4{ 10, -10, -10, 0 }) );
// sphere outside frustum planes
EXPECT_FALSE( frustum.intersects(sphere + float4{ 0, 0, 0, 0}) );
EXPECT_FALSE( frustum.intersects(sphere + float4{ 0, 0, -101, 0}) );
EXPECT_FALSE( frustum.intersects(sphere + float4{ -1.51, 0, -0.5, 0}) );
// slightly inside the frustum
EXPECT_TRUE( frustum.intersects(sphere + float4{ -100, 0, -100, 0}) );
// A sphere that entirely contain the frustum
EXPECT_TRUE(frustum.intersects({ 0, 200 }));
}
TEST(FilamentTest, ColorConversion) {
// Linear to Gamma
// 0.0 stays 0.0
EXPECT_PRED2(vec3eq, (sRGBColor{0.0f, 0.0f, 0.0f}), Color::toSRGB<FAST>(LinearColor{0.0f}));
// 1.0 stays 1.0
EXPECT_PRED2(vec3eq, (sRGBColor{1.0f, 0.0f, 0.0f}), Color::toSRGB<FAST>({1.0f, 0.0f, 0.0f}));
// 0.0 stays 0.0
EXPECT_PRED2(vec3eq, (sRGBColor{0.0f, 0.0f, 0.0f}),
Color::toSRGB<ACCURATE>(LinearColor{0.0f}));
// 1.0 stays 1.0
EXPECT_PRED2(vec3eq, (sRGBColor{1.0f, 0.0f, 0.0f}),
Color::toSRGB<ACCURATE>({1.0f, 0.0f, 0.0f}));
EXPECT_LT((sRGBColor{0.5f, 0.0f, 0.0f}.x), Color::toSRGB<FAST>({0.5f, 0.0f, 0.0f}).x);
EXPECT_LT((sRGBColor{0.5f, 0.0f, 0.0f}.x), Color::toSRGB<ACCURATE>({0.5f, 0.0f, 0.0f}).x);
EXPECT_PRED1(isGray, Color::toSRGB<FAST>(LinearColor{0.5f}));
EXPECT_PRED1(isGray, Color::toSRGB<ACCURATE>(LinearColor{0.5f}));
// Gamma to Linear
// 0.0 stays 0.0
EXPECT_PRED2(vec3eq, (LinearColor{0.0f, 0.0f, 0.0f}), Color::toLinear<FAST>(sRGBColor{0.0f}));
// 1.0 stays 1.0
EXPECT_PRED2(vec3eq, (LinearColor{1.0f, 0.0f, 0.0f}), Color::toLinear<FAST>({1.0f, 0.0f, 0.0f}));
// 0.0 stays 0.0
EXPECT_PRED2(vec3eq, (LinearColor{0.0f, 0.0f, 0.0f}), Color::toLinear<ACCURATE>(sRGBColor{0.0f}));
// 1.0 stays 1.0
EXPECT_PRED2(vec3eq, (LinearColor{1.0f, 0.0f, 0.0f}), Color::toLinear<ACCURATE>({1.0f, 0.0f, 0.0f}));
EXPECT_GT((LinearColor{0.5f, 0.0f, 0.0f}.x), Color::toLinear<FAST>({0.5f, 0.0f, 0.0f}).x);
EXPECT_GT((LinearColor{0.5f, 0.0f, 0.0f}.x), Color::toLinear<ACCURATE>({0.5f, 0.0f, 0.0f}).x);
EXPECT_PRED1(isGray, Color::toLinear<FAST>(sRGBColor{0.5f}));
EXPECT_PRED1(isGray, Color::toLinear<ACCURATE>(sRGBColor{0.5f}));
}
TEST(FilamentTest, FroxelData) {
using namespace filament;
FEngine* engine = downcast(Engine::create());
LinearAllocatorArena arena("FRenderer: per-frame allocator", 3 * 1024 * 1024);
utils::ArenaScope<LinearAllocatorArena> scope(arena);
// view-port size is chosen so that we fit exactly a integer # of froxels horizontally
// (unfortunately there is no way to guarantee it as it depends on the max # of froxel
// used by the engine). We do this to infer the value of the left and right most planes
// to check if they're computed correctly.
Viewport vp(0, 0, 1280, 640);
mat4f p = mat4f::perspective(90, 1.0f, 0.1, 100, mat4f::Fov::HORIZONTAL);
Froxelizer froxelData(*engine);
froxelData.setOptions(5, 100);
froxelData.prepare(engine->getDriverApi(), scope, vp, p, 0.1, 100, {1,1,0,0});
Froxel f = froxelData.getFroxelAt(0,0,0);
// 45-deg plane, with normal pointing outward to the left
EXPECT_FLOAT_EQ(-F_SQRT2/2, f.planes[Froxel::LEFT].x);
EXPECT_FLOAT_EQ( 0, f.planes[Froxel::LEFT].y);
EXPECT_FLOAT_EQ( F_SQRT2/2, f.planes[Froxel::LEFT].z);
// the right side of froxel 1 is near 45-deg plane pointing outward to the right
EXPECT_TRUE(f.planes[Froxel::RIGHT].x > 0);
EXPECT_FLOAT_EQ(0, f.planes[Froxel::RIGHT].y);
EXPECT_TRUE(f.planes[Froxel::RIGHT].z < 0);
// right side of last horizontal froxel is 45-deg plane pointing outward to the right
Froxel g = froxelData.getFroxelAt(froxelData.getFroxelCountX()-1,0,0);
EXPECT_FLOAT_EQ(F_SQRT2/2, g.planes[Froxel::RIGHT].x);
EXPECT_FLOAT_EQ( 0, g.planes[Froxel::RIGHT].y);
EXPECT_FLOAT_EQ(F_SQRT2/2, g.planes[Froxel::RIGHT].z);
// first froxel near plane facing us
EXPECT_FLOAT_EQ( 0, f.planes[Froxel::NEAR].x);
EXPECT_FLOAT_EQ( 0, f.planes[Froxel::NEAR].y);
EXPECT_FLOAT_EQ( 1, f.planes[Froxel::NEAR].z);
// first froxel far plane away from us
EXPECT_FLOAT_EQ( 0, f.planes[Froxel::FAR].x);
EXPECT_FLOAT_EQ( 0, f.planes[Froxel::FAR].y);
EXPECT_FLOAT_EQ( -1, f.planes[Froxel::FAR].z);
// first froxel near plane distance always 0
EXPECT_FLOAT_EQ( 0, f.planes[Froxel::NEAR].w);
// first froxel far plane distance always zLightNear
EXPECT_FLOAT_EQ( 5,-f.planes[Froxel::FAR].w);
Froxel l = froxelData.getFroxelAt(0, 0, froxelData.getFroxelCountZ()-1);
// farthest froxel far plane distance always zLightFar
EXPECT_FLOAT_EQ( 100,-l.planes[Froxel::FAR].w);
// create a dummy point light that can be referenced in LightSoa
Entity e = engine->getEntityManager().create();
LightManager::Builder(LightManager::Type::POINT).build(*engine, e);
LightManager::Instance instance = engine->getLightManager().getInstance(e);
FScene::LightSoa lights;
lights.push_back({}, {}, {}, {}, {}, {}, {}, {}); // first one is always skipped
lights.push_back(float4{ 0, 0, -5, 1 }, {}, {}, {}, instance, 1, {}, {});
{
froxelData.froxelizeLights(*engine, {}, lights);
auto const& froxelBuffer = froxelData.getFroxelBufferUser();
auto const& recordBuffer = froxelData.getRecordBufferUser();
// light straddles the "light near" plane
size_t pointCount = 0;
for (const auto& entry : froxelBuffer) {
EXPECT_LE(entry.count(), 1);
pointCount += entry.count();
}
EXPECT_GT(pointCount, 0);
}
{
// light doesn't cross any froxel near or far plane
lights.elementAt<FScene::POSITION_RADIUS>(1) = float4{ 0, 0, -3, 1 };
auto pos = lights.elementAt<FScene::POSITION_RADIUS>(1);
EXPECT_TRUE(pos == float4( 0, 0, -3, 1 ));
froxelData.froxelizeLights(*engine, {}, lights);
auto const& froxelBuffer = froxelData.getFroxelBufferUser();
auto const& recordBuffer = froxelData.getRecordBufferUser();
size_t pointCount = 0;
for (const auto& entry : froxelBuffer) {
EXPECT_LE(entry.count(), 1);
pointCount += entry.count();
}
EXPECT_GT(pointCount, 0);
}
froxelData.terminate(engine->getDriverApi());
Engine::destroy((Engine **)&engine);
}
TEST(FilamentTest, GoogleLineDirective) {
{
char s[512] = "#line 10 \"foobar\"";
EXPECT_FALSE(filament::backend::requestsGoogleLineDirectivesExtension({ &s[0], strlen(s) }));
}
{
char s[512] =
"#extension GL_GOOGLE_cpp_style_line_directive : enable\n"
"#line 10 \"foobar\"";
EXPECT_TRUE(filament::backend::requestsGoogleLineDirectivesExtension({ &s[0], strlen(s) }));
}
{
char s[512] =
"#extension GL_GOOGLE_cpp_style_line_directive : enable\n"
"#line 10 \"foobar\"";
filament::backend::removeGoogleLineDirectives(&s[0], strlen(s));
EXPECT_STREQ(s,
"#extension GL_GOOGLE_cpp_style_line_directive : enable\n"
"#line 10 ");
}
{
char s[512] =
"#extension GL_GOOGLE_cpp_style_line_directive : enable\n"
"#line 10 \"foobar\"\n"
"#line 100 \"foobar\" abc\n"
"//\n"
"#line 20\n"
"#line 20 \"foo\n"
"// valid quote: \"\n"
"#line 100 \"baz\"\n"
"#line";
filament::backend::removeGoogleLineDirectives(&s[0], strlen(s));
EXPECT_STREQ(s,
"#extension GL_GOOGLE_cpp_style_line_directive : enable\n"
"#line 10 \n"
"#line 100 \n"
"//\n"
"#line 20\n"
"#line 20 \n"
"// valid quote: \"\n"
"#line 100 \n"
"#line");
}
}
TEST(FilamentTest, GridSnapping) {
FEngine* engine = downcast(Engine::create(backend::Backend::NOOP));
FView* view = downcast(engine->createView());
FScene* scene = downcast(engine->createScene());
Entity cameraEntity = engine->getEntityManager().create();
FCamera* camera = downcast(engine->createCamera(cameraEntity));
view->setScene(scene);
view->setCamera(camera);
engine->features.view.enable_grid_based_world_origin = true;
engine->debug.view.camera_at_origin = true;
view->setGridSize(10.0);
// Test case 1: Camera at (0,0,0)
camera->setModelMatrix(mat4::translation(double3{0.0, 0.0, 0.0}));
CameraInfo ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, 0.0f, 1e-5f);
// Test case 2: Camera at (9.9,0,0) - should not snap yet if hysteresis is 50%
// Grid size is 10, threshold is 10.
camera->setModelMatrix(mat4::translation(double3{9.9, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, 0.0f, 1e-5f);
// Test case 3: Camera at (10.1,0,0) - should snap to 10
camera->setModelMatrix(mat4::translation(double3{10.1, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
// If it snapped to 10, the translation applied to world should be -10
EXPECT_NEAR(ci.worldTransform[3].x, -10.0f, 1e-5f);
// Test case 4: Move back to (1.0,0,0) - should stay at 10 due to hysteresis
// Diff to origin (10) is 9.0 < 10.
camera->setModelMatrix(mat4::translation(double3{1.0, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, -10.0f, 1e-5f);
// Test case 5: Move to (-0.1,0,0) - should snap back to 0
// Diff to origin (10) is 10.1 > 10.
camera->setModelMatrix(mat4::translation(double3{-0.1, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, 0.0f, 1e-5f);
// Test case 6: Automatic grid size (Perspective)
view->setGridSize(0.0); // Enable auto
static_cast<Camera*>(camera)->setProjection(90.0, 1.0, 0.1, 100.0, Camera::Fov::VERTICAL); // Set far plane to 100
// FOV 90, aspect 1.0 -> baseScale = 2.0. Width at far plane = 200.
// Auto grid size should be 2.0 * 100 * 0.1 = 20.
camera->setModelMatrix(mat4::translation(double3{0.0, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, 0.0f, 1e-5f);
camera->setModelMatrix(mat4::translation(double3{20.1, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, -20.0f, 1e-5f); // Should snap to 20
// Test case 7: Ortho automatic grid size
view->setGridSize(0.0); // Enable auto
static_cast<Camera*>(camera)->setProjection(Camera::Projection::ORTHO, -10.0, 10.0, -10.0, 10.0, -100.0, 100.0);
// Width is 20. Auto grid size should be 20 * 0.1 = 2.
// Move to -0.1 to trigger snap from previous origin (20) with threshold 20
camera->setModelMatrix(mat4::translation(double3{-0.1, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, 0.0f, 1e-5f); // Should snap to 0
camera->setModelMatrix(mat4::translation(double3{2.1, 0.0, 0.0}));
ci = view->computeCameraInfo(*engine);
EXPECT_NEAR(ci.worldTransform[3].x, -2.0f, 1e-5f); // Should snap to 2
engine->destroy(scene);
engine->destroy(view);
engine->destroyCameraComponent(cameraEntity);
engine->getEntityManager().destroy(cameraEntity);
Engine::destroy((Engine **)&engine);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}