mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-29 04:02:56 +01:00
v0.2.0: splats, copy/paste, faster homes, seat facing, and a full bug sweep
Frame Studio - Gaussian splats (PLY, SPZ v2-v4), PLY vertex colours, panorama skies - Copy, cut, paste and duplicate for objects, splats and world items - Walkable-area rebake moved off the UI thread; viewport one frame sooner; refresh-locked pacing (FS_PACE=timer for the old one) - Fixed: bundle thumbnail use-after-free crash, auto-save deleting environments, saves into scratch copies, edits lost on non-ASCII paths, unopenable manifests with inf/nan, bundle save data loss, opening a home marking it changed, gizmo NaN, long paths, many malformed-file crashes - Updater verifies sha256, journals the swap and never leaves the exe missing Frame Home - Seats turn you to face their way - Draw batching (Hand Tutorials up to ~20% less GPU time), cut-out EQUAL pass, late head-pose read before submit - Proper queue-family hand-over to the compositor layer; layer descriptor generations and seqlocked slots; structural SPIR-V check - Fixed: hangs/crashes on damaged .fenv files and shaders, render-pass validation error, sync hazard in the foveation smoothing pass, unverified update downloads, half-done installs Tests: tests_loco/env_share/spv_check wired into CMake (FRAME_TESTS) and CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsaXGU27gsrXjDoFtyFJP9
This commit is contained in:
1 parent
d8e5d9e71e
commit
6d0b4fcd2f
544 files changed
+210660
-796
No files matched your search
+373
-19
@@ -6,13 +6,28 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
extern "C" unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp, int req_comp);
|
||||
extern "C" float* stbi_loadf_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp, int req_comp);
|
||||
extern "C" void stbi_image_free(void* retval_from_stbi_load);
|
||||
extern "C" int stbi_info_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp);
|
||||
|
||||
// At most this many pixels decoded (16384 x 8192, a 16K panorama): a small damaged file can claim a picture of
|
||||
// gigabytes. (stb itself also refuses a side over 16384: stb_image_impl.c.)
|
||||
constexpr unsigned long long kMaxPixels = 16384ull * 8192ull;
|
||||
static bool sizeOk(const unsigned char* data, size_t n) {
|
||||
int w = 0, h = 0, c = 0;
|
||||
return data && n > 0 && n <= 0x7fffffff && stbi_info_from_memory(data, static_cast<int>(n), &w, &h, &c) && w > 0 && h > 0 &&
|
||||
static_cast<unsigned long long>(w) * static_cast<unsigned long long>(h) <= kMaxPixels;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -28,10 +43,41 @@ std::vector<u8> readFile(const std::string& path) {
|
||||
return std::vector<u8>((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// cgltf's file reads, by the UTF-8 path as a wide name on Windows (its own fopen takes the ANSI code page, so a
|
||||
// model under C:\Users\Zoë\ wasn't found).
|
||||
cgltf_result gltfRead(const cgltf_memory_options*, const cgltf_file_options*, const char* path, cgltf_size* size, void** data) {
|
||||
std::ifstream f(fs::u8path(path), std::ios::binary);
|
||||
if (!f) return cgltf_result_file_not_found;
|
||||
std::vector<char> bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (size && *size) { // a buffer's declared length: as cgltf's own, short is an error
|
||||
if (bytes.size() < *size) return cgltf_result_io_error;
|
||||
bytes.resize(*size);
|
||||
}
|
||||
void* out = std::malloc(std::max<size_t>(bytes.size(), 1));
|
||||
if (!out) return cgltf_result_out_of_memory;
|
||||
if (!bytes.empty()) std::memcpy(out, bytes.data(), bytes.size());
|
||||
if (size) *size = bytes.size();
|
||||
*data = out;
|
||||
return cgltf_result_success;
|
||||
}
|
||||
void gltfRelease(const cgltf_memory_options*, const cgltf_file_options*, void* data) { std::free(data); }
|
||||
|
||||
// An accessor's floats, `n` a vertex, sparse substitutions applied (cgltf_accessor_read_float leaves sparse
|
||||
// accessors as zeros). `comps` floats a vertex; vertices past the accessor's own count stay `fill`.
|
||||
std::vector<float> unpackFloats(const cgltf_accessor* a, size_t vertices, size_t comps, float fill) {
|
||||
std::vector<float> out(vertices * comps, fill);
|
||||
if (!a || cgltf_num_components(a->type) != comps) return out;
|
||||
std::vector<float> all(a->count * comps);
|
||||
if (cgltf_accessor_unpack_floats(a, all.data(), all.size()) != all.size()) return out;
|
||||
std::copy_n(all.begin(), std::min(all.size(), out.size()), out.begin());
|
||||
return out;
|
||||
}
|
||||
|
||||
struct Image { std::vector<u8> rgba; u32 w = 0, h = 0; bool alpha = false; };
|
||||
|
||||
bool decode(const u8* data, size_t n, Image& img) {
|
||||
int w = 0, h = 0, comp = 0;
|
||||
if (!sizeOk(data, n)) return false;
|
||||
unsigned char* px = stbi_load_from_memory(data, static_cast<int>(n), &w, &h, &comp, 4);
|
||||
if (!px) return false;
|
||||
img.w = static_cast<u32>(w);
|
||||
@@ -64,6 +110,8 @@ void finish(MeshData& md, const Image* img, const float color[4], bool blend, bo
|
||||
|
||||
bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
cgltf_options opt = {};
|
||||
opt.file.read = gltfRead;
|
||||
opt.file.release = gltfRelease;
|
||||
cgltf_data* data = nullptr;
|
||||
if (cgltf_parse_file(&opt, path.c_str(), &data) != cgltf_result_success) { error = "not a readable glTF file: " + path; return false; }
|
||||
if (cgltf_load_buffers(&opt, data, path.c_str()) != cgltf_result_success) { cgltf_free(data); error = "glTF buffers missing next to " + path; return false; }
|
||||
@@ -72,7 +120,12 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
||||
files.push_back(path);
|
||||
const fs::path dir = fs::u8path(path).parent_path();
|
||||
for (cgltf_size b = 0; b < data->buffers_count; ++b)
|
||||
if (data->buffers[b].uri && std::strncmp(data->buffers[b].uri, "data:", 5) != 0) files.push_back((dir / fs::u8path(data->buffers[b].uri)).u8string());
|
||||
if (data->buffers[b].uri && std::strncmp(data->buffers[b].uri, "data:", 5) != 0) {
|
||||
std::string rel = data->buffers[b].uri; // (as written in the file: "my%20model.bin" is "my model.bin")
|
||||
cgltf_decode_uri(rel.data());
|
||||
rel.resize(std::strlen(rel.c_str()));
|
||||
files.push_back((dir / fs::u8path(rel)).u8string());
|
||||
}
|
||||
std::map<const cgltf_image*, Image> images;
|
||||
auto image = [&](const cgltf_texture_view& view) -> const Image* {
|
||||
if (!view.texture || !view.texture->image) return nullptr;
|
||||
@@ -88,7 +141,11 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
||||
} else if (im->uri && std::strncmp(im->uri, "data:", 5) == 0) {
|
||||
const char* comma = std::strchr(im->uri, ',');
|
||||
if (comma) {
|
||||
const size_t len = std::strlen(comma + 1), size = len / 4 * 3;
|
||||
// (the '=' padding isn't data: cgltf's decoder fails on it, so the length leaves it out)
|
||||
const size_t len = std::strlen(comma + 1);
|
||||
size_t pad = 0;
|
||||
while (pad < 2 && pad < len && comma[len - pad] == '=') ++pad;
|
||||
const size_t size = len / 4 * 3 - std::min(pad, len / 4 * 3);
|
||||
void* bytes = nullptr;
|
||||
if (cgltf_load_buffer_base64(&opt, size, comma + 1, &bytes) == cgltf_result_success) {
|
||||
decode(static_cast<const u8*>(bytes), size, out);
|
||||
@@ -125,19 +182,15 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
||||
md.name = std::string(node.name ? node.name : node.mesh->name ? node.mesh->name : "mesh") + (node.mesh->primitives_count > 1 ? "_" + std::to_string(p) : "");
|
||||
for (const cgltf_node* a = node.parent; a; a = a->parent) // the nodes above it in the file
|
||||
if (a->name) md.path = md.path.empty() ? std::string(a->name) : std::string(a->name) + "/" + md.path;
|
||||
md.positions.resize(pos->count * 3);
|
||||
md.uvs.assign(pos->count * 2, 0.0f);
|
||||
for (cgltf_size v = 0; v < pos->count; ++v) {
|
||||
cgltf_accessor_read_float(pos, v, &md.positions[v * 3], 3);
|
||||
if (uv) cgltf_accessor_read_float(uv, v, &md.uvs[v * 2], 2);
|
||||
}
|
||||
md.positions = unpackFloats(pos, pos->count, 3, 0.0f);
|
||||
md.uvs = unpackFloats(uv, pos->count, 2, 0.0f);
|
||||
if (col) {
|
||||
const size_t nc = cgltf_num_components(col->type);
|
||||
const std::vector<float> c = unpackFloats(col, pos->count, nc, 1.0f);
|
||||
md.colors.resize(pos->count * 4);
|
||||
for (cgltf_size v = 0; v < pos->count; ++v) {
|
||||
float c[4] = {1, 1, 1, 1};
|
||||
cgltf_accessor_read_float(col, v, c, cgltf_num_components(col->type));
|
||||
for (int k = 0; k < 4; ++k) md.colors[v * 4 + k] = static_cast<u8>(std::clamp(c[k], 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
}
|
||||
for (cgltf_size v = 0; v < pos->count; ++v)
|
||||
for (size_t k = 0; k < 4; ++k)
|
||||
md.colors[v * 4 + k] = static_cast<u8>(std::clamp(k < nc ? c[v * nc + k] : 1.0f, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
}
|
||||
if (prim.indices) {
|
||||
md.indices.resize(prim.indices->count);
|
||||
@@ -148,6 +201,8 @@ bool importGltf(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
||||
md.indices.resize(pos->count);
|
||||
for (cgltf_size i = 0; i < pos->count; ++i) md.indices[i] = static_cast<u32>(i);
|
||||
}
|
||||
md.indices.resize(md.indices.size() / 3 * 3); // only whole triangles (a damaged file's count may not be)
|
||||
if (md.indices.empty()) continue;
|
||||
std::memcpy(md.worldMatrix, world, sizeof world);
|
||||
md.hasWorldMatrix = true;
|
||||
float color[4] = {1, 1, 1, 1};
|
||||
@@ -183,6 +238,7 @@ bool importUfbx(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
||||
opts.target_unit_meters = 1.0f;
|
||||
opts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY;
|
||||
opts.load_external_files = true;
|
||||
opts.ignore_missing_external_files = true; // (an OBJ whose .mtl is missing opens, untextured, instead of not at all)
|
||||
ufbx_error err;
|
||||
ufbx_scene* scene = ufbx_load_file(path.c_str(), &opts, &err);
|
||||
if (!scene) {
|
||||
@@ -279,17 +335,315 @@ bool importUfbx(const std::string& path, std::vector<MeshData>& meshes, std::vec
|
||||
return true;
|
||||
}
|
||||
|
||||
// One mesh of triangles (3 positions each, already Y-up) drawn flat white, or in its vertex colours when it has them.
|
||||
void finishPlain(MeshData& md, const std::string& path) {
|
||||
md.name = fs::u8path(path).stem().u8string();
|
||||
md.uvs.resize(md.positions.size() / 3 * 2, 0.0f);
|
||||
md.indices.resize(md.positions.size() / 3);
|
||||
for (size_t i = 0; i < md.indices.size(); ++i) md.indices[i] = static_cast<u32>(i);
|
||||
const float white[4] = {1, 1, 1, 1};
|
||||
finish(md, nullptr, white, false, false, 0.5f);
|
||||
}
|
||||
|
||||
// STL (3D printing, CAD): binary (80-byte header, a count, 50 bytes a triangle) or ASCII ("solid ... facet ...
|
||||
// vertex x y z"). Its axes are Z-up, as CAD's are: turned to Y-up. No units are recorded; taken as metres (the
|
||||
// Scale field fixes a millimetre file: 0.001).
|
||||
bool importStl(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
const std::vector<u8> b = readFile(path);
|
||||
if (b.size() < 15) { error = "not an STL file: " + path; return false; }
|
||||
MeshData md;
|
||||
auto vertex = [&](float x, float y, float z) { md.positions.insert(md.positions.end(), {x, z, -y}); };
|
||||
uint32_t count = 0;
|
||||
if (b.size() >= 84) std::memcpy(&count, &b[80], 4);
|
||||
if (b.size() >= 84 && static_cast<uint64_t>(count) * 50 + 84 == b.size()) { // binary: the size says so exactly
|
||||
md.positions.reserve(static_cast<size_t>(count) * 9);
|
||||
for (uint32_t t = 0; t < count; ++t) {
|
||||
float v[9];
|
||||
std::memcpy(v, &b[84 + static_cast<size_t>(t) * 50 + 12], sizeof v); // (after the facet normal)
|
||||
for (int k = 0; k < 3; ++k) vertex(v[k * 3], v[k * 3 + 1], v[k * 3 + 2]);
|
||||
}
|
||||
} else {
|
||||
std::istringstream in(std::string(b.begin(), b.end()));
|
||||
std::string word;
|
||||
while (in >> word) {
|
||||
if (word != "vertex") continue;
|
||||
float x = 0, y = 0, z = 0;
|
||||
if (!(in >> x >> y >> z)) { error = "damaged STL file: " + path; return false; }
|
||||
vertex(x, y, z);
|
||||
}
|
||||
md.positions.resize(md.positions.size() / 9 * 9); // only whole triangles
|
||||
}
|
||||
for (float v : md.positions) if (!std::isfinite(v)) { error = "damaged STL file (bad numbers): " + path; return false; }
|
||||
if (md.positions.empty()) { error = "no triangles in " + path; return false; }
|
||||
finishPlain(md, path);
|
||||
files.push_back(path);
|
||||
meshes.push_back(std::move(md));
|
||||
return true;
|
||||
}
|
||||
|
||||
// PLY (scanners, photogrammetry, MeshLab): ASCII or binary, any element layout. The vertices' x y z, colours
|
||||
// (red green blue [alpha], 0..255 or 0..1) and texture coordinates (s t / u v / texture_u texture_v) when present, and
|
||||
// the faces' index lists, fanned into triangles. Axes as stored (scanners mostly write Y-up). A PLY with Gaussian
|
||||
// splat properties, or only points, isn't a mesh: said so.
|
||||
bool importPly(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
const std::vector<u8> b = readFile(path);
|
||||
const std::string bad = "damaged or unsupported PLY file: " + path;
|
||||
struct Prop { std::string name, type, countType; bool list = false; };
|
||||
struct Element { std::string name; uint64_t count = 0; std::vector<Prop> props; };
|
||||
std::vector<Element> els;
|
||||
int format = -1; // 0 ascii, 1 binary little-endian, 2 binary big-endian
|
||||
size_t pos = 0;
|
||||
{ // the header: text lines up to "end_header"
|
||||
const std::string head(b.begin(), b.begin() + std::min<size_t>(b.size(), 1 << 16));
|
||||
const size_t end = head.find("end_header");
|
||||
if (head.rfind("ply", 0) != 0 || end == std::string::npos) { error = "not a PLY file: " + path; return false; }
|
||||
pos = head.find('\n', end);
|
||||
if (pos == std::string::npos) { error = bad; return false; }
|
||||
++pos;
|
||||
std::istringstream in(head.substr(0, end));
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
std::istringstream ls(line);
|
||||
std::string w;
|
||||
ls >> w;
|
||||
if (w == "format") {
|
||||
std::string f;
|
||||
ls >> f;
|
||||
format = f == "ascii" ? 0 : f == "binary_little_endian" ? 1 : f == "binary_big_endian" ? 2 : -1;
|
||||
} else if (w == "element") {
|
||||
Element e;
|
||||
ls >> e.name >> e.count;
|
||||
els.push_back(e);
|
||||
} else if (w == "property" && !els.empty()) {
|
||||
Prop p;
|
||||
ls >> p.type;
|
||||
if (p.type == "list") { p.list = true; ls >> p.countType >> p.type; }
|
||||
ls >> p.name;
|
||||
els.back().props.push_back(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (format < 0) { error = bad; return false; }
|
||||
for (const Element& e : els)
|
||||
for (const Prop& p : e.props)
|
||||
if (e.name == "vertex" && (p.name == "f_dc_0" || p.name == "scale_0")) {
|
||||
error = "this PLY holds Gaussian splats, which Frame Studio can't open yet: " + path;
|
||||
return false;
|
||||
}
|
||||
auto size = [](const std::string& t) -> int {
|
||||
if (t == "char" || t == "uchar" || t == "int8" || t == "uint8") return 1;
|
||||
if (t == "short" || t == "ushort" || t == "int16" || t == "uint16") return 2;
|
||||
if (t == "int" || t == "uint" || t == "int32" || t == "uint32" || t == "float" || t == "float32") return 4;
|
||||
if (t == "double" || t == "float64") return 8;
|
||||
return 0;
|
||||
};
|
||||
std::istringstream ascii(format == 0 ? std::string(b.begin() + pos, b.end()) : std::string());
|
||||
bool ok = true;
|
||||
auto read = [&](const std::string& t) -> double { // one value of type t, at pos (binary) or the next word (ASCII)
|
||||
if (format == 0) { double v = 0; if (!(ascii >> v)) ok = false; return v; }
|
||||
const int n = size(t);
|
||||
if (n == 0 || pos + n > b.size()) { ok = false; return 0; }
|
||||
u8 raw[8];
|
||||
std::memcpy(raw, &b[pos], n);
|
||||
pos += n;
|
||||
if (format == 2) std::reverse(raw, raw + n);
|
||||
if (t == "char" || t == "int8") return static_cast<int8_t>(raw[0]);
|
||||
if (t == "uchar" || t == "uint8") return raw[0];
|
||||
if (t == "short" || t == "int16") { int16_t v; std::memcpy(&v, raw, 2); return v; }
|
||||
if (t == "ushort" || t == "uint16") { uint16_t v; std::memcpy(&v, raw, 2); return v; }
|
||||
if (t == "int" || t == "int32") { int32_t v; std::memcpy(&v, raw, 4); return v; }
|
||||
if (t == "uint" || t == "uint32") { uint32_t v; std::memcpy(&v, raw, 4); return v; }
|
||||
if (t == "float" || t == "float32") { float v; std::memcpy(&v, raw, 4); return v; }
|
||||
double v; std::memcpy(&v, raw, 8); return v;
|
||||
};
|
||||
std::vector<float> xyz, uv;
|
||||
std::vector<u8> rgba;
|
||||
std::vector<u32> tris;
|
||||
bool colours = false, coords = false;
|
||||
for (const Element& e : els) {
|
||||
// a hostile count: never more items than bytes left (each takes at least one)
|
||||
if (e.count > b.size()) { error = bad; return false; }
|
||||
const bool vert = e.name == "vertex", face = e.name == "face";
|
||||
if (vert) {
|
||||
xyz.reserve(e.count * 3);
|
||||
for (const Prop& p : e.props) {
|
||||
colours |= p.name == "red";
|
||||
coords |= p.name == "s" || p.name == "u" || p.name == "texture_u";
|
||||
}
|
||||
}
|
||||
for (uint64_t i = 0; i < e.count && ok; ++i) {
|
||||
float v[3] = {0, 0, 0}, t[2] = {0, 0}, c[4] = {1, 1, 1, 1};
|
||||
for (const Prop& p : e.props) {
|
||||
if (p.list) {
|
||||
const double n = read(p.countType);
|
||||
if (!ok || n < 0 || n > 1024) { ok = false; break; }
|
||||
std::vector<u32> poly(static_cast<size_t>(n));
|
||||
for (u32& k : poly) { // (a negative, huge or NaN index is no vertex: refused below, never an out-of-range cast)
|
||||
const double d = read(p.type);
|
||||
k = d >= 0 && d < 4294967295.0 ? static_cast<u32>(d) : UINT32_MAX;
|
||||
}
|
||||
if (face && (p.name == "vertex_indices" || p.name == "vertex_index"))
|
||||
for (size_t k = 2; k < poly.size(); ++k) tris.insert(tris.end(), {poly[0], poly[k - 1], poly[k]});
|
||||
continue;
|
||||
}
|
||||
const double x = read(p.type);
|
||||
if (!vert) continue;
|
||||
const bool byteColour = size(p.type) == 1;
|
||||
if (p.name == "x") v[0] = static_cast<float>(x);
|
||||
else if (p.name == "y") v[1] = static_cast<float>(x);
|
||||
else if (p.name == "z") v[2] = static_cast<float>(x);
|
||||
else if (p.name == "s" || p.name == "u" || p.name == "texture_u") t[0] = static_cast<float>(x);
|
||||
else if (p.name == "t" || p.name == "v" || p.name == "texture_v") t[1] = static_cast<float>(x);
|
||||
else if (p.name == "red" || p.name == "green" || p.name == "blue" || p.name == "alpha") {
|
||||
// 0..255 (uchar), 0..65535 (ushort) or 0..1 (float)
|
||||
const double unit = byteColour ? x / 255 : size(p.type) == 2 && p.type.find('u') == 0 ? x / 65535 : x;
|
||||
c[p.name == "red" ? 0 : p.name == "green" ? 1 : p.name == "blue" ? 2 : 3] = static_cast<float>(unit);
|
||||
}
|
||||
}
|
||||
if (vert) {
|
||||
xyz.insert(xyz.end(), v, v + 3);
|
||||
uv.insert(uv.end(), {t[0], 1.0f - t[1]});
|
||||
// Scanners and MeshLab store display (sRGB) colours; the shader works in linear light, as glTF's COLOR_0 is.
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const float s = std::clamp(c[k], 0.0f, 1.0f);
|
||||
c[k] = s <= 0.04045f ? s / 12.92f : std::pow((s + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
for (float k : c) rgba.push_back(static_cast<u8>(std::clamp(k, 0.0f, 1.0f) * 255.0f + 0.5f));
|
||||
}
|
||||
}
|
||||
if (!ok) { error = bad; return false; }
|
||||
}
|
||||
for (float v : xyz) if (!std::isfinite(v)) { error = "damaged PLY file (bad numbers): " + path; return false; }
|
||||
if (tris.empty()) { error = "this PLY has only points (a point cloud), not faces: Frame Studio can't open those yet: " + path; return false; }
|
||||
MeshData md;
|
||||
for (u32 k : tris) {
|
||||
if (k >= xyz.size() / 3) { error = bad; return false; }
|
||||
md.positions.insert(md.positions.end(), &xyz[k * 3], &xyz[k * 3] + 3);
|
||||
if (colours) md.colors.insert(md.colors.end(), &rgba[k * 4], &rgba[k * 4] + 4);
|
||||
}
|
||||
finishPlain(md, path);
|
||||
for (size_t i = 3; i < md.colors.size() && !md.useBlend; i += 4) md.useBlend = md.colors[i] < 250; // see-through vertices: blended
|
||||
if (coords) for (size_t i = 0; i < tris.size(); ++i) { md.uvs[i * 2] = uv[tris[i] * 2]; md.uvs[i * 2 + 1] = uv[tris[i] * 2 + 1]; }
|
||||
files.push_back(path);
|
||||
meshes.push_back(std::move(md));
|
||||
return true;
|
||||
}
|
||||
|
||||
// A panorama sky (equirectangular: 360 x 180 degrees, width = 2 x height) made into the six faces of a sky cube, each
|
||||
// a quad with its own picture, drawn as a home's own skybox is (the unit cube follows the head, behind everything).
|
||||
// The middle of the picture faces -Z (ahead). Radiance .hdr is shown at its own exposure (clipped at white).
|
||||
bool importSkyPanorama(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
const std::vector<u8> b = readFile(path);
|
||||
if (b.empty()) { error = "can't read the panorama " + path; return false; }
|
||||
if (!sizeOk(b.data(), b.size())) { error = "not a picture Frame Studio can read (.hdr, .jpg, .png), or larger than 16384 x 8192: " + path; return false; }
|
||||
const bool hdr = b.size() > 10 && (std::memcmp(b.data(), "#?RADIANCE", 10) == 0 || std::memcmp(b.data(), "#?RGBE", 6) == 0);
|
||||
int w = 0, h = 0, comp = 0;
|
||||
std::vector<float> px; // RGB: linear (HDR) or 0..1 as stored (sRGB pictures)
|
||||
if (hdr) {
|
||||
float* f = stbi_loadf_from_memory(b.data(), static_cast<int>(b.size()), &w, &h, &comp, 3);
|
||||
if (f) { px.assign(f, f + static_cast<size_t>(w) * h * 3); stbi_image_free(f); }
|
||||
} else {
|
||||
unsigned char* c = stbi_load_from_memory(b.data(), static_cast<int>(b.size()), &w, &h, &comp, 3);
|
||||
if (c) {
|
||||
px.resize(static_cast<size_t>(w) * h * 3);
|
||||
for (size_t i = 0; i < px.size(); ++i) px[i] = c[i] / 255.0f;
|
||||
stbi_image_free(c);
|
||||
}
|
||||
}
|
||||
if (px.empty() || w < 8 || h < 4) { error = "not a picture Frame Studio can read (.hdr, .jpg, .png): " + path; return false; }
|
||||
// A face spans 90 degrees = a quarter of the width; at most 2048 (8K panoramas), at least 64.
|
||||
const u32 n = static_cast<u32>(std::clamp(w / 4, 64, 2048));
|
||||
auto sample = [&](const float d[3], float out[3]) { // bilinear, wrapping round, clamped at the poles
|
||||
const float len = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
|
||||
const float lon = std::atan2(d[0], -d[2]), lat = std::asin(std::clamp(d[1] / len, -1.0f, 1.0f));
|
||||
const float x = (0.5f + lon / 6.2831853f) * w - 0.5f, y = (0.5f - lat / 3.14159265f) * h - 0.5f;
|
||||
const int x0 = static_cast<int>(std::floor(x)), y0 = static_cast<int>(std::floor(y));
|
||||
const float fx = x - x0, fy = y - y0;
|
||||
auto at = [&](int xx, int yy, int c) {
|
||||
xx = ((xx % w) + w) % w;
|
||||
yy = std::clamp(yy, 0, h - 1);
|
||||
return px[(static_cast<size_t>(yy) * w + xx) * 3 + c];
|
||||
};
|
||||
for (int c = 0; c < 3; ++c)
|
||||
out[c] = (at(x0, y0, c) * (1 - fx) + at(x0 + 1, y0, c) * fx) * (1 - fy) + (at(x0, y0 + 1, c) * (1 - fx) + at(x0 + 1, y0 + 1, c) * fx) * fy;
|
||||
};
|
||||
struct Face { const char* name; float f[3], r[3], u[3]; };
|
||||
static const Face faces[6] = {
|
||||
{"ahead", {0, 0, -1}, {1, 0, 0}, {0, 1, 0}}, {"behind", {0, 0, 1}, {-1, 0, 0}, {0, 1, 0}},
|
||||
{"right", {1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, {"left", {-1, 0, 0}, {0, 0, -1}, {0, 1, 0}},
|
||||
{"up", {0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, {"down", {0, -1, 0}, {1, 0, 0}, {0, 0, -1}},
|
||||
};
|
||||
const std::string stem = fs::u8path(path).stem().stem().u8string(); // ("dusk.sky.hdr": "dusk")
|
||||
const float white[4] = {1, 1, 1, 1};
|
||||
for (const Face& fc : faces) {
|
||||
Image img;
|
||||
img.w = img.h = n;
|
||||
img.rgba.resize(static_cast<size_t>(n) * n * 4);
|
||||
// Texel centres run edge to edge (the outer ones ON the cube's edges) and the quad's UVs stop half a texel in,
|
||||
// so neighbouring faces meet on the same colours: no seam, and the wrapping sampler never reaches across.
|
||||
for (u32 j = 0; j < n; ++j)
|
||||
for (u32 i = 0; i < n; ++i) {
|
||||
const float a = -1.0f + 2.0f * i / (n - 1), bb = 1.0f - 2.0f * j / (n - 1);
|
||||
const float d[3] = {fc.f[0] + a * fc.r[0] + bb * fc.u[0], fc.f[1] + a * fc.r[1] + bb * fc.u[1], fc.f[2] + a * fc.r[2] + bb * fc.u[2]};
|
||||
float c[3];
|
||||
sample(d, c);
|
||||
u8* o = &img.rgba[(static_cast<size_t>(j) * n + i) * 4];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
float s = std::clamp(c[k], 0.0f, 1.0f);
|
||||
if (hdr) s = s <= 0.0031308f ? s * 12.92f : 1.055f * std::pow(s, 1.0f / 2.4f) - 0.055f; // linear -> sRGB
|
||||
o[k] = static_cast<u8>(s * 255.0f + 0.5f);
|
||||
}
|
||||
o[3] = 255;
|
||||
}
|
||||
MeshData md;
|
||||
md.name = "Sky " + std::string(fc.name);
|
||||
md.path = stem + " (panorama sky)";
|
||||
const float hf = 0.5f / n;
|
||||
const float corner[4][4] = {{-1, 1, hf, hf}, {1, 1, 1 - hf, hf}, {1, -1, 1 - hf, 1 - hf}, {-1, -1, hf, 1 - hf}};
|
||||
for (const auto& q : corner) {
|
||||
for (int k = 0; k < 3; ++k) md.positions.push_back(fc.f[k] + q[0] * fc.r[k] + q[1] * fc.u[k]);
|
||||
md.uvs.insert(md.uvs.end(), {q[2], q[3]});
|
||||
}
|
||||
md.indices = {0, 2, 1, 0, 3, 2};
|
||||
finish(md, &img, white, false, false, 0.5f);
|
||||
md.isSkybox = true;
|
||||
meshes.push_back(std::move(md));
|
||||
}
|
||||
files.push_back(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isSkyPanoramaPath(const std::string& path) {
|
||||
const std::string name = lower(fs::u8path(path).filename().u8string());
|
||||
for (const char* e : {".sky.hdr", ".sky.jpg", ".sky.jpeg", ".sky.png"})
|
||||
if (name.size() > std::strlen(e) && name.compare(name.size() - std::strlen(e), std::string::npos, e) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isModelPath(const std::string& path) {
|
||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||
return ext == ".gltf" || ext == ".glb" || ext == ".fbx" || ext == ".obj";
|
||||
return ext == ".gltf" || ext == ".glb" || ext == ".fbx" || ext == ".obj" || ext == ".stl" || ext == ".ply" || isSkyPanoramaPath(path);
|
||||
}
|
||||
|
||||
bool importModel(const std::string& path, std::vector<MeshData>& meshes, std::vector<std::string>& files, std::string& error) {
|
||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||
if (ext == ".gltf" || ext == ".glb") return importGltf(path, meshes, files, error);
|
||||
if (ext == ".fbx" || ext == ".obj") return importUfbx(path, meshes, files, error);
|
||||
error = "unsupported model type: " + path;
|
||||
return false;
|
||||
// Each runs on a loader thread of its own: a model too big for memory (bad_alloc, length_error) must come back as
|
||||
// an error, not escape the thread and end the program (with the open home's unsaved changes).
|
||||
const size_t meshesBefore = meshes.size(), filesBefore = files.size();
|
||||
try {
|
||||
if (isSkyPanoramaPath(path)) return importSkyPanorama(path, meshes, files, error);
|
||||
const std::string ext = lower(fs::u8path(path).extension().u8string());
|
||||
if (ext == ".gltf" || ext == ".glb") return importGltf(path, meshes, files, error);
|
||||
if (ext == ".fbx" || ext == ".obj") return importUfbx(path, meshes, files, error);
|
||||
if (ext == ".stl") return importStl(path, meshes, files, error);
|
||||
if (ext == ".ply") return importPly(path, meshes, files, error);
|
||||
error = "unsupported model type: " + path;
|
||||
return false;
|
||||
} catch (const std::exception&) {
|
||||
meshes.resize(meshesBefore);
|
||||
files.resize(filesBefore);
|
||||
error = "not enough memory to open " + path;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user