2#include <assimp/scene.h>
3#include <glm/gtc/quaternion.hpp>
4#include <glm/gtx/matrix_decompose.hpp>
24 for (
auto& part :
parts) {
25 if (
auto* entity = ecs::try_get(part.entity)) ecs::DestroyEntity(entity);
26 if (
auto* entity = ecs::try_get(part.outline)) ecs::DestroyEntity(entity);
28 if (
auto* gfx = ModuleManager::getInstance<graphics::Graphics>(
"Graphics"); gfx && !
providerLifetime.expired()) {
29 for (
auto& part :
parts)
30 if (gfx->releaseMesh(part.mesh))
delete part.mesh;
33 if (gfx->releaseTexture(texture))
delete texture;
39 auto* fs = filesystem::Filesystem::create();
40 std::unique_ptr<filesystem::FileData>
file(fs->read(std::string(
path)));
41 if (!
file)
throw std::runtime_error(
"VRM file could not be read");
42 auto parsed =
parseVrm({
static_cast<const std::byte*
>(
file->getData()),
file->getSize()});
43 if (!parsed.ok())
return R::failure(parsed.status());
44 auto* gfx = ModuleManager::getInstance<graphics::Graphics>(
"Graphics");
45 if (!gfx || gfx->getBackendName() !=
"vulkan")
47 "VRM import requires an initialized graphics provider",
49 auto candidate = std::make_unique<VrmRuntime>();
50 candidate->document = std::move(parsed).takeValue();
51 candidate->providerLifetime = gfx->resourceLifetime();
52 auto&
d = candidate->document;
54 candidate->model.reset(model3d::Model3D::create()->newModelData(
file.get(),
".glb"));
56 candidate->player = std::make_unique<animation::AnimPlayer>(candidate->skeleton.get());
57 candidate->nodeBones.resize(
d.nodes.size(), -1);
58 for (
size_t n = 0;
n <
d.nodes.size(); ++
n) {
59 if (!
d.nodes[
n].empty()) candidate->nodeBones[
n] = candidate->skeleton->findBone(
d.nodes[
n]);
61 auto requireNode = [&](
int node) {
62 if (
node >= 0 && candidate->nodeBones[
node] < 0)
63 throw std::runtime_error(
"Cannot resolve VRM node: " +
d.nodes[
node]);
66 for (
auto&
c :
d.colliders) requireNode(
c.node);
67 for (
auto&
s :
d.springs) {
68 requireNode(
s.center);
69 for (
auto& j :
s.joints) requireNode(j.node);
71 std::vector<VrmAtlasRect> rectangles;
73 candidate->textures.push_back(atlas);
74 for (
auto&
m :
d.materials) candidate->surfaces.push_back(
buildVrmSurface(*gfx,
m, *atlas, rectangles));
75 const auto*
scene = candidate->model->getScene();
76 for (
size_t n = 0;
n <
d.nodes.size(); ++
n) {
77 if (
d.nodeMeshes[
n] < 0)
continue;
78 const auto*
node =
scene->mRootNode->FindNode(
d.nodes[
n].c_str());
79 if (!
node)
throw std::runtime_error(
"Cannot resolve VRM mesh node: " +
d.nodes[
n]);
80 const auto&
slots =
d.meshMaterials[
d.nodeMeshes[
n]];
82 throw std::runtime_error(
"VRM primitive mapping changed during decode");
83 for (
unsigned p = 0;
p <
node->mNumMeshes; ++
p) {
88 if (part.
material < 0)
throw std::runtime_error(
"VRM primitive is missing its material");
89 candidate->parts.push_back(std::move(part));
90 auto&
owned = candidate->parts.back();
92 if (!
owned.mesh)
throw std::runtime_error(
"Mesh upload failed");
93 if (candidate->model->hasBones(
owned.sourceMesh)) {
95 candidate->skeleton.get()));
97 throw std::runtime_error(
"VRM GPU skin upload failed");
99 auto* entity = graphics::Renderable3D::create();
100 owned.entity = ecs::handle_of(entity);
101 entity->setVisible(
false);
102 entity->setMesh(
owned.mesh);
103 entity->setMaterial(candidate->surfaces[
owned.material]->material.get());
104 if (
auto*
material = candidate->surfaces[
owned.material]->outlineMaterial.get()) {
105 auto* outline = graphics::Renderable3D::create();
106 owned.outline = ecs::handle_of(outline);
107 outline->setVisible(
false);
108 outline->setMesh(
owned.mesh);
113 if (candidate->parts.empty())
throw std::runtime_error(
"VRM contains no renderable primitives");
114 candidate->particles_.resize(
d.springs.size());
115 for (
size_t i = 0; i <
d.springs.size(); ++i) candidate->particles_[i].resize(
d.springs[i].joints.size());
116 return R::success(std::move(candidate));
117 }
catch (
const std::exception& e) {
125 for (
auto& part :
parts) {
126 glm::mat4 partWorld = matrix;
127 if (!part.skin &&
nodeBones[part.node] >= 0) {
128 glm::mat4 nodeMatrix(1);
129 player->getPose()->getWorldMatrix(
nodeBones[part.node], &nodeMatrix[0][0]);
130 partWorld *= nodeMatrix;
132 glm::vec3
scale, translation, skew;
134 glm::vec4 perspective;
135 glm::decompose(partWorld,
scale,
rotation, translation, skew, perspective);
136 const glm::vec3 angles = glm::eulerAngles(
rotation);
137 for (
auto handle : {part.entity, part.outline})
139 entity->setPosition(translation.x, translation.y, translation.z);
140 entity->setRotation(angles.y, angles.x, angles.z);
150 for (
auto& part :
parts)
151 if (part.skin && !part.skin->updateGpuMesh(part.mesh, &
pose))
152 throw std::runtime_error(
"VRM skin palette no longer matches imported skeleton");
157 float blink = 0, mouth = 0, look = 0;
158 auto overrideValue = [](
const std::string& mode,
float weight) {
159 return mode ==
"block" &&
weight > 0 ? 1.f : mode ==
"blend" ?
weight : 0.f;
162 float w = std::clamp(avatar.
getParameter(e.name) + (gazeWeights_.contains(e.name) ? gazeWeights_[e.name] : 0.f),
164 if (e.binary)
w =
w > .5f ? 1.f : 0.f;
166 blink += overrideValue(e.overrideBlink,
w);
167 mouth += overrideValue(e.overrideMouth,
w);
168 look += overrideValue(e.overrideLookAt,
w);
171 s->current =
s->base;
173 s->uvOffset = {0, 0};
175 for (
auto& part :
parts) {
176 part.mesh->clearMorphWeights();
177 for (
int i = 0; i < part.mesh->getMorphCount(); ++i) {
178 auto name = part.mesh->getMorphName(i);
184 float suppression = 0;
185 if (e.name ==
"blink" || e.name ==
"blinkLeft" || e.name ==
"blinkRight") suppression = blink;
186 if (e.name ==
"aa" || e.name ==
"ih" || e.name ==
"ou" || e.name ==
"ee" || e.name ==
"oh") suppression = mouth;
187 if (e.name ==
"lookUp" || e.name ==
"lookDown" || e.name ==
"lookLeft" || e.name ==
"lookRight")
189 float w =
weights[i] * (1 - std::clamp(suppression, 0.f, 1.f));
190 if (e.binary && suppression > 0)
w = 0;
191 for (
const auto& bind : e.morphs)
192 for (
auto& part :
parts)
193 if (part.node == bind.node) {
194 auto name = part.mesh->getMorphName(bind.index);
195 part.mesh->setMorphWeight(
name, part.mesh->getMorphWeight(
name) + bind.weight *
w);
197 for (
const auto&
b : e.colors) {
199 auto add = [&](
auto& out,
const auto& base) {
200 for (
size_t c = 0;
c < out.size(); ++
c) out[
c] += (
b.target[
c] - base[
c]) *
w;
202 if (
b.type ==
"color")
203 add(
s.current.color,
s.base.color);
204 else if (
b.type ==
"emissionColor")
205 add(
s.current.emission,
s.base.emission);
206 else if (
b.type ==
"shadeColor")
207 add(
s.current.shade,
s.base.shade);
208 else if (
b.type ==
"matcapColor")
209 add(
s.current.matcap,
s.base.matcap);
210 else if (
b.type ==
"rimColor")
211 add(
s.current.rim,
s.base.rim);
212 else if (
b.type ==
"outlineColor")
213 add(
s.current.outline,
s.base.outline);
215 for (
const auto&
b : e.uvs)
216 for (
int c = 0;
c < 2; ++
c) {
221 auto* gfx = ModuleManager::getInstance<graphics::Graphics>(
"Graphics");
223 for (
auto& part :
parts)
224 if (part.mesh->isMorphDirty()) gfx->bakeMeshMorph(part.mesh);
eve::EntitySpatialPose pose
std::array< float, 4 > rotation
std::array< float, 3 > scale
std::weak_ptr< PrimitiveScene > scene
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static AnimSkeleton * loadSkeletonFromModel(const model3d::ModelData *model)
ModelData wrappers (AnimImporterModel.cpp; requires model3d).
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
static AnimSkin * fromModel(const model3d::ModelData *model, int meshIndex, const AnimSkeleton *skeleton)
Build skin binding for meshIndex on model, mapping bone names onto skeleton. Throws if the mesh has n...
Unified avatar instance. Kind is a string: "image" | "live2d" | "vroid". Script-facing API avoids ove...
float getParameter(const std::string &name) const
Returns the parameter.
bool hasParameter(const std::string &name) const
True when parameter.
static eve::Result< std::unique_ptr< VrmRuntime > > prepare(std::string_view path)
Prepare.
std::vector< int > nodeBones
void animate(AvatarInstance &avatar, animation::AnimPose &pose, float dt)
Animate.
std::vector< std::unique_ptr< VrmSurface > > surfaces
void morphs(AvatarInstance &avatar)
Morphs.
std::vector< graphics::Texture * > textures
std::vector< Part > parts
void sync(AvatarInstance &avatar, const glm::mat4 &world, bool visible)
Synchronizes .
std::weak_ptr< const void > providerLifetime
std::unique_ptr< animation::AnimPlayer > player
VrmRuntime()
Constructs a VrmRuntime.
~VrmRuntime()
Releases VrmRuntime resources.
EVENGINE_API_BACKENDS public API.
eve::Result< VrmDocument > parseVrm(std::span< const std::byte > bytes)
Validate a self-contained GLB's VRM extension before publishing runtime state.
graphics::Texture & buildVrmAtlas(graphics::Graphics &gfx, const VrmDocument &d, std::vector< VrmAtlasRect > &rects)
Prepare an atlas using the provider's owning resource factory. @ownership Graphics owns the result; c...
std::unique_ptr< VrmSurface > buildVrmSurface(graphics::Graphics &gfx, const VrmMaterial &data, graphics::Texture &atlas, const std::vector< VrmAtlasRect > &rects)
Builds vrm surface.
std::vector< VrmExpression > expressions