From 4473beb1d4d5be581729c3d6fb5d09c94969fb13 Mon Sep 17 00:00:00 2001 From: Rix Date: Wed, 7 Oct 2026 05:19:26 -0700 Subject: [PATCH] Scene view: light meshes with the exporter's vertex colours The viewport had its own lighting (a fixed ambient, a default sun, linear falloff) that the exporter never ships, so the Scene view looked lit while the game drew everything the torch misses black. It now draws the colours the exporter bakes. Export output is unchanged. Courtyard relied on the invented light and on two empty objects: give it a Sun and a Fill light, put a Player component on Player Start, and drop the empty Main Camera. --- core/splashpack.cpp | 70 ++++++++++++++++++--------- core/splashpack.hh | 8 +++ examples/courtyard/courtyard.scene | 78 +++++++++++++++++++++++++----- src/viewport/ps1view.cpp | 64 +++--------------------- 4 files changed, 128 insertions(+), 92 deletions(-) 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});