CHAOS /DOCS/CHENG

.CHENG

.cheng, your own stuff on the stage

A .cheng is a mesh already cleaned, cut down and sized for the stage, so loading it costs nothing. On the stage it replaces the main shape and the satellites go. Palette, mood, mode, backdrop and particles stay the seed's. It's a display thing on your install only: never a seed, never claimable, never valued, the registry never hears about it.

What goes in

client/src/model/cheng.hc++
// .cheng: the engine's own mesh file. one object, already cleaned, decimated and sized for the stage, so loading it
// costs nothing: positions quantized to 16 bits inside the bounding box, normals as 16-bit octahedral pairs, triangle
// indices. a 20k-vertex model is about 300 KB. made from .obj / .stl / .gltf / .glb (and .blend / .fbx / .dae / .step
// through blender when it is installed) by `chaos --cheng` or the app's model tab. never a seed, never claimable.

.obj, .stl, .gltf and .glb the app reads itself. .blend, .fbx, .dae, .step, .usd, .x3d, .abc go through Blender if you have it installed: the app writes a small python script, Blender exports a .glb, the app reads that. No Blender = no those formats, export a .glb yourself.

Clean, cut, size

Weld and decimate in one go: vertices snap to a grid and merge, the grid gets coarser until the count fits the cap, collapsed triangles get dropped, then center and scale to the stage radius.

client/src/model/cheng.cppc++
Mesh prepareModel(Mesh m, int maxVerts, float radius) {
    if (m.positions.empty()) return m;
    // bounds
    glm::vec3 lo(1e30f), hi(-1e30f);
    for (auto& p : m.positions) { lo = glm::min(lo, p); hi = glm::max(hi, p); }
    glm::vec3 size = glm::max(hi - lo, glm::vec3(1e-6f));
    // weld + decimate by clustering on a grid. the grid gets coarser until the vertex count fits
    float cell = std::max(size.x, std::max(size.y, size.z)) / 512.f;
    Mesh out;
    for (int iter = 0; iter < 24; iter++) {
        out = Mesh{};
        std::unordered_map<uint64_t, uint32_t> cells;
        std::vector<uint32_t> remap(m.positions.size());
        std::vector<glm::vec3> sumP, sumN; std::vector<int> cnt;
        for (size_t i = 0; i < m.positions.size(); i++) {
            glm::ivec3 g = glm::ivec3(glm::floor((m.positions[i] - lo) / cell));
            uint64_t key = (uint64_t(uint32_t(g.x) & 0x1fffff) << 42) | (uint64_t(uint32_t(g.y) & 0x1fffff) << 21) | (uint32_t(g.z) & 0x1fffff);
            auto it = cells.find(key);
            if (it == cells.end()) { it = cells.emplace(key, uint32_t(sumP.size())).first; sumP.push_back(glm::vec3(0)); sumN.push_back(glm::vec3(0)); cnt.push_back(0); }
            sumP[it->second] += m.positions[i]; sumN[it->second] += m.normals.size() > i ? m.normals[i] : glm::vec3(0); cnt[it->second]++;
            remap[i] = it->second;
        }
        out.positions.resize(sumP.size()); out.normals.resize(sumP.size());
        for (size_t k = 0; k < sumP.size(); k++) { out.positions[k] = sumP[k] / float(cnt[k]); glm::vec3 n = sumN[k]; out.normals[k] = glm::dot(n, n) > 1e-12f ? glm::normalize(n) : glm::vec3(0, 1, 0); }
        for (size_t t = 0; t + 2 < m.indices.size(); t += 3) {
            uint32_t a = remap[m.indices[t]], b = remap[m.indices[t + 1]], c = remap[m.indices[t + 2]];
            if (a == b || b == c || a == c) continue;                 // collapsed
            out.indices.push_back(a); out.indices.push_back(b); out.indices.push_back(c);
        }
        if (int(out.positions.size()) <= maxVerts || out.indices.empty()) break;
        cell *= 1.35f;
    }
    // drop vertices no triangle uses
    std::vector<uint32_t> used(out.positions.size(), UINT32_MAX); Mesh packed;
    for (uint32_t& i : out.indices) { if (used[i] == UINT32_MAX) { used[i] = uint32_t(packed.positions.size()); packed.positions.push_back(out.positions[i]); packed.normals.push_back(out.normals[i]); } i = used[i]; }
    packed.indices = std::move(out.indices);
    bool flat = true; for (auto& n : packed.normals) if (n != glm::vec3(0, 1, 0)) { flat = false; break; }
    if (flat || packed.normals.empty()) packed.computeVertexNormals();
    packed.center();
    float r = packed.boundingRadius();
    if (r > 1e-6f) for (auto& p : packed.positions) p *= radius / r;
    return packed;
}

The format

Magic, counts, bounds, name, then every vertex as five 16 bit numbers (position inside the bounds, normal as an octahedral pair), then 32 bit indices. A 20k vertex model is around 300 KB.

client/src/model/cheng.cppc++
bool writeCheng(const fs::path& path, const Mesh& m, const std::string& name) {
    std::ofstream out(path, std::ios::binary);
    if (!out) return false;
    glm::vec3 lo(1e30f), hi(-1e30f);
    for (auto& p : m.positions) { lo = glm::min(lo, p); hi = glm::max(hi, p); }
    glm::vec3 size = glm::max(hi - lo, glm::vec3(1e-6f));
    uint32_t nv = uint32_t(m.positions.size()), ni = uint32_t(m.indices.size());
    uint8_t nameLen = uint8_t(std::min<size_t>(name.size(), 255));
    out.write(MAGIC, 6); out.write(reinterpret_cast<const char*>(&nv), 4); out.write(reinterpret_cast<const char*>(&ni), 4);
    out.write(reinterpret_cast<const char*>(&lo), 12); out.write(reinterpret_cast<const char*>(&hi), 12);
    out.write(reinterpret_cast<const char*>(&nameLen), 1); out.write(name.data(), nameLen);
    std::vector<uint16_t> q; q.reserve(size_t(nv) * 5);
    for (uint32_t i = 0; i < nv; i++) {
        glm::vec3 t = (m.positions[i] - lo) / size;
        for (int k = 0; k < 3; k++) q.push_back(uint16_t(std::lround(glm::clamp(t[k], 0.f, 1.f) * 65535.f)));
        glm::vec2 e = octEncode(m.normals.size() > i ? m.normals[i] : glm::vec3(0, 1, 0));
        q.push_back(uint16_t(std::lround((glm::clamp(e.x, -1.f, 1.f) * 0.5f + 0.5f) * 65535.f)));
        q.push_back(uint16_t(std::lround((glm::clamp(e.y, -1.f, 1.f) * 0.5f + 0.5f) * 65535.f)));
    }
    out.write(reinterpret_cast<const char*>(q.data()), std::streamsize(q.size() * 2));
    out.write(reinterpret_cast<const char*>(m.indices.data()), std::streamsize(ni * 4));
    return bool(out);
}

std::optional<Mesh> readCheng(const fs::path& path, std::string* name) {
    std::ifstream in(path, std::ios::binary);
    if (!in) return std::nullopt;
    char magic[6]; in.read(magic, 6);
    if (!in || std::memcmp(magic, MAGIC, 6) != 0) return std::nullopt;
    uint32_t nv, ni; glm::vec3 lo, hi; uint8_t nameLen;
    in.read(reinterpret_cast<char*>(&nv), 4); in.read(reinterpret_cast<char*>(&ni), 4);
    in.read(reinterpret_cast<char*>(&lo), 12); in.read(reinterpret_cast<char*>(&hi), 12); in.read(reinterpret_cast<char*>(&nameLen), 1);
    if (!in || nv == 0 || nv > 2'000'000 || ni > 6'000'000 || ni % 3) return std::nullopt;
    std::string nm(nameLen, '\0'); in.read(nm.data(), nameLen); if (name) *name = nm;
    std::vector<uint16_t> q(size_t(nv) * 5); in.read(reinterpret_cast<char*>(q.data()), std::streamsize(q.size() * 2));
    Mesh m; m.positions.resize(nv); m.normals.resize(nv); m.indices.resize(ni);
    in.read(reinterpret_cast<char*>(m.indices.data()), std::streamsize(ni * 4));
    if (!in) return std::nullopt;
    glm::vec3 size = hi - lo;
    for (uint32_t i = 0; i < nv; i++) {
        m.positions[i] = lo + size * glm::vec3(q[i * 5] / 65535.f, q[i * 5 + 1] / 65535.f, q[i * 5 + 2] / 65535.f);
        m.normals[i] = octDecode(glm::vec2(q[i * 5 + 3] / 65535.f * 2 - 1, q[i * 5 + 4] / 65535.f * 2 - 1));
    }
    for (uint32_t i : m.indices) if (i >= nv) return std::nullopt;
    return m;
}

On the stage

The dimension keeps everything except its main mesh and its swarm. Skins: berry (the seed's own skin on your model), mono (one color, lit), wire. Random picks one per start.

client/src/scene/dimension.cppc++
void applyModel(Dimension& d, Mesh mesh, int skin, glm::vec3 colour) {
    if (mesh.indices.empty()) return;
    d.mainRadius = mesh.boundingRadius();
    d.mainMesh = std::move(mesh);
    d.sats.clear(); d.folds.clear(); d.debris.clear(); d.rings.clear();
    d.modelSkin = skin < 0 ? MODEL_BERRY : skin > 2 ? MODEL_WIRE : skin;
    d.modelColor = colour;
}

Where

The model tab: choose a file or type a path, pick skin and color, put it on the stage, save. chaos --cheng file converts from the terminal, chaos --model file shows one. Converted files land in the models folder next to your vault. The KDE wallpaper carries the model when you hit "set as desktop wallpaper" and has the same settings on its page.