Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
574e8ec
fix(build): build OpenUSD against vcpkg's oneTBB and allow one oneTBB…
drsnuggles8 Oct 12, 2026
cd6d138
fix(build): let OpenUSD's nested build take the build lock's job cap
drsnuggles8 Oct 12, 2026
cbb7b29
fix(build): build OpenUSD with MSVC when the engine tree is Ninja
drsnuggles8 Oct 12, 2026
7f195a6
fix(groom): grow a card's coverage only as far as it stands for a loc…
drsnuggles8 Oct 12, 2026
bfd7215
fix(assets): re-export the GroomAnimals horse coats with their card g…
drsnuggles8 Oct 12, 2026
50ffdf3
fix(tests): give the groom simulation fixture a runtime camera that s…
drsnuggles8 Oct 12, 2026
5e5cca3
fix(tests): wait for the asynchronous foliage re-bake in the impostor…
drsnuggles8 Oct 12, 2026
121fc27
docs(usd): record vcpkg's Debug oneTBB asserts and the by-name TBB di…
drsnuggles8 Oct 12, 2026
15fcc06
fix(build): answer the oneTBB guard's codemodel query in the configur…
drsnuggles8 Oct 13, 2026
756d742
fix(tests): let the memory-accounting groom case see its tuft
drsnuggles8 Oct 13, 2026
b52cede
docs(tests): a test's runtime camera must set its projection
drsnuggles8 Oct 13, 2026
5381878
test(evidence): the card tier's coverage cells after the growth reach…
drsnuggles8 Oct 13, 2026
78bbb1e
test(evidence): the groom simulation captures with the solver actuall…
drsnuggles8 Oct 13, 2026
a34fdbb
fix(build): the oneTBB guard recognises oneTBB's non-Windows library …
drsnuggles8 Oct 13, 2026
88f835d
fix(build): key OpenUSD on the full oneTBB version and never build it…
drsnuggles8 Oct 13, 2026
ad9d845
fix(tests): drain the frames after the groom and lawn first draw
drsnuggles8 Oct 13, 2026
e9a4c3b
docs: move #1575's USD material into its own guide and keep both guid…
drsnuggles8 Oct 13, 2026
73b859e
test(evidence): GroomLodVisualEvidenceTest's card-tier frames after t…
drsnuggles8 Oct 13, 2026
c6164c1
fix(build): cap OpenUSD's /MP compile at the lock's count and refuse …
drsnuggles8 Oct 13, 2026
318adb1
fix(tests): assert the first draw before draining, and drain as usePa…
drsnuggles8 Oct 13, 2026
addfaa2
docs: give OpenUSD's build and link their own guide
drsnuggles8 Oct 13, 2026
a3cbb52
test(groom): hold the short coat's card coverage on Deferred and Forw…
drsnuggles8 Oct 13, 2026
34b98d9
style: clang-format the impostor streaming test's eviction wait
drsnuggles8 Oct 13, 2026
86d3a48
fix(build): pin policies in the -P scripts so CMake 3.x runs the oneT…
drsnuggles8 Oct 13, 2026
e4f638e
build: require CMake 3.27, which the oneTBB guard's cmake_file_api() …
drsnuggles8 Oct 13, 2026
adcc799
fix(build): build-usd-config fails clearly without a configured USD tree
drsnuggles8 Oct 13, 2026
fa55bf8
build: declare the 3.27 floor in cmake_minimum_required, and pin the …
drsnuggles8 Oct 13, 2026
96aa8eb
docs(mcp): document olo_foliage_rebuild as a consented write
drsnuggles8 Oct 13, 2026
8313d48
build: pin the repo's other -P scripts and correct the 3.25 references
drsnuggles8 Oct 13, 2026
34c6985
docs(mcp): say when olo_foliage_rebuild's rebuild runs off the frame
drsnuggles8 Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
cmake_minimum_required(VERSION 3.25)
# 3.27 for cmake_file_api(), which cmake/OneTbbGuard.cmake runs in every configuration (#1575).
cmake_minimum_required(VERSION 3.27)
# The policies stay at 3.25's, as they were before the floor moved: switching CMP0143-CMP0152 to NEW
# is its own change, not a side effect of needing one 3.27 command.
cmake_policy(VERSION 3.25)

message(STATUS "CMake version: ${CMAKE_VERSION}")

Expand Down Expand Up @@ -75,7 +79,7 @@ endif()
# only thing available is whatever the user put in the cache / on the command
# line. Keep the two in sync — a mismatch means either an unused port build
# (harmless, slow) or a missing one (a find_package failure at configure).
foreach(_olo_feat alembic materialx openvdb vulkan)
foreach(_olo_feat alembic materialx openvdb usd vulkan)
string(TOUPPER "${_olo_feat}" _olo_feat_uc)
if(NOT DEFINED OLO_WITH_${_olo_feat_uc} OR OLO_WITH_${_olo_feat_uc})
list(APPEND VCPKG_MANIFEST_FEATURES "${_olo_feat}")
Expand Down Expand Up @@ -243,13 +247,13 @@ option(OLO_WARN_STATIC_INIT "Warn on global constructors / exit-time destructors
# Asset-import format options (#655). All default ON — importing USD / Alembic / MaterialX works
# out of the box, matching UE5's USD-by-default posture. Alembic + MaterialX are small static
# FetchContent builds. OpenUSD is large + install-centric: it is built from source once via
# ExternalProject (oneTBB + static-monolithic USD, ~10-15 min + ~2 GB on the FIRST build, then
# cached), OR reused from a prebuilt install via -DOLO_USD_INSTALL_DIR=<prefix> (CI cache / a
# shared build). CI that doesn't need USD should pass -DOLO_WITH_USD=OFF to skip the heavy build.
# ExternalProject (static-monolithic USD against vcpkg's oneTBB, ~30-45 min + ~10 GB on the FIRST
# build, then cached per machine). CI that doesn't need USD should pass -DOLO_WITH_USD=OFF to skip
# the heavy build.
# See docs/agent-rules/asset-import-usd-alembic.md.
option(OLO_WITH_ALEMBIC "Import Alembic (.abc) geometry caches (vendors Imath + Alembic)" ON)
option(OLO_WITH_MATERIALX "Import MaterialX (.mtlx) materials (vendors MaterialX)" ON)
option(OLO_WITH_USD "Import OpenUSD stages (.usd/.usda/.usdc/.usdz); builds USD from source unless OLO_USD_INSTALL_DIR is set" ON)
option(OLO_WITH_USD "Import OpenUSD stages (.usd/.usda/.usdc/.usdz); builds USD from source against vcpkg's oneTBB" ON)

# OpenVDB (.vdb) volumetric import (#724). Unlike Alembic/USD/MaterialX above, this is
# deliberately NOT linked onto the OloEngine target — the issue's second acceptance
Expand Down Expand Up @@ -710,6 +714,10 @@ add_subdirectory(OloCtl)
# Every target has handed its heavy TUs to olo_bind_heavy_compile_pool() by now.
olo_write_heavy_compile_manifest()

# One oneTBB per linked binary (#1575): every target exists by now, so the check sees them all.
include(cmake/OneTbbGuard.cmake)
olo_add_onetbb_guard(OloEngine)

set_target_properties(OloEngine-LuaScriptCore PROPERTIES FOLDER "Scripting")
set_target_properties(OloServer PROPERTIES FOLDER "Applications")
set_target_properties(oloctl PROPERTIES FOLDER "Applications")
Expand Down
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified OloEditor/assets/tests/visual/GroomLod_GL_Deferred.png
Binary file modified OloEditor/assets/tests/visual/GroomLod_GL_Forward.png
Binary file modified OloEditor/assets/tests/visual/GroomLod_GL_ForwardPlus.png
Binary file modified OloEditor/assets/tests/visual/GroomLod_GL_Forward_Far.png
Binary file modified OloEditor/assets/tests/visual/GroomLod_GL_Forward_Near.png
Binary file modified OloEditor/assets/tests/visual/GroomSimulationOff_GL_Deferred.png
Binary file modified OloEditor/assets/tests/visual/GroomSimulationOff_GL_Forward.png
Binary file modified OloEditor/assets/tests/visual/GroomSimulationOff_GL_ForwardPlus.png
Binary file modified OloEditor/assets/tests/visual/GroomSimulation_GL_Deferred.png
Binary file modified OloEditor/assets/tests/visual/GroomSimulation_GL_Forward.png
Binary file modified OloEditor/assets/tests/visual/GroomSimulation_GL_ForwardPlus.png
29 changes: 14 additions & 15 deletions OloEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -475,18 +475,18 @@ if(OLO_WITH_MATERIALX)
endif()
endif()
if(OLO_WITH_USD)
# Resolve OpenUSD: reuse a prebuilt install (OLO_USD_INSTALL_DIR) or build oneTBB + static-
# monolithic USD from source via ExternalProject into a per-user cache. Sets OloEngine_USD_PREFIX
# + the olo_usd_ext target. USD's CMake is install-centric (its runtime plugInfo tree is produced
# Resolve OpenUSD: build static-monolithic USD from source via ExternalProject, against vcpkg's
# oneTBB, into a per-user cache. Sets the OloEngine_USD_* paths, the GLOBAL TBB::tbb target and
# the olo_usd_ext target. USD's CMake is install-centric (its runtime plugInfo tree is produced
# by the install step), so it is NOT FetchContent'd. See docs/agent-rules/asset-import-usd-alembic.md.
include(${CMAKE_SOURCE_DIR}/cmake/vendor/OpenUSD.cmake)
add_dependencies(OloEngine olo_usd_ext) # build USD before the engine (and downstream exe) link
# OLO_WITH_USD and USD's include directory reach ONLY the TUs that use them (#1386). No header
# outside USD/ tests the macro, and UsdMeshImporter.h is included only by these TUs; a new
# `#if defined(OLO_WITH_USD)` elsewhere must be added to this list. USD's install prefix ships
# its own oneTBB headers (2022.2), and as a PUBLIC include directory they shadowed vcpkg's
# (2023.1) in every TU, OpenVDB's users included. The two static oneTBB libraries are still
# both linked (#1575). These TUs skip the precompiled header: a define the PCH was not
# `#if defined(OLO_WITH_USD)` elsewhere must be added to this list. USD's install prefix used
# to ship its own oneTBB headers (2022.2), and as a PUBLIC include directory they shadowed
# vcpkg's (2023.1) in every TU, OpenVDB's users included; since #1575 USD is built against
# vcpkg's oneTBB and ships none. These TUs skip the precompiled header: a define the PCH was not
# built with must not be mixed into one, and MSVC's STL fixes its deprecation macros when the
# PCH is built, so the silencers below would arrive too late.
set(_olo_usd_tus
Expand All @@ -500,19 +500,18 @@ if(OLO_WITH_USD)
# USD and a Release/RelWithDebInfo engine links Release USD — no CRT mismatch.
# USD's own headers use C++23-deprecated std::aligned_storage and has_denorm, which the
# -Werror=deprecated-declarations rule (#1501) would reject. Silence exactly those two, in
# the one TU that includes USD. Not SYSTEM: that reorders the search path and USD's
# headers then find vcpkg's oneTBB instead of the one USD was built against.
# the one TU that includes USD. Not SYSTEM: it would reorder the search path for every
# header this TU includes, which only this one TU's include list needs.
set(_olo_usd_importer "${CMAKE_CURRENT_SOURCE_DIR}/src/OloEngine/Asset/Interchange/USD/UsdMeshImporter.cpp")
set_property(SOURCE "${_olo_usd_importer}" TARGET_DIRECTORY OloEngine APPEND PROPERTY
INCLUDE_DIRECTORIES "${OloEngine_USD_INCLUDE_DIR}")
set_property(SOURCE "${_olo_usd_importer}" TARGET_DIRECTORY OloEngine APPEND PROPERTY
COMPILE_DEFINITIONS _SILENCE_CXX23_ALIGNED_STORAGE_DEPRECATION_WARNING _SILENCE_CXX23_DENORM_DEPRECATION_WARNING)
unset(_olo_usd_importer)
# oneTBB auto-links tbb12(.|_debug.)lib via #pragma comment(lib) inside usd_m, so the config's
# lib dir must be on the linker search path. Shlwapi (Path*) + Dbghelp (Sym*) + Ws2_32 are the
# Windows system libs USD's Arch/Tf need (verified via the USD smoke test).
target_link_directories(OloEngine PUBLIC "${OloEngine_USD_LIB_DIR}")
target_link_libraries(OloEngine "${OloEngine_USD_LIB}" "${OloEngine_USD_TBB_LIB}")
# oneTBB is vcpkg's TBB::tbb, the one OpenVDB links too (#1575): never a second tbb12 archive.
# cmake/OneTbbGuard.cmake fails the configure if one reaches a link. Shlwapi (Path*) + Dbghelp
# (Sym*) + Ws2_32 are the Windows system libs USD's Arch/Tf need (verified via the USD smoke test).
target_link_libraries(OloEngine "${OloEngine_USD_LIB}" TBB::tbb)
if(MSVC)
target_link_libraries(OloEngine Shlwapi Dbghelp Ws2_32)
endif()
Expand All @@ -527,7 +526,7 @@ if(OLO_WITH_USD)
# OloEngine_USD_LIB's $<CONFIG> genexpr makes it config-matched into the bargain.
# Do NOT delete the option to "fix" a whole-archive error — it is load-bearing under
# both linkers, silently at runtime for link.exe and loudly at link for lld-link.
# Full linker matrix + failure modes: docs/agent-rules/asset-import-usd-alembic.md.
# Full linker matrix + failure modes: docs/agent-rules/openusd-build-and-link.md.
if(MSVC)
target_link_options(OloEngine PUBLIC "/WHOLEARCHIVE:${OloEngine_USD_LIB}")
endif()
Expand Down
19 changes: 19 additions & 0 deletions OloEngine/src/OloEngine/Groom/GroomLod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,25 @@ namespace OloEngine
return 1.0f - (pixelSize / sourcePixelSize);
}

f32 GroomCardCoverageGrowthReach(u32 baseStrands, u32 cards) noexcept
{
if (cards == 0u || baseStrands == 0u)
{
return 0.0f;
}
const f32 strandsPerCard = static_cast<f32>(static_cast<f64>(baseStrands) / static_cast<f64>(cards));
return std::clamp((strandsPerCard - 1.0f) / (kGroomCardFullLockStrands - 1.0f), 0.0f, 1.0f);
}

f32 GroomCardReachedGrowth(f32 growth, f32 reach) noexcept
{
if (!std::isfinite(growth) || !std::isfinite(reach) || !(growth > 1.0f))
{
return 1.0f;
}
return 1.0f + (growth - 1.0f) * std::clamp(reach, 0.0f, 1.0f);
}

namespace
{
// span (1 - exp(-summed / span)): bands of summed width `summed` spread
Expand Down
25 changes: 25 additions & 0 deletions OloEngine/src/OloEngine/Groom/GroomLod.h
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,31 @@ namespace OloEngine
/// "draw the card as cooked".
[[nodiscard]] f32 GroomCardCoverageGrowthBlend(f32 pixelSize, f32 sourcePixelSize) noexcept;

/// How many strands a card must stand for before its coverage growth applies
/// in full (#1595): a lock, the population GroomCardCoverageWiden's law
/// describes and was validated on (GroomLodCook.AWidenedCardCoversWhatItsMembers
/// CoverFartherAway's 32-strand lock).
inline constexpr f32 kGroomCardFullLockStrands = 32.0f;

/// The share of its cooked coverage growth a card level reaches (#1595):
/// (strands per card - 1) / (kGroomCardFullLockStrands - 1), clamped to
/// [0, 1], from the base groom's and the card level's curve counts.
///
/// The growth law widens a card toward what its members cover as a LOCK.
/// A card standing for one strand is that strand and has nothing to grow; a
/// thin coat, a handful of strands behind each card, is between the two.
/// Measured on the GroomAnimals horses (9 and 14 strands a card), whose
/// cards cover their strands without any growth (1.145 and 1.013 at 32 m) and
/// whose full growth put the short coat at 1.21x its strands, past the 1.2
/// ceiling. The foxes (26-30) and the dogs (45-127) fall 4-15% short of
/// their strands without it and keep it. 0 for no cards or no strands.
[[nodiscard]] f32 GroomCardCoverageGrowthReach(u32 baseStrands, u32 cards) noexcept;

/// A card point's growth as drawn: 1 + reach (growth - 1), the cooked growth's
/// excess over 1 scaled by GroomCardCoverageGrowthReach. A growth that is not
/// finite or not above 1, or a reach that is not finite, draws as cooked (1).
[[nodiscard]] f32 GroomCardReachedGrowth(f32 growth, f32 reach) noexcept;

/// How much wider a card is drawn than it was cooked (#1558): the CPU
/// mirror of GroomStrand.glsl's oloGroomCardCoverageWiden. Every argument is
/// in units of a pixel now: the card's cooked width, its members' summed
Expand Down
6 changes: 4 additions & 2 deletions OloEngine/src/OloEngine/Groom/GroomStrandMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ namespace OloEngine
source.BaseCurveCount = base.GetCurveCount();
source.BaseGroupRanges = base.GetGroupRanges();
source.CoverageGrowth = { level.PointCoverageGrowth.GetData(), static_cast<sizet>(level.PointCoverageGrowth.Num()) };
source.CoverageGrowthReach = GroomCardCoverageGrowthReach(base.GetCurveCount(), level.GetCurveCount());
return source;
}

Expand Down Expand Up @@ -842,8 +843,9 @@ namespace OloEngine
segment.Radius1 = widths[first + i + 1u] * 0.5f * widthScale;
if (!source.CoverageGrowth.empty())
{
segment.Growth0 = source.CoverageGrowth[first + i];
segment.Growth1 = source.CoverageGrowth[first + i + 1u];
segment.Growth0 = GroomCardReachedGrowth(source.CoverageGrowth[first + i], source.CoverageGrowthReach);
segment.Growth1 =
GroomCardReachedGrowth(source.CoverageGrowth[first + i + 1u], source.CoverageGrowthReach);
}
else
{
Expand Down
5 changes: 5 additions & 0 deletions OloEngine/src/OloEngine/Groom/GroomStrandMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,11 @@ namespace OloEngine
/// for the base groom, which reads as 1 everywhere.
std::span<const f32> CoverageGrowth{};

/// The share of CoverageGrowth the stream carries (#1595): the level's
/// GroomCardCoverageGrowthReach, so a thin coat's cards grow less than a
/// lock's. Unused when CoverageGrowth is empty.
f32 CoverageGrowthReach = 1.0f;

[[nodiscard]] u32 SourceCurve(u32 curve) const noexcept
{
return SourceCurves.empty() ? curve : SourceCurves[curve];
Expand Down
14 changes: 14 additions & 0 deletions OloEngine/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ add_executable(OloEngine-Tests
BitwiseEqualLayoutTest.cpp
ModelImporterTest.cpp
MeshInterchangeTest.cpp
# A real OpenUSD stage through the importer registry (#1575); empty without OLO_WITH_USD.
UsdStageImportTest.cpp
VolumeImportTest.cpp
SceneMeshRaycastTest.cpp
SlugFontRenderingTest.cpp
Expand Down Expand Up @@ -1918,6 +1920,18 @@ olo_copy_ffmpeg_runtime(OloEngine-Tests)
olo_copy_steam_runtime(OloEngine-Tests)
olo_copy_aftermath_runtime(OloEngine-Tests)

# UsdStageImportTest opens a real stage (#1575). It needs OLO_WITH_USD, which reaches only the TUs
# that test it (#1386, see OloEngine/CMakeLists.txt), so it skips the precompiled header for the
# same reason those do. UsdStage::Open needs USD's plugInfo tree next to the binary, staged the way
# OloEditor/src/CMakeLists.txt stages it for the editor.
if(OLO_WITH_USD AND OLO_USD_PLUGIN_TREE)
set_property(SOURCE UsdStageImportTest.cpp APPEND PROPERTY COMPILE_DEFINITIONS OLO_WITH_USD=1)
set_property(SOURCE UsdStageImportTest.cpp PROPERTY SKIP_PRECOMPILE_HEADERS ON)
add_custom_command(TARGET OloEngine-Tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory "${OLO_USD_PLUGIN_TREE}" "$<TARGET_FILE_DIR:OloEngine-Tests>/usd"
COMMENT "Staging USD plugInfo tree next to OloEngine-Tests")
endif()

target_compile_definitions(OloEngine-Tests PRIVATE SOL_ALL_SAFETIES_ON=1)

# Tests do not inherit the engine's private OLO_DEBUG macro. These policy
Expand Down
87 changes: 83 additions & 4 deletions OloEngine/tests/Groom/GroomLodContractTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include <span>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

using namespace OloEngine;
Expand Down Expand Up @@ -890,9 +891,13 @@ TEST(GroomLodCook, ACardCarriesHowMuchWiderItsMembersCoverFarAway)
EXPECT_NE(reason.find("coverage growth"), std::string::npos) << reason;

// The stream carries it to the shader per corner, the level's value at each
// segment's end; the strand tier carries 1. (The level was rebuilt above, so
// its view is taken again: the old one's spans point at freed arrays.)
// segment's end, reached as far as this coat's cards stand for a lock
// (#1595): two cards of sixteen strands each reach 15/31 of it. The strand
// tier carries 1. (The level was rebuilt above, so its view is taken again:
// the old one's spans point at freed arrays.)
const GroomCurveView cards = level.GetCurveView();
const f32 reach = GroomCardCoverageGrowthReach(groom->GetCurveCount(), cards.GetCurveCount());
ASSERT_NEAR(reach, 15.0f / 31.0f, 1.0e-6f);
GroomStrandBuildSettings build;
build.MaxStrands = groom->GetCurveCount();
std::vector<GroomStrandVertex> vertices;
Expand All @@ -905,8 +910,8 @@ TEST(GroomLodCook, ACardCarriesHowMuchWiderItsMembersCoverFarAway)
const u32 first = cards.GetCurveFirstPoint(card);
for (u32 s = 0; s + 1u < cards.GetCurvePointCount(card); ++s)
{
const f32 g0 = level.PointCoverageGrowth[first + s];
const f32 g1 = level.PointCoverageGrowth[first + s + 1u];
const f32 g0 = GroomCardReachedGrowth(level.PointCoverageGrowth[first + s], reach);
const f32 g1 = GroomCardReachedGrowth(level.PointCoverageGrowth[first + s + 1u], reach);
expected.insert(expected.end(), { g0, g0, g1, g1 });
}
}
Expand Down Expand Up @@ -1002,6 +1007,80 @@ TEST(GroomLodCook, AWidenedCardCoversWhatItsMembersCoverFartherAway)
}
}

TEST(GroomLodCook, AThinCoatsCardsGrowOnlyAsFarAsTheyStandForALock)
{
// #1595. The growth law widens a card toward what its members cover as a
// LOCK; a card standing for one strand is that strand. The GroomAnimals
// horses, 9 and 14 strands a card, cover their strands at 32 m with no
// growth at all (1.145 and 1.013), and full growth put the short coat at
// 1.211, past the card tier's 1.2 ceiling. The foxes (26-30 a card) and the
// dogs (45-127) fall 4-15% short without it. So a coat reaches the share
// (strands per card - 1) / 31 of its cooked growth, all of it from a lock of 32.
EXPECT_FLOAT_EQ(GroomCardCoverageGrowthReach(100u, 100u), 0.0f) << "a card per strand is the strand";
EXPECT_NEAR(GroomCardCoverageGrowthReach(56895u, 6243u), (56895.0f / 6243.0f - 1.0f) / 31.0f, 1.0e-6f)
<< "the short horse: a quarter of its growth";
EXPECT_LT(GroomCardCoverageGrowthReach(56895u, 6243u), 0.3f);
EXPECT_LT(GroomCardCoverageGrowthReach(93727u, 6620u), 0.5f) << "the long horse";
EXPECT_GT(GroomCardCoverageGrowthReach(265724u, 8947u), 0.9f) << "the short fox keeps nearly all of it";
for (const auto [strands, cards] : { std::pair{ 409940u, 9111u }, std::pair{ 1999138u, 15688u },
std::pair{ 1615603u, 14938u }, std::pair{ 32u, 1u } })
{
EXPECT_FLOAT_EQ(GroomCardCoverageGrowthReach(strands, cards), 1.0f) << strands << " / " << cards;
}
// Monotone in how many strands stand behind a card, and never past [0, 1].
f32 previous = 0.0f;
for (u32 strands = 1u; strands <= 64u; ++strands)
{
const f32 reach = GroomCardCoverageGrowthReach(strands, 1u);
EXPECT_GE(reach, previous) << strands;
EXPECT_LE(reach, 1.0f) << strands;
previous = reach;
}
// Nothing to stand for, or nothing standing: no growth.
EXPECT_FLOAT_EQ(GroomCardCoverageGrowthReach(0u, 10u), 0.0f);
EXPECT_FLOAT_EQ(GroomCardCoverageGrowthReach(10u, 0u), 0.0f);

// The drawn growth: the cooked excess over 1 times the reach, the identity at
// a full lock and 1 for anything unmeasurable.
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(3.0f, 1.0f), 3.0f);
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(3.0f, 0.25f), 1.5f);
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(3.0f, 0.0f), 1.0f);
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(3.0f, 2.0f), 3.0f) << "the reach is a share";
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(3.0f, -1.0f), 1.0f);
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(0.5f, 1.0f), 1.0f) << "never under the cooked width";
for (const f32 bad : { std::numeric_limits<f32>::quiet_NaN(), std::numeric_limits<f32>::infinity() })
{
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(bad, 1.0f), 1.0f);
EXPECT_FLOAT_EQ(GroomCardReachedGrowth(3.0f, bad), 1.0f);
}

// Through the real cook and stream: a full lock carries its cooked growth to
// the shader unchanged, a lock of eight carries 7/31 of it.
for (const u32 strands : { 32u, 8u })
{
SCOPED_TRACE(strands);
const Ref<GroomAsset> groom = MakeSpreadLock(strands, 4.5e-3f, 5.0e-4f);
ASSERT_TRUE(groom);
GroomCardSettings settings;
settings.SourcePixelSize = 12.5f;
GroomLodLevel level;
std::string reason;
ASSERT_TRUE(GroomLodBuilder::BuildCardLevel(*groom, settings, level, reason, nullptr)) << reason;
ASSERT_EQ(level.GetCurveCount(), 1u);
ASSERT_GT(level.PointCoverageGrowth[1], 1.05f) << "a lock whose strands barely overlap tests nothing";
GroomStrandBuildSettings build;
build.MaxStrands = groom->GetCurveCount();
std::vector<GroomStrandVertex> vertices;
std::vector<u32> indices;
(void)BuildGroomStrandMesh(GroomBuildSource::FromLevel(*groom, level), build, vertices, indices);
ASSERT_FALSE(vertices.empty());
// Corners 2 and 3 are the first segment's far end: the card's point 1.
const f32 cooked = level.PointCoverageGrowth[1];
const f32 expected = strands == 32u ? cooked : 1.0f + (cooked - 1.0f) * (7.0f / 31.0f);
EXPECT_NEAR(vertices[2].CoverageGrowth, expected, 1.0e-5f);
}
}

TEST(GroomLodPolicy, ACardGrowsByTheShareOfItsCookedFootprintTheCoatHasShrunkPast)
{
// #1558: 1 - pixelSize / sourcePixelSize below the cooked size -- one minus
Expand Down
Loading
Loading