diff --git a/core/splashpack.cpp b/core/splashpack.cpp index 0fb8a79..f197c6c 100644 --- a/core/splashpack.cpp +++ b/core/splashpack.cpp @@ -171,6 +171,38 @@ std::vector smoothNormals(const Mesh& m) { return out; } +SceneLight sceneLight(const FlatObject& fo) { + const LightComponent& l = *fo.object->light; + return {l.kind, fo.worldPosition, rotate(fo.worldRotation, {0, 0, 1}), l.color[0], l.color[1], l.color[2], + l.intensity, l.spotAngle, l.innerSpotAngle}; +} + +// World-space positions and lighting normals, then one colour per vertex as +// the mesh component's colour mode picks it. +std::vector meshColors(const FlatObject& fo, const MeshComponent& mc, const Mesh& m, + const std::vector& lights) { + std::vector out(m.positions.size()); + bool hasColors = mc.vertexColors == VertexColorMode::Mesh && m.colors.size() == m.positions.size(); + std::vector lightNormals; + if (mc.vertexColors != VertexColorMode::Flat && mc.vertexColors != VertexColorMode::Mesh) + lightNormals = mc.smoothNormals ? smoothNormals(m) : m.normals; + for (size_t i = 0; i < m.positions.size(); i++) { + switch (mc.vertexColors) { + case VertexColorMode::Flat: + out[i] = {mc.flatColor[0] / 255.f, mc.flatColor[1] / 255.f, mc.flatColor[2] / 255.f}; + break; + case VertexColorMode::Mesh: + out[i] = hasColors ? Color{m.colors[i][0], m.colors[i][1], m.colors[i][2]} : Color{0.5f, 0.5f, 0.5f}; + break; + default: + out[i] = bake(fo.localToWorld.point(m.positions[i]), + normalized(rotate(fo.worldRotation, lightNormals[i])), lights); + break; + } + } + return out; +} + PsxVert toPsxVertex(Vec3 vertex, float gte, Vec3 normal, Vec2 uv, int width, int height, Color c) { PsxVert p; p.vx = toPsxCoord(vertex.x, gte); @@ -201,13 +233,7 @@ void buildTris(ExpObject& e, float gte, const std::vector& lights, E res.errors.push_back(e.obj->name + ": mesh component has no materials"); return; } - std::vector lightNormals = mc.smoothNormals ? smoothNormals(m) : m.normals; - bool hasColors = mc.vertexColors == VertexColorMode::Mesh && m.colors.size() == m.positions.size(); - std::vector wv(m.positions.size()), wn(m.positions.size()); - for (size_t i = 0; i < m.positions.size(); i++) { - wv[i] = fo.localToWorld.point(m.positions[i]); - wn[i] = normalized(rotate(fo.worldRotation, lightNormals[i])); - } + const std::vector colors = meshColors(fo, mc, m, lights); std::vector uvs = m.uv; if (uvs.empty()) uvs.assign(m.positions.size(), {}); @@ -224,21 +250,7 @@ void buildTris(ExpObject& e, float gte, const std::vector& lights, E auto convert = [&](int idx) { size_t i = size_t(idx); Vec3 v = scale(m.positions[i], fo.lossyScale); - Color c; - switch (mc.vertexColors) { - case VertexColorMode::Flat: - c = {mc.flatColor[0] / 255.f, mc.flatColor[1] / 255.f, mc.flatColor[2] / 255.f}; - break; - case VertexColorMode::Mesh: - if (hasColors) - c = {m.colors[i][0], m.colors[i][1], m.colors[i][2]}; - else - c = {0.5f, 0.5f, 0.5f}; - break; - default: - c = bake(wv[i], wn[i], lights); - break; - } + Color c = colors[i]; if (texIndex == -1) { c = {c.r * mat.color[0], c.g * mat.color[1], c.b * mat.color[2]}; return toPsxVertex(v, gte, m.normals[i], {}, 0, 0, c); @@ -1009,8 +1021,7 @@ ExportResult exportSplashpack(const Scene& scene, const fs::path& root, const fs // Disabled runtime lights are still exported so Lua can switch them on. if (l.runtime && fo.activeInHierarchy) runtimeLights.push_back({&fo, &l}); if (!l.enabled) continue; - SceneLight sl{l.kind, fo.worldPosition, rotate(fo.worldRotation, {0, 0, 1}), l.color[0], l.color[1], - l.color[2], l.intensity, l.spotAngle, l.innerSpotAngle}; + SceneLight sl = sceneLight(fo); lights.push_back(sl); if (!l.runtime) bakedOnlyLights.push_back(sl); } @@ -1892,4 +1903,15 @@ ExportResult exportSplashpack(const Scene& scene, const fs::path& root, const fs return res; } +std::vector exportVertexColors(const std::vector& flat, const FlatObject& object, + const Mesh& mesh) { + std::vector out; + if (!object.object->mesh || mesh.normals.size() != mesh.positions.size()) return out; + std::vector lights; + for (const FlatObject& fo : flat) + if (fo.object->light && fo.object->light->enabled) lights.push_back(sceneLight(fo)); + for (const Color& c : meshColors(object, *object.object->mesh, mesh, lights)) out.push_back({c.r, c.g, c.b}); + return out; +} + } // namespace splash diff --git a/core/splashpack.hh b/core/splashpack.hh index 526ce4b..71ac96e 100644 --- a/core/splashpack.hh +++ b/core/splashpack.hh @@ -65,4 +65,12 @@ struct ExportResult { ExportResult exportSplashpack(const Scene& scene, const std::filesystem::path& projectRoot, const std::filesystem::path& out, const ExportOptions& options = {}); +// The vertex colours the exporter writes for `object`'s mesh, in mesh vertex +// order, 0..1 per channel (the shipped byte is the value * 255, truncated). +// Material colour on untextured submeshes is not included. Baked meshes get +// every enabled light; on a runtime-lit mesh the console adds the runtime +// lights itself, which this approximates by baking them too. +std::vector exportVertexColors(const std::vector& flat, const FlatObject& object, + const Mesh& mesh); + } // namespace splash diff --git a/examples/courtyard/courtyard.scene b/examples/courtyard/courtyard.scene index 7622ad8..4978c8a 100644 --- a/examples/courtyard/courtyard.scene +++ b/examples/courtyard/courtyard.scene @@ -574,23 +574,27 @@ 1.0 ] }, - "components": [], + "components": [ + { + "type": "player" + } + ], "children": [] }, { - "name": "Main Camera", + "name": "Sun", "active": true, "transform": { "position": [ - 7.6, - 5.2, - -9.2 + 0.0, + 6.0, + 0.0 ], "rotation": [ - 0.14017116, - -0.31381935, - 0.046900615, - 0.93790716 + 0.40821789, + -0.23456972, + 0.10938165, + 0.8754261 ], "scale": [ 1.0, @@ -598,8 +602,60 @@ 1.0 ] }, - "components": [], - "children": [] + "components": [ + { + "type": "light", + "kind": "directional", + "color": [ + 1.0, + 0.95, + 0.85 + ], + "intensity": 0.75, + "range": 10.0, + "spotAngle": 30.0, + "innerSpotAngle": 0.0, + "enabled": true + } + ] + }, + { + "name": "Fill", + "active": true, + "transform": { + "position": [ + 0.0, + 6.0, + 0.0 + ], + "rotation": [ + 0.07782839, + 0.92121983, + -0.2904595, + 0.24684011 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + }, + "components": [ + { + "type": "light", + "kind": "directional", + "color": [ + 0.6, + 0.65, + 0.85 + ], + "intensity": 0.35, + "range": 10.0, + "spotAngle": 30.0, + "innerSpotAngle": 0.0, + "enabled": true + } + ] }, { "name": "Torch Light", diff --git a/src/viewport/ps1view.cpp b/src/viewport/ps1view.cpp index bbde870..816dabf 100644 --- a/src/viewport/ps1view.cpp +++ b/src/viewport/ps1view.cpp @@ -11,6 +11,7 @@ #include "editor/pick.hh" #include "gl.h" #include "scene.hh" +#include "splashpack.hh" #include "unitymath.hh" namespace viewport { @@ -204,32 +205,6 @@ static void makeFallback(std::vector& px) { } } -// A point light as the lighting pass consumes it, in Unity world space. -struct PointLight { - splash::Vec3 pos; - Vec3 color; // colour * intensity - float range; -}; - -// Per-vertex lighting in Unity world space, matching the exporter's look: -// ambient + one directional + attenuated point lights. The default -// directional (used when the scene has none) reproduces the old torch scene's -// sun. Returns colour scaled so the shader's `* 2.0` keeps the PS1 brightness. -static Vec3 shade(splash::Vec3 p, splash::Vec3 n, const std::vector& lights, Vec3 ambient, Vec3 sunDir, - Vec3 sunColor) { - Vec3 nn{n.x, n.y, n.z}; - Vec3 c = ambient; - c = c + sunColor * std::max(0.0f, dot(nn, sunDir)); - for (const PointLight& pl : lights) { - Vec3 l{pl.pos.x - p.x, pl.pos.y - p.y, pl.pos.z - p.z}; - float dist = std::sqrt(dot(l, l)); - float atten = std::max(0.0f, 1.0f - dist / pl.range); - float nd = std::max(0.0f, dot(nn, normalize(l))); - c = c + pl.color * (atten * atten * (0.3f + 0.7f * nd)); - } - return {std::min(c.x, 1.0f) * 0.62f, std::min(c.y, 1.0f) * 0.62f, std::min(c.z, 1.0f) * 0.62f}; -} - unsigned Ps1View::textureFor(const std::string& projectPath) { if (projectPath.empty()) return m_white; auto it = m_texCache.find(projectPath); @@ -267,30 +242,6 @@ void Ps1View::rebuild() { const splash::Scene& scene = m_doc->scene(); std::vector flats = splash::flatten(scene); - // Gather lights, and work out the default directional when there is none. - std::vector points; - bool haveDirectional = false; - Vec3 sunDir = normalize({0.45f, 1.0f, -0.35f}); // Unity-space twin of the old GL sun {0.45,1,0.35} - Vec3 sunColor{0.46f, 0.44f, 0.46f}; - const Vec3 ambient{0.34f, 0.35f, 0.44f}; - for (const splash::FlatObject& fo : flats) { - if (!fo.activeInHierarchy || !fo.object->light) continue; - const splash::LightComponent& lc = *fo.object->light; - if (!lc.enabled) continue; - Vec3 col{lc.color[0] * lc.intensity, lc.color[1] * lc.intensity, lc.color[2] * lc.intensity}; - if (lc.kind == splash::LightKind::Directional) { - if (!haveDirectional) { - // Unity forward is +Z; the direction towards the light is -forward. - splash::Vec3 fwd = splash::rotate(fo.worldRotation, {0, 0, 1}); - sunDir = normalize({-fwd.x, -fwd.y, -fwd.z}); - sunColor = col; - haveDirectional = true; - } - } else if (lc.kind == splash::LightKind::Point) { - points.push_back({fo.localToWorld.position(), col, lc.range > 0 ? lc.range : 1.0f}); - } - } - // Collect geometry into per-texture buckets. std::map> buckets; std::map& meshCache = m_meshCache; @@ -328,13 +279,11 @@ void Ps1View::rebuild() { } if (mesh->positions.empty() || mesh->normals.size() != mesh->positions.size()) continue; - // World positions and normals (Unity space); render space negates Z. - std::vector wp(mesh->positions.size()), wn(mesh->positions.size()); - for (size_t i = 0; i < mesh->positions.size(); ++i) { - wp[i] = fo.localToWorld.point(mesh->positions[i]); - wn[i] = splash::normalized(splash::rotate(fo.worldRotation, mesh->normals[i])); - } + // World positions (Unity space); render space negates Z. + std::vector wp(mesh->positions.size()); + for (size_t i = 0; i < mesh->positions.size(); ++i) wp[i] = fo.localToWorld.point(mesh->positions[i]); const bool haveUv = mesh->uv.size() == mesh->positions.size(); + const std::vector colors = splash::exportVertexColors(flats, fo, *mesh); for (size_t sub = 0; sub < mesh->submeshes.size(); ++sub) { const splash::Material& mat = mc.materials[std::min(sub, mc.materials.size() - 1)]; @@ -349,7 +298,8 @@ void Ps1View::rebuild() { m_pick.ranges.push_back({(int)fi, firstTri, m_pick.positions.size() / 3 - firstTri}); for (int idx : tri) { size_t i = (size_t)idx; - Vec3 col = shade(wp[i], wn[i], points, ambient, sunDir, sunColor); + // The exporter's colour, so this view lights the scene the way the game does. + Vec3 col = i < colors.size() ? Vec3{colors[i].x, colors[i].y, colors[i].z} : Vec3{0.5f, 0.5f, 0.5f}; if (!textured) col = {col.x * mat.color[0], col.y * mat.color[1], col.z * mat.color[2]}; splash::Vec2 uv = haveUv ? mesh->uv[i] : splash::Vec2{}; bucket.push_back({wp[i].x, wp[i].y, -wp[i].z, uv.x, uv.y, col.x, col.y, col.z});