Materials shown in the view: each object's metal and rough reach the shader (Solid and Rendered); a Material picker with sixteen to start from (gold, chrome, wood, ceramic...); o.material and fry.materials() in scripts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-20 17:14:55 +02:00
commit e93ae44684
9 changed files with 101 additions and 5 deletions

View file

@ -53,7 +53,8 @@ struct DrawData { // DRAW_FIELDS in model.glsl
glm::vec4 cube[6];
glm::vec4 params; // x = sun visibility, y = lit by probes, z = liquid cut, w = LiquidStyle
glm::vec4 plane; // liquid surface
glm::vec4 probe; // the nearest reflection probe: x = its texture (0 = none), y = its top level; z = time (s)
glm::vec4 probe; // the nearest reflection probe: x = its texture (0 = none), y = its top level; z = time (s);
// w = its own surface without a material map: 0 none, else 1 + metal (0..255) + rough (0..255) * 256
};
struct FrameData { // FRAME_FIELDS in model.glsl, then (Vulkan) four buffer addresses
glm::mat4 cascades[4];

View file

@ -222,7 +222,7 @@ void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float widt
}
void Renderer::drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint, const std::vector<glm::mat4>* skin,
const ModelLighting* lighting, bool castShadow, const glm::vec4* liquidPlane, LiquidStyle liquid) {
const ModelLighting* lighting, bool castShadow, const glm::vec4* liquidPlane, LiquidStyle liquid, const Surface* surface) {
if (!model.valid() || packet_.draws.size() >= kMaxDraws) return;
ModelDraw d{model.index, transform, tint};
d.castShadow = castShadow || shadowOnly_;
@ -261,6 +261,8 @@ void Renderer::drawModel(ModelId model, const glm::mat4& transform, const glm::v
}
static const auto t0 = std::chrono::steady_clock::now(); // water's waves move with this
dd.probe.z = hasClock_ ? float(std::fmod(clock_, 3600.0)) : std::fmod(std::chrono::duration<float>(std::chrono::steady_clock::now() - t0).count(), 3600.0f);
if (surface)
dd.probe.w = 1.0f + std::round(std::clamp(surface->metal, 0.0f, 1.0f) * 255.0f) + std::round(std::clamp(surface->rough, 0.0f, 1.0f) * 255.0f) * 256.0f;
packet_.draws.push_back(dd);
if (skin && mdl.skinned && packet_.joints.size() + skin->size() <= kMaxJointsPerFrame) {
d.firstJoint = uint32_t(packet_.joints.size());

View file

@ -25,6 +25,11 @@ struct Camera {
};
// Baked light for a moving model (from the map's probes): ambient cube + sun visibility.
// A whole model's surface where it has no material map: how metal (0..1) and how rough (0..1).
struct Surface {
float metal = 0.0f, rough = 0.85f;
};
struct ModelLighting {
glm::vec3 cube[6]; // light arriving from +X -X +Y -Y +Z -Z
float sunVisibility = 1.0f;
@ -98,7 +103,7 @@ public:
// lighting: probe light for moving things (nullptr = lightmap or simple sun shading).
void drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint = glm::vec4(1.0f),
const std::vector<glm::mat4>* skin = nullptr, const ModelLighting* lighting = nullptr, bool castShadow = true,
const glm::vec4* liquidPlane = nullptr, LiquidStyle liquid = LiquidStyle::None);
const glm::vec4* liquidPlane = nullptr, LiquidStyle liquid = LiquidStyle::None, const Surface* surface = nullptr);
// liquidPlane (world space: xyz normal, w distance): draws a closed mesh as liquid filling it
// up to the plane — above it is cut away, and its back faces show as the flat surface.
// Alpha below 1 (tint × material) makes a submesh translucent: sorted, blended, after opaque.

View file

@ -159,6 +159,10 @@ void main() {
n = normalize(tbn * vec3(xy, sqrt(max(1.0 - dot(xy, xy), 0.0))));
metal = m.z;
gloss = m.w;
} else if (DRAW.probe.w > 0.5) { // the model's own surface (Renderer::Surface)
uint s = uint(DRAW.probe.w - 0.5);
metal = float(s & 255u) / 255.0;
gloss = 1.0 - float((s >> 8) & 255u) / 255.0;
}
// Weather: how open to the sky this spot is (the lightmap's sky map; moving things: their
// sun visibility), then rain wetting it (darker, glossy) and snow lying on what faces up.

View file

@ -4576,7 +4576,8 @@ private:
for (const Object& o : scene_.objects) {
if (!o.visible) continue;
const glm::vec4 tint = painted(o) ? glm::vec4(1.0f) : glm::vec4(glm::pow(o.color, glm::vec3(2.2f)), 1.0f); // (colours are picked as seen: sRGB)
r.drawModel(modelOf(o), toUnits * objectMatrix(o), tint, nullptr, &light, rendered);
const keel::Surface surface{o.metal, o.rough};
r.drawModel(modelOf(o), toUnits * objectMatrix(o), tint, nullptr, &light, rendered, nullptr, keel::LiquidStyle::None, &surface);
}
// Rendered: a ground to stand on and catch the shadows (not part of what you make).
if (rendered && look.ground) {
@ -5710,6 +5711,7 @@ private:
if (ImGui::Button("Save a picture (F12)", ImVec2(w, 0))) dialog(5);
if (Object* o = selected_.size() == 1 ? find(active_) : nullptr) {
ImGui::SeparatorText(o->name.c_str());
materialPicker(*o);
undoable([&] { return ImGui::ColorEdit3("Colour", &o->color.x); });
undoable([&] { return ImGui::SliderFloat("Metal", &o->metal, 0, 1); });
undoable([&] { return ImGui::SliderFloat("Rough", &o->rough, 0, 1); });
@ -5916,6 +5918,7 @@ private:
const glm::vec3 lo = boundsMin(shownMesh(*o)) * glm::abs(o->scale), hi = boundsMax(shownMesh(*o)) * glm::abs(o->scale);
ImGui::TextDisabled("%.3f x %.3f x %.3f m", hi.x - lo.x, hi.y - lo.y, hi.z - lo.z);
ImGui::SeparatorText("Look");
materialPicker(*o);
undoable([&] { return ImGui::ColorEdit3("Colour", &o->color.x); });
undoable([&] { return ImGui::SliderFloat("Metal", &o->metal, 0, 1); });
undoable([&] { return ImGui::SliderFloat("Rough", &o->rough, 0, 1); });
@ -5970,6 +5973,31 @@ private:
ImGui::End();
}
// A material to start from: sets colour, metal and rough in one go (each a little ball of it).
void materialPicker(Object& o) {
const MaterialPreset* now = nullptr;
for (const MaterialPreset& m : materialPresets())
if (std::abs(m.metal - o.metal) < 0.01f && std::abs(m.rough - o.rough) < 0.01f && (m.keepColor || glm::all(glm::lessThan(glm::abs(m.color - o.color), glm::vec3(0.01f))))) now = &m;
ImGui::SetNextItemWidth(ImGui::CalcItemWidth());
if (!ImGui::BeginCombo("Material", now ? now->name : "(its own)", ImGuiComboFlags_HeightLarge)) return;
for (const MaterialPreset& m : materialPresets()) {
const ImVec2 p = ImGui::GetCursorScreenPos();
const float h = ImGui::GetTextLineHeight();
ImGui::PushID(m.id);
if (ImGui::Selectable("##m", now == &m, 0, ImVec2(0, h))) pushUndo(), applyMaterial(o, m), touched(o.id);
ImGui::PopID();
// A ball: its colour, darker round the edge, a highlight as sharp as it's smooth.
const glm::vec3 c = m.keepColor ? o.color : m.color;
const ImVec2 mid(p.x + h * 0.5f, p.y + h * 0.5f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddCircleFilled(mid, h * 0.45f, ImGui::ColorConvertFloat4ToU32(ImVec4(c.r * 0.55f, c.g * 0.55f, c.b * 0.55f, 1)));
dl->AddCircleFilled(ImVec2(mid.x - h * 0.05f, mid.y - h * 0.05f), h * 0.36f, ImGui::ColorConvertFloat4ToU32(ImVec4(c.r, c.g, c.b, 1)));
dl->AddCircleFilled(ImVec2(mid.x - h * 0.14f, mid.y - h * 0.14f), h * (0.06f + 0.1f * m.rough), IM_COL32(255, 255, 255, int(230 * (1.0f - m.rough * 0.8f))));
dl->AddText(ImVec2(p.x + h * 1.4f, p.y), ImGui::GetColorU32(ImGuiCol_Text), m.name);
}
ImGui::EndCombo();
}
void editProperties(Object& o, float w) {
ImGui::Text("Editing the shape of %s", o.name.c_str());
ImGui::TextDisabled("%zu points, %zu faces", o.mesh.verts.size(), o.mesh.faces.size());

View file

@ -32,6 +32,31 @@ glm::mat4 objectMatrix(const Object& o) {
return glm::translate(glm::mat4(1.0f), o.position) * glm::mat4_cast(o.rotation) * glm::scale(glm::mat4(1.0f), o.scale);
}
const std::vector<MaterialPreset>& materialPresets() {
static const std::vector<MaterialPreset> list = {
{"matte", "Matte (its colour)", {}, 0.0f, 0.9f, true}, {"satin", "Satin (its colour)", {}, 0.0f, 0.45f, true},
{"glossy", "Glossy (its colour)", {}, 0.0f, 0.12f, true}, {"plastic", "White plastic", {0.9f, 0.9f, 0.88f}, 0.0f, 0.35f},
{"rubber", "Black rubber", {0.08f, 0.08f, 0.08f}, 0.0f, 0.95f}, {"wood", "Wood", {0.62f, 0.42f, 0.26f}, 0.0f, 0.7f},
{"clay", "Clay", {0.72f, 0.4f, 0.28f}, 0.0f, 0.95f}, {"ceramic", "Ceramic", {0.95f, 0.95f, 0.93f}, 0.0f, 0.08f},
{"concrete", "Concrete", {0.6f, 0.6f, 0.58f}, 0.0f, 1.0f}, {"steel", "Brushed steel", {0.75f, 0.76f, 0.77f}, 1.0f, 0.4f},
{"chrome", "Chrome", {0.95f, 0.95f, 0.96f}, 1.0f, 0.04f}, {"aluminium", "Aluminium", {0.91f, 0.92f, 0.92f}, 1.0f, 0.3f},
{"gold", "Gold", {1.0f, 0.78f, 0.34f}, 1.0f, 0.18f}, {"copper", "Copper", {0.96f, 0.64f, 0.54f}, 1.0f, 0.25f},
{"brass", "Brass", {0.89f, 0.75f, 0.42f}, 1.0f, 0.3f}, {"iron", "Cast iron", {0.3f, 0.3f, 0.31f}, 1.0f, 0.75f},
};
return list;
}
const MaterialPreset* findMaterial(std::string_view id) {
for (const MaterialPreset& m : materialPresets())
if (id == m.id) return &m;
return nullptr;
}
void applyMaterial(Object& o, const MaterialPreset& m) {
if (!m.keepColor) o.color = m.color;
o.metal = m.metal, o.rough = m.rough;
}
Mesh finishedMesh(const Object& o, bool mirrorOnly) {
Mesh m = o.mesh;
for (int axis = 0; axis < 3; ++axis) {

View file

@ -11,6 +11,7 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace fry {
@ -128,6 +129,19 @@ struct Scene {
uint32_t nextId = 1;
};
// Materials to start from: a colour (unless `keepColor`: only how metal and rough it is) and
// how metal and rough. Shown in the view (Solid and Rendered) and exported with the object.
struct MaterialPreset {
const char* id; // as scripts name it
const char* name; // as shown
glm::vec3 color; // sRGB
float metal, rough;
bool keepColor = false;
};
const std::vector<MaterialPreset>& materialPresets();
const MaterialPreset* findMaterial(std::string_view id);
void applyMaterial(Object& o, const MaterialPreset& m);
glm::mat4 objectMatrix(const Object& o);
// A turn as degrees about X, then Y, then Z (what the Properties panel shows), and back.
glm::vec3 toEuler(const glm::quat& q);

View file

@ -202,6 +202,10 @@ bool setProperty(lua_State* L, Object& o, const std::string& k, int v) {
else if (k == "mirror") {
const std::string s = lower(luaL_checkstring(L, v));
o.mirror = (s.find('x') != std::string::npos ? 1 : 0) | (s.find('y') != std::string::npos ? 2 : 0) | (s.find('z') != std::string::npos ? 4 : 0);
} else if (k == "material") {
const MaterialPreset* m = findMaterial(lower(luaL_checkstring(L, v)));
if (!m) luaL_error(L, "there's no material called %s (fry.materials() lists them)", lua_tostring(L, v));
else applyMaterial(o, *m);
} else if (k == "round") o.round = std::max(0.0f, float(luaL_checknumber(L, v)));
else if (k == "round_steps") o.roundSteps = std::clamp(int(luaL_checkinteger(L, v)), 1, 12);
else if (k == "smooth_levels") o.smoothLevels = std::clamp(int(luaL_checkinteger(L, v)), 0, 4);
@ -640,6 +644,12 @@ int fryShapes(lua_State* L) {
}
return 1;
}
int fryMaterials(lua_State* L) { // fry.materials(): every material's name, for o.material = ...
lua_newtable(L);
int i = 0;
for (const MaterialPreset& m : materialPresets()) lua_pushstring(L, m.id), lua_rawseti(L, -2, ++i);
return 1;
}
int fryLook(lua_State* L) { // fry.look{sun_turn = 40, sky = false, ...}; returns the look as it is
Look& k = scene(L).look;
struct Num {
@ -696,7 +706,7 @@ int fryHelp(lua_State* L) {
const luaL_Reg kFry[] = {
{"add", fryAdd}, {"mesh", fryMesh}, {"lathe", fryLathe}, {"extrude", fryExtrude}, {"objects", fryObjects}, {"find", fryFind},
{"selected", frySelected}, {"select", frySelect}, {"clear", fryClear}, {"group", fryGroup}, {"shapes", fryShapes}, {"look", fryLook},
{"selected", frySelected}, {"select", frySelect}, {"clear", fryClear}, {"group", fryGroup}, {"shapes", fryShapes}, {"look", fryLook}, {"materials", fryMaterials},
{"vec", fryVec}, {"say", fryPrint}, {"frame", fryFrame}, {"help", fryHelp}, {nullptr, nullptr},
};
@ -781,6 +791,7 @@ const char* scriptHelp() {
fry.add(shape, {setting = value, property = value, ...}) a library shape: fry.add("cube", {width = 2, color = "#ff8000"})
fry.shapes() every shape: {id, name, settings}
fry.materials() every material's name, for o.material
fry.mesh(points, faces, name) your own: points {{x,y,z}, ...}, faces {{1,2,3,4}, ...} (counter-clockwise from outside)
fry.lathe(profile, sides, name) a profile of {out, up} points turned round the Z axis (bottles, vases, bowls)
fry.extrude(outline, height, name) an outline of {x, y} points on the ground, made that tall
@ -792,6 +803,7 @@ fry.frame() fry.say(text) print(...) fry.help()
An object (o):
o.name o.position o.turn o.size o.color o.metal o.rough o.smooth o.visible o.locked
o.material = "gold" sets colour, metal and rough ("matte", "satin", "glossy" keep the colour)
o.mirror ("x", "xy"...) o.round o.round_steps o.smooth_levels (finishing touches, not baked)
o.width, o.sides ... a library shape's own settings, changeable until its shape is changed by hand
o.id o.shape o.point_count o.face_count (to read)

View file

@ -57,6 +57,11 @@ int main() {
CHECK(!r.ok, "no io");
r = runScript(host, "local o = fry.add('cube')\nlocal n = #o:points()\nlocal p = o:points()\nfor i = 1, n do p[i].z = p[i].z * 2 end\no:setpoints(p)\nlocal lo, hi = o:bounds()\nprint(hi.z - lo.z)");
CHECK(r.ok && out.find("\n2.0\n") != std::string::npos, "points round-trip (%s)", r.error.c_str());
r = runScript(host, "local o = fry.add('cube')\no.material = 'Gold'\nlocal m = fry.add('cube', {color = '#102030', material = 'glossy'})\nprint(#fry.materials())");
CHECK(r.ok && scene.objects.back().metal == 0.0f && scene.objects.back().rough < 0.2f && scene.objects.back().color.r < 0.1f, "a material keeping its colour (%s)", r.error.c_str());
CHECK(scene.objects[scene.objects.size() - 2].metal == 1.0f && scene.objects[scene.objects.size() - 2].color.g > 0.7f, "gold");
r = runScript(host, "fry.add('cube').material = 'cheese'");
CHECK(!r.ok && r.error.find("no material called cheese") != std::string::npos, "an unknown material is an error");
r = runScript(host, "fry.clear()");
CHECK(r.ok && scene.objects.empty() && active == 0 && selected.empty(), "clear");