7#include <assimp/mesh.h>
8#include <assimp/vector3.h>
31aiVector3D unitOrUp(
float x,
float y,
float z) {
34 return aiVector3D(0.f, 0.f, 1.f);
38bool ensureNormals(aiMesh *
mesh) {
39 if (!
mesh ||
mesh->mNumVertices == 0 || !
mesh->mVertices)
43 mesh->mNormals =
new aiVector3D[
mesh->mNumVertices];
44 for (
unsigned i = 0; i <
mesh->mNumVertices; ++i)
45 mesh->mNormals[i] = aiVector3D(0.f, 0.f, 1.f);
49bool meshAabbCenter(
const aiMesh *
mesh, aiVector3D *out) {
50 if (!
mesh ||
mesh->mNumVertices == 0 || !
mesh->mVertices)
52 aiVector3D lo =
mesh->mVertices[0];
53 aiVector3D hi =
mesh->mVertices[0];
54 for (
unsigned i = 1; i <
mesh->mNumVertices; ++i) {
55 const aiVector3D &
p =
mesh->mVertices[i];
56 lo.x = std::min(lo.x,
p.x);
57 lo.y = std::min(lo.y,
p.y);
58 lo.z = std::min(lo.z,
p.z);
59 hi.x = std::max(hi.x,
p.x);
60 hi.y = std::max(hi.y,
p.y);
61 hi.z = std::max(hi.z,
p.z);
63 *out = aiVector3D((lo.x + hi.x) * 0.5f, (lo.y + hi.y) * 0.5f, (lo.z + hi.z) * 0.5f);
67void applyRadial(aiMesh *
mesh,
const aiVector3D &
origin) {
68 for (
unsigned i = 0; i <
mesh->mNumVertices; ++i) {
69 const aiVector3D &
p =
mesh->mVertices[i];
85Vec3 add(
const Vec3 &
a,
const Vec3 &
b) {
return {
a.x +
b.x,
a.y +
b.y,
a.z +
b.z}; }
86Vec3 sub(
const Vec3 &
a,
const Vec3 &
b) {
return {
a.x -
b.x,
a.y -
b.y,
a.z -
b.z}; }
88float dot3(
const Vec3 &
a,
const Vec3 &
b) {
return a.x *
b.x +
a.y *
b.y +
a.z *
b.z; }
89Vec3 cross3(
const Vec3 &
a,
const Vec3 &
b) {
90 return {
a.y *
b.z -
a.z *
b.y,
a.z *
b.x -
a.x *
b.z,
a.x *
b.y -
a.y *
b.x};
92Vec3 normalize3(
const Vec3 &
a) {
93 const aiVector3D
n = unitOrUp(
a.x,
a.y,
a.z);
94 return {
n.x,
n.y,
n.z};
97bool barycentric2(
const Vec2 &
p,
const Vec2 &
a,
const Vec2 &
b,
const Vec2 &
c,
float *wa,
float *wb,
99 const float den = (
b.y -
c.y) * (
a.x -
c.x) + (
c.x -
b.x) * (
a.y -
c.y);
100 if (!std::isfinite(den) || std::fabs(den) <= 1e-12f)
102 *wa = ((
b.y -
c.y) * (
p.x -
c.x) + (
c.x -
b.x) * (
p.y -
c.y)) / den;
103 *wb = ((
c.y -
a.y) * (
p.x -
c.x) + (
a.x -
c.x) * (
p.y -
c.y)) / den;
104 *wc = 1.f - *wa - *wb;
110 c.r = std::clamp(
n.x * 0.5f + 0.5f, 0.f, 1.f);
111 c.g = std::clamp(
n.y * 0.5f + 0.5f, 0.f, 1.f);
112 c.b = std::clamp(
n.z * 0.5f + 0.5f, 0.f, 1.f);
117Vec3 toTangent(
const Vec3 &objectNormal,
const Vec3 &pos0,
const Vec3 &pos1,
const Vec3 &pos2,
118 const Vec2 &uv0,
const Vec2 &uv1,
const Vec2 &uv2) {
119 const Vec3 e1 = sub(pos1, pos0);
120 const Vec3 e2 = sub(pos2, pos0);
121 const float du1 = uv1.x - uv0.x;
122 const float dv1 = uv1.y - uv0.y;
123 const float du2 = uv2.x - uv0.x;
124 const float dv2 = uv2.y - uv0.y;
125 const float det = du1 * dv2 - du2 * dv1;
126 Vec3 geometric = normalize3(cross3(e1, e2));
127 if (dot3(geometric, objectNormal) < 0.f)
128 geometric =
scale(geometric, -1.f);
131 if (!std::isfinite(det) || std::fabs(det) <= 1e-12f) {
133 if (std::fabs(dot3(
tangent, geometric)) > 0.9f)
136 bitangent = normalize3(cross3(geometric,
tangent));
138 const float inv = 1.f / det;
140 bitangent = normalize3(add(
scale(e1, -du2 * inv),
scale(e2, du1 * inv)));
142 const Vec3 reconstructed = cross3(geometric,
tangent);
143 if (dot3(reconstructed, bitangent) < 0.f)
144 bitangent =
scale(reconstructed, -1.f);
146 bitangent = reconstructed;
148 return {dot3(objectNormal,
tangent), dot3(objectNormal, bitangent),
149 dot3(objectNormal, geometric)};
156 aiMesh *
mesh = meshAtMutable(meshIndex);
157 if (!
mesh || vertexIndex < 0 ||
static_cast<unsigned>(vertexIndex) >=
mesh->mNumVertices)
159 invalidArg(
"invalid mesh or vertex index",
"model3d.setVertexNormal"));
160 if (!ensureNormals(
mesh))
162 invalidArg(
"mesh has no vertices",
"model3d.setVertexNormal"));
163 mesh->mNormals[vertexIndex] = unitOrUp(
x,
y,
z);
168 aiMesh *
mesh = meshAtMutable(meshIndex);
171 invalidArg(
"invalid mesh index",
"model3d.applyVertexNormals"));
175 invalidArg(
"mesh has no vertices",
"model3d.applyVertexNormals"));
180 float originX,
float originY,
float originZ) {
181 aiMesh *
mesh = meshAtMutable(meshIndex);
184 invalidArg(
"invalid mesh index",
"model3d.applyVertexNormalsFrom"));
185 if (
kind !=
"radial")
187 unsupported(
"unknown vertex-normal kind",
"model3d.applyVertexNormalsFrom.kind"));
188 if (!ensureNormals(
mesh))
190 invalidArg(
"mesh has no vertices",
"model3d.applyVertexNormalsFrom"));
191 applyRadial(
mesh, aiVector3D(originX, originY, originZ));
196 int meshIndex,
int width,
int height,
int uvChannel, std::string_view space)
const {
197 using ImagePtr = std::unique_ptr<image::ImageData>;
198 const aiMesh *
mesh = meshAt(meshIndex);
201 invalidArg(
"invalid mesh index",
"model3d.bakeNormalMap"));
204 invalidArg(
"normal map size must be positive",
"model3d.bakeNormalMap.size"));
205 if (space !=
"tangent" && space !=
"object")
207 unsupported(
"space must be \"tangent\" or \"object\"",
"model3d.bakeNormalMap.space"));
208 if (!
mesh->HasNormals())
210 "mesh has no vertex normals; apply or set them before baking",
211 "model3d.bakeNormalMap.normals"));
212 if (uvChannel < 0 || uvChannel >= AI_MAX_NUMBER_OF_TEXTURECOORDS ||
213 !
mesh->HasTextureCoords(
static_cast<unsigned>(uvChannel)))
216 "model3d.bakeNormalMap.channel"));
218 for (
unsigned f = 0;
f <
mesh->mNumFaces; ++
f) {
219 if (
mesh->mFaces[
f].mNumIndices != 3)
221 "normal-map baking requires triangulated faces",
"model3d.bakeNormalMap.triangle"));
234 const float widthF =
static_cast<float>(
width);
235 const float heightF =
static_cast<float>(
height);
236 const bool tangentSpace = space ==
"tangent";
238 for (
unsigned f = 0;
f <
mesh->mNumFaces; ++
f) {
239 const aiFace &face =
mesh->mFaces[
f];
240 const unsigned i0 = face.mIndices[0];
241 const unsigned i1 = face.mIndices[1];
242 const unsigned i2 = face.mIndices[2];
249 const aiVector3D &t0 =
mesh->mTextureCoords[uvChannel][
i0];
250 const aiVector3D &t1 =
mesh->mTextureCoords[uvChannel][
i1];
251 const aiVector3D &t2 =
mesh->mTextureCoords[uvChannel][
i2];
252 const Vec2 uv0{t0.x, t0.y};
253 const Vec2 uv1{t1.x, t1.y};
254 const Vec2 uv2{t2.x, t2.y};
255 const Vec2 pix0{uv0.x * widthF, (1.f - uv0.y) * heightF};
256 const Vec2 pix1{uv1.x * widthF, (1.f - uv1.y) * heightF};
257 const Vec2 pix2{uv2.x * widthF, (1.f - uv2.y) * heightF};
259 const int minX = std::max(0,
static_cast<int>(std::floor(std::min({pix0.x, pix1.x, pix2.x}))));
261 std::min(
width - 1,
static_cast<int>(std::ceil(std::max({pix0.x, pix1.x, pix2.x}))));
262 const int minY = std::max(0,
static_cast<int>(std::floor(std::min({pix0.y, pix1.y, pix2.y}))));
264 std::min(
height - 1,
static_cast<int>(std::ceil(std::max({pix0.y, pix1.y, pix2.y}))));
266 for (
int y = minY;
y <= maxY; ++
y) {
267 for (
int x = minX;
x <= maxX; ++
x) {
268 const Vec2 sample{
static_cast<float>(
x) + 0.5f,
static_cast<float>(
y) + 0.5f};
269 float wa = 0.f, wb = 0.f, wc = 0.f;
270 if (!barycentric2(sample, pix0, pix1, pix2, &wa, &wb, &wc))
272 constexpr float kEps = -1e-4f;
273 if (wa < kEps || wb < kEps || wc < kEps)
275 Vec3
object = normalize3(add(add(
scale(n0, wa),
scale(n1, wb)),
scale(n2, wc)));
277 tangentSpace ? toTangent(
object, p0, p1, p2, uv0, uv1, uv2) :
object;
278 image->setPixel(
x,
y, encodeNormal(encoded));
Stable, structured diagnostics shared by engine modules.
std::array< float, 3 > scale
Move-only, checked operation results for the common layer.
A structured explanation of a failed, degraded, or noteworthy result.
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 Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
medialoader::Colorf Colorf
eve::Result< void > applyVertexNormalsFrom(int meshIndex, std::string_view kind, float originX, float originY, float originZ)
Like applyVertexNormals, but "radial" uses an explicit origin.
eve::Result< void > applyVertexNormals(int meshIndex, std::string_view kind)
Replace every vertex normal on a mesh from a named procedure.
eve::Result< void > setVertexNormal(int meshIndex, int vertexIndex, float x, float y, float z)
Write one unit (or zero) object-space vertex normal. Allocates the normal stream when the mesh has po...
eve::Result< std::unique_ptr< image::ImageData > > bakeNormalMap(int meshIndex, int width, int height, int uvChannel=0, std::string_view space="tangent") const
Rasterize current object-space vertex normals into an RGBA8 normal map.
Build metadata (engine git commit, build time, third-party version).