diff --git a/engine/render/backend.h b/engine/render/backend.h index 6c2ade5..5665f96 100644 --- a/engine/render/backend.h +++ b/engine/render/backend.h @@ -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]; diff --git a/engine/render/renderer.cpp b/engine/render/renderer.cpp index c93030d..112564d 100644 --- a/engine/render/renderer.cpp +++ b/engine/render/renderer.cpp @@ -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* 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(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()); diff --git a/engine/render/renderer.h b/engine/render/renderer.h index 27431bb..a22da7d 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -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* 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. diff --git a/engine/shaders/mesh.frag b/engine/shaders/mesh.frag index f8e4010..7687aaf 100644 --- a/engine/shaders/mesh.frag +++ b/engine/shaders/mesh.frag @@ -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. diff --git a/src/app.cpp b/src/app.cpp index 3ccf623..40a06ce 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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()); diff --git a/src/scene.cpp b/src/scene.cpp index 5f940d6..4d29bc6 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -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& materialPresets() { + static const std::vector 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) { diff --git a/src/scene.h b/src/scene.h index b68bd6e..1e900e3 100644 --- a/src/scene.h +++ b/src/scene.h @@ -11,6 +11,7 @@ #include #include #include +#include #include 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& 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); diff --git a/src/script.cpp b/src/script.cpp index 13edc43..7cab9f9 100644 --- a/src/script.cpp +++ b/src/script.cpp @@ -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) diff --git a/tests/script_test.cpp b/tests/script_test.cpp index 16d4676..3635b6b 100644 --- a/tests/script_test.cpp +++ b/tests/script_test.cpp @@ -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");