Files
西风逍遥游andCursor Agent 7ad13bf3ac feat(graphics): hair/groom subsystem (Phases 1–5 + Cards wiring) (#401)
* feat(graphics): add Phase 1 hair/groom subsystem under graphics/hair

Introduce a UE5-Groom-inspired strand data model, procedural growth,
ribbon expansion, GroomAsset/GroomInstance, design doc, and CPU tests.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* fix(graphics): move GroomInstance getters to .cpp for binding contracts

Binding contract generation only resolves methods defined in translation
units; inline header getters were reported as unresolved.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* docs(graphics): record Phase 1 hair/groom implementation status

Note ctest results and the binding-contract getter-in-cpp requirement.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* feat(graphics): add ClusterGrid cull for hair groom Phase 2

Wire optional CPU cluster frustum culling into GroomInstance and keep
Cards geometric LOD owned by HairCards (PR #400) instead of duplicating
a CardsBuilder under graphics/hair.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* feat(graphics): multi-group groom bake and Marschner lobe weights

Merge all groom groups into one ribbon mesh via appendRibbonMesh, and
expose Marschner R/TT/TRT push-constant weights on the hair shader
(GLSL/WGSL/SPIR-V) with script bindings and coverage tests.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* test(graphics): begin 3D frame before groom Marschner draw smoke

GroomInstance::draw routes through drawMeshShader, which requires an
active 3D frame; wrap the multi-group GPU smoke accordingly.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* feat(graphics): add analytical hair self-shadow and root AO

Finish Phase 3 shading with push-constant fiber self-shadow and
along-strand root AO (not deep shadow maps), wired through
GroomInstance and script bindings, with updated SPIR-V/WGSL and tests.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* feat(graphics): add groom skin binding and guide interpolation

Phase 4 CPU binding projects strand roots onto skin triangles with
Rigid/Offset deform, and guide extraction/weights drive
Rigid/Offset/Smooth strand interpolation without animation coupling.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* feat(graphics): add guide XPBD/Verlet simulation for groom Phase 5

Introduce GuideSimulator (Verlet + XPBD distance constraints, pinned roots,
optional ground plane) without a physics-module include, wire it through
GroomInstance::enableGuideSimulation/update, and cover it with hair unit tests.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* feat(graphics): wire groom Cards LOD through HairCards

Representation::Cards now expands strands via HairCards polylines and
uploadCardMesh; default mid LOD switches to Cards. Adds Cards LOD bake/draw
coverage after merging #400.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* docs(graphics): document GroomInstance script bindings

Source quality was failing on NEW binding gaps for hair/groom APIs.
Add newGroomInstance and GroomInstance LOD/shading getters to graphics.md.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* docs(graphics): add ownership/lifetime contracts for groom APIs

Satisfy api-lifetime architecture gate for newGroomInstance,
GroomInstance::primaryGroup, and kHairFragWgsl.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-15 11:32:53 +08:00

103 lines
3.5 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "Fixtures.h"
#include "graphics/AmbientOcclusion.h"
#include "graphics/AntiAliasing.h"
#include "graphics/Canvas.h"
#include "graphics/DrawItem2D.h"
#include "graphics/Font.h"
#include "graphics/GBuffer.h"
#include "graphics/GlobalIllumination.h"
#include "graphics/Graphics.h"
#include "graphics/Grass.h"
#include "graphics/HairShader.h"
#include "graphics/Light.h"
#include "graphics/Material.h"
#include "graphics/Mesh.h"
#include "graphics/Outline.h"
#include "graphics/Quad.h"
#include "graphics/RenderControl.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/ScreenSpaceReflection.h"
#include "graphics/Shader.h"
#include "graphics/Texture.h"
#include "graphics/Volumetric.h"
#include "graphics/Water.h"
#include "graphics/Waterfall.h"
#include "graphics/shaders/mesh3d_hair_frag_spv.inc"
#include "graphics/shaders/mesh3d_hair_vert_spv.inc"
#include "window/Window.h"
using eve::graphics::Graphics;
using eve::graphics::Renderable3D;
using eve::graphics::Shader;
TEST_CASE("graphics.HairShader.bindDefaults") {
Shader sh;
sh.setKind(Shader::Kind::eMesh3D);
eve::graphics::hair::bindDefaults(&sh);
CHECK(sh.hasUniform("specExp"));
CHECK(sh.hasUniform("strandDir"));
CHECK(sh.hasUniform("marschnerR"));
CHECK(sh.hasUniform("marschnerTT"));
CHECK(sh.hasUniform("marschnerTRT"));
CHECK(sh.hasUniform("selfShadowStrength"));
CHECK(sh.hasUniform("selfShadowBias"));
CHECK(sh.hasUniform("rootAoStrength"));
CHECK_EQ(sh.getUniformIndex("specExp"), 0);
CHECK_EQ(sh.getUniformIndex("strandDir"), 6);
CHECK_EQ(sh.getUniformIndex("marschnerR"), 9);
CHECK_EQ(sh.usedFloats(), 15);
}
TEST_CASE("graphics.HairShader.paramNames") {
CHECK_EQ(eve::graphics::hair::paramCount(), 15);
CHECK_EQ(eve::graphics::hair::paramName(0), std::string("specExp"));
CHECK_EQ(eve::graphics::hair::paramName(8), std::string("strandDirZ"));
CHECK_EQ(eve::graphics::hair::paramName(9), std::string("marschnerR"));
CHECK_EQ(eve::graphics::hair::paramName(11), std::string("marschnerTRT"));
CHECK_EQ(eve::graphics::hair::paramName(12), std::string("selfShadowStrength"));
CHECK_EQ(eve::graphics::hair::paramName(14), std::string("rootAoStrength"));
}
TEST_CASE("graphics.HairShader.spvMagic") {
CHECK(mesh3d_hair_vert_spv_count > 0);
CHECK(mesh3d_hair_frag_spv_count > 0);
CHECK_EQ(mesh3d_hair_vert_spv[0], 0x07230203u);
CHECK_EQ(mesh3d_hair_frag_spv[0], 0x07230203u);
}
TEST_CASE("graphics.HairShader.createGpuPipeline") {
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx, 96, 64);
Shader *shader = gfx->newHairShader();
REQUIRE(shader != nullptr);
REQUIRE(shader->gpuHandle != nullptr);
CHECK(shader->hasUniform("specExp"));
CHECK(shader->hasUniform("strandDir"));
CHECK(shader->hasUniform("marschnerR"));
shader->sendFloat("specStrength", 0.7f);
shader->sendVec3("strandDir", 0.f, 1.f, 0.f);
shader->sendFloat("marschnerR", 1.2f);
shader->sendFloat("marschnerTT", 0.5f);
shader->sendFloat("marschnerTRT", 0.3f);
CHECK(shader->hasUniform("selfShadowStrength"));
shader->sendFloat("selfShadowStrength", 0.5f);
shader->sendFloat("selfShadowBias", 0.2f);
shader->sendFloat("rootAoStrength", 0.4f);
win->close();
}
TEST_CASE("graphics.Renderable3D.hairFlag") {
auto *r = Renderable3D::create();
REQUIRE(r != nullptr);
CHECK(!r->getHair());
r->setHair(true);
CHECK(r->getHair());
r->setHair(false);
CHECK(!r->getHair());
}