Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 46 additions & 24 deletions core/splashpack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,38 @@ std::vector<Vec3> 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<Color> meshColors(const FlatObject& fo, const MeshComponent& mc, const Mesh& m,
const std::vector<SceneLight>& lights) {
std::vector<Color> out(m.positions.size());
bool hasColors = mc.vertexColors == VertexColorMode::Mesh && m.colors.size() == m.positions.size();
std::vector<Vec3> 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);
Expand Down Expand Up @@ -201,13 +233,7 @@ void buildTris(ExpObject& e, float gte, const std::vector<SceneLight>& lights, E
res.errors.push_back(e.obj->name + ": mesh component has no materials");
return;
}
std::vector<Vec3> lightNormals = mc.smoothNormals ? smoothNormals(m) : m.normals;
bool hasColors = mc.vertexColors == VertexColorMode::Mesh && m.colors.size() == m.positions.size();
std::vector<Vec3> 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<Color> colors = meshColors(fo, mc, m, lights);
std::vector<Vec2> uvs = m.uv;
if (uvs.empty()) uvs.assign(m.positions.size(), {});

Expand All @@ -224,21 +250,7 @@ void buildTris(ExpObject& e, float gte, const std::vector<SceneLight>& 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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1892,4 +1903,15 @@ ExportResult exportSplashpack(const Scene& scene, const fs::path& root, const fs
return res;
}

std::vector<Vec3> exportVertexColors(const std::vector<FlatObject>& flat, const FlatObject& object,
const Mesh& mesh) {
std::vector<Vec3> out;
if (!object.object->mesh || mesh.normals.size() != mesh.positions.size()) return out;
std::vector<SceneLight> 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
8 changes: 8 additions & 0 deletions core/splashpack.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec3> exportVertexColors(const std::vector<FlatObject>& flat, const FlatObject& object,
const Mesh& mesh);

} // namespace splash
78 changes: 67 additions & 11 deletions examples/courtyard/courtyard.scene
Original file line number Diff line number Diff line change
Expand Up @@ -574,32 +574,88 @@
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,
1.0,
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",
Expand Down
64 changes: 7 additions & 57 deletions src/viewport/ps1view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "editor/pick.hh"
#include "gl.h"
#include "scene.hh"
#include "splashpack.hh"
#include "unitymath.hh"

namespace viewport {
Expand Down Expand Up @@ -204,32 +205,6 @@ static void makeFallback(std::vector<uint8_t>& 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<PointLight>& 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);
Expand Down Expand Up @@ -267,30 +242,6 @@ void Ps1View::rebuild() {
const splash::Scene& scene = m_doc->scene();
std::vector<splash::FlatObject> flats = splash::flatten(scene);

// Gather lights, and work out the default directional when there is none.
std::vector<PointLight> 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<unsigned, std::vector<Vertex>> buckets;
std::map<std::string, splash::Mesh>& meshCache = m_meshCache;
Expand Down Expand Up @@ -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<splash::Vec3> 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<splash::Vec3> 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<splash::Vec3> 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)];
Expand All @@ -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});
Expand Down
Loading