VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
material.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 11.10.2025.
5//
6#include "material.h"
7
8#include <algorithm>
9#include <assert.h>
10#include <cmath>
11#include <initializer_list>
12#include <unordered_map>
13
17
18namespace visutwin::canvas
19{
21 std::unordered_map<GraphicsDevice*, std::shared_ptr<Material>> defaultMaterials;
22
23 namespace
24 {
25 const Material::ParameterValue* getParam(const Material* material, std::initializer_list<const char*> names)
26 {
27 if (!material) {
28 return nullptr;
29 }
30 for (const char* name : names) {
31 if (const auto* value = material->parameter(name)) {
32 return value;
33 }
34 }
35 return nullptr;
36 }
37
38 bool readFloat(const Material::ParameterValue* value, float& out)
39 {
40 if (!value) {
41 return false;
42 }
43 if (const auto* v = std::get_if<float>(value)) {
44 out = *v;
45 return true;
46 }
47 if (const auto* v = std::get_if<int32_t>(value)) {
48 out = static_cast<float>(*v);
49 return true;
50 }
51 if (const auto* v = std::get_if<uint32_t>(value)) {
52 out = static_cast<float>(*v);
53 return true;
54 }
55 return false;
56 }
57
58 bool readInt(const Material::ParameterValue* value, int& out)
59 {
60 if (!value) {
61 return false;
62 }
63 if (const auto* v = std::get_if<int32_t>(value)) {
64 out = static_cast<int>(*v);
65 return true;
66 }
67 if (const auto* v = std::get_if<uint32_t>(value)) {
68 out = static_cast<int>(*v);
69 return true;
70 }
71 if (const auto* v = std::get_if<float>(value)) {
72 out = static_cast<int>(*v);
73 return true;
74 }
75 if (const auto* v = std::get_if<bool>(value)) {
76 out = *v ? 1 : 0;
77 return true;
78 }
79 return false;
80 }
81
82 bool readColor4(const Material::ParameterValue* value, float out[4])
83 {
84 if (!value) {
85 return false;
86 }
87 if (const auto* v = std::get_if<Color>(value)) {
88 out[0] = v->r;
89 out[1] = v->g;
90 out[2] = v->b;
91 out[3] = v->a;
92 return true;
93 }
94 if (const auto* v = std::get_if<Vector3>(value)) {
95 out[0] = v->getX();
96 out[1] = v->getY();
97 out[2] = v->getZ();
98 out[3] = 1.0f;
99 return true;
100 }
101 if (const auto* v = std::get_if<Vector4>(value)) {
102 out[0] = v->getX();
103 out[1] = v->getY();
104 out[2] = v->getZ();
105 out[3] = v->getW();
106 return true;
107 }
108 if (const auto* v = std::get_if<float>(value)) {
109 out[0] = *v;
110 out[1] = *v;
111 out[2] = *v;
112 out[3] = 1.0f;
113 return true;
114 }
115 return false;
116 }
117
118 bool readTexture(const Material::ParameterValue* value, Texture*& out)
119 {
120 if (!value) {
121 return false;
122 }
123 if (const auto* v = std::get_if<Texture*>(value)) {
124 out = *v;
125 return true;
126 }
127 return false;
128 }
129
130 // pre-compute 3×2 affine matrix from tiling, offset, rotation.
131 // Matches upstream defineUniform() for texture_*MapTransform0/1.
132 constexpr float DEG_TO_RAD = 3.14159265358979323846f / 180.0f;
133
134 void packTransform(const TextureTransform& t, float row0[4], float row1[4])
135 {
136 const float cr = std::cos(t.rotation * DEG_TO_RAD);
137 const float sr = std::sin(t.rotation * DEG_TO_RAD);
138 row0[0] = cr * t.tiling.x;
139 row0[1] = -sr * t.tiling.y;
140 row0[2] = t.offset.x;
141 row0[3] = 0.0f;
142 row1[0] = sr * t.tiling.x;
143 row1[1] = cr * t.tiling.y;
144 row1[2] = 1.0f - t.tiling.y - t.offset.y;
145 row1[3] = 0.0f;
146 }
147 }
148
150 {
151 _blendState = std::make_shared<BlendState>();
152 _depthState = std::make_shared<DepthState>();
153 }
154
155 void Material::setParameter(const std::string& name, const ParameterValue& value)
156 {
157 if (name.empty()) {
158 return;
159 }
160 _parameters[name] = value;
161 }
162
163 bool Material::removeParameter(const std::string& name)
164 {
165 if (name.empty()) {
166 return false;
167 }
168 return _parameters.erase(name) > 0;
169 }
170
172 {
173 _parameters.clear();
174 }
175
176 const Material::ParameterValue* Material::parameter(const std::string& name) const
177 {
178 const auto it = _parameters.find(name);
179 return it == _parameters.end() ? nullptr : &it->second;
180 }
181
183 {
184 // Pack typed properties into GPU struct.
185 uniforms.baseColor[0] = _baseColorFactor.r;
186 uniforms.baseColor[1] = _baseColorFactor.g;
187 uniforms.baseColor[2] = _baseColorFactor.b;
188 uniforms.baseColor[3] = _baseColorFactor.a;
189 uniforms.emissiveColor[0] = _emissiveFactor.r;
190 uniforms.emissiveColor[1] = _emissiveFactor.g;
191 uniforms.emissiveColor[2] = _emissiveFactor.b;
192 uniforms.emissiveColor[3] = _emissiveFactor.a;
193 uniforms.alphaCutoff = _alphaCutoff;
194 uniforms.metallicFactor = _metallicFactor;
195 uniforms.roughnessFactor = _roughnessFactor;
196 uniforms.normalScale = _normalScale;
197 uniforms.occlusionStrength = _occlusionStrength;
198 uniforms.occludeSpecularMode = _occludeSpecular;
199 uniforms.occludeSpecularIntensity = _occludeSpecularIntensity;
200 uniforms.flags = 0u;
201
202 // Allow custom parameter overrides (same alias chains as the original inline code).
203 readColor4(getParam(this, {"material_baseColor", "baseColorFactor"}), uniforms.baseColor);
204 readColor4(getParam(this, {"material_emissive", "emissiveFactor"}), uniforms.emissiveColor);
205 readFloat(getParam(this, {"material_alphaCutoff", "alphaCutoff"}), uniforms.alphaCutoff);
206 readFloat(getParam(this, {"material_metallic", "metallicFactor"}), uniforms.metallicFactor);
207 readFloat(getParam(this, {"material_roughness", "roughnessFactor"}), uniforms.roughnessFactor);
208 readFloat(getParam(this, {"material_normalScale", "normalScale"}), uniforms.normalScale);
209 readFloat(getParam(this, {"material_occlusionStrength", "occlusionStrength"}), uniforms.occlusionStrength);
210 readFloat(getParam(this, {"material_occludeSpecularIntensity", "occludeSpecularIntensity"}),
211 uniforms.occludeSpecularIntensity);
212 {
213 int occludeSpecularMode = static_cast<int>(uniforms.occludeSpecularMode);
214 readInt(getParam(this, {"material_occludeSpecular", "occludeSpecular"}), occludeSpecularMode);
215 occludeSpecularMode = std::clamp(occludeSpecularMode,
216 static_cast<int>(SPECOCC_NONE), static_cast<int>(SPECOCC_GLOSSDEPENDENT));
217 uniforms.occludeSpecularMode = static_cast<uint32_t>(occludeSpecularMode);
218 }
219
220 // Flag bits — matches MaterialData.flags layout in common.metal.
221 if (_hasBaseColorTexture) {
222 uniforms.flags |= 1u; // bit 0: hasBaseColorMap
223 }
224 if (_alphaMode == AlphaMode::MASK) {
225 uniforms.flags |= (1u << 1); // bit 1: alphaTest
226 }
227 if (_hasNormalTexture) {
228 uniforms.flags |= (1u << 2); // bit 2: hasNormalMap
229 }
230 if (_cullMode == CullMode::CULLFACE_NONE) {
231 uniforms.flags |= (1u << 3); // bit 3: doubleSided
232 }
233
234 // UV set selection bits.
235 int baseUvSet = _baseColorUvSet;
236 int normalUvSet = _normalUvSet;
237 int metallicUvSet = _metallicRoughnessUvSet;
238 int occlusionUvSet = _occlusionUvSet;
239 int emissiveUvSet = _emissiveUvSet;
240 readInt(getParam(this, {"baseColorUvSet"}), baseUvSet);
241 readInt(getParam(this, {"normalUvSet"}), normalUvSet);
242 readInt(getParam(this, {"metallicRoughnessUvSet"}), metallicUvSet);
243 readInt(getParam(this, {"occlusionUvSet"}), occlusionUvSet);
244 readInt(getParam(this, {"emissiveUvSet"}), emissiveUvSet);
245 if (baseUvSet == 1) uniforms.flags |= (1u << 4);
246 if (normalUvSet == 1) uniforms.flags |= (1u << 5);
247 if (_hasMetallicRoughnessTexture) uniforms.flags |= (1u << 6);
248 if (metallicUvSet == 1) uniforms.flags |= (1u << 7);
249 if (_isSkybox) uniforms.flags |= (1u << 8);
250 if (_hasOcclusionTexture) uniforms.flags |= (1u << 9);
251 if (occlusionUvSet == 1) uniforms.flags |= (1u << 10);
252 if (_hasEmissiveTexture) uniforms.flags |= (1u << 11);
253 if (emissiveUvSet == 1) uniforms.flags |= (1u << 12);
254
255 int occludeDirect = _occludeDirect ? 1 : 0;
256 readInt(getParam(this, {"material_occludeDirect", "occludeDirect"}), occludeDirect);
257 if (occludeDirect != 0) uniforms.flags |= (1u << 13);
258
259 // Height/parallax map: flag bit 17.
260 Texture* heightTex = nullptr;
261 readTexture(getParam(this, {"texture_heightMap"}), heightTex);
262 if (heightTex) uniforms.flags |= (1u << 17);
263 readFloat(getParam(this, {"material_heightMapFactor", "heightMapFactor"}), uniforms.heightMapFactor);
264
265 // Anisotropy: parameter override.
266 readFloat(getParam(this, {"material_anisotropy", "anisotropy"}), uniforms.anisotropy);
267
268 // Transmission/refraction: parameter overrides.
269 readFloat(getParam(this, {"material_transmissionFactor", "transmissionFactor"}), uniforms.transmissionFactor);
270 readFloat(getParam(this, {"material_refractionIndex", "refractionIndex"}), uniforms.refractionIndex);
271 readFloat(getParam(this, {"material_thickness", "thickness"}), uniforms.thickness);
272
273 // Sheen: parameter overrides (KHR_materials_sheen).
274 readColor4(getParam(this, {"material_sheenColor", "sheenColor"}), uniforms.sheenColor);
275 readFloat(getParam(this, {"material_sheenRoughness", "sheenRoughness"}), uniforms.sheenColor[3]);
276 {
277 Texture* sheenTex = nullptr;
278 readTexture(getParam(this, {"texture_sheenMap"}), sheenTex);
279 if (sheenTex) uniforms.flags |= (1u << 18);
280 }
281
282 // Iridescence: parameter overrides (KHR_materials_iridescence).
283 readFloat(getParam(this, {"material_iridescenceIntensity", "iridescenceIntensity"}), uniforms.iridescenceParams[0]);
284 readFloat(getParam(this, {"material_iridescenceIOR", "iridescenceIOR"}), uniforms.iridescenceParams[1]);
285 readFloat(getParam(this, {"material_iridescenceThicknessMin", "iridescenceThicknessMin"}), uniforms.iridescenceParams[2]);
286 readFloat(getParam(this, {"material_iridescenceThicknessMax", "iridescenceThicknessMax"}), uniforms.iridescenceParams[3]);
287 {
288 Texture* iriTex = nullptr;
289 readTexture(getParam(this, {"texture_iridescenceMap"}), iriTex);
290 if (iriTex) uniforms.flags |= (1u << 19);
291 Texture* iriThickTex = nullptr;
292 readTexture(getParam(this, {"texture_iridescenceThicknessMap"}), iriThickTex);
293 if (iriThickTex) uniforms.flags |= (1u << 20);
294 }
295
296 // Spec-Gloss: parameter overrides (KHR_materials_pbrSpecularGlossiness).
297 readColor4(getParam(this, {"material_specularColor", "specularColor"}), uniforms.specGlossParams);
298 readFloat(getParam(this, {"material_glossiness", "glossiness"}), uniforms.specGlossParams[3]);
299 {
300 Texture* sgTex = nullptr;
301 readTexture(getParam(this, {"texture_specGlossMap"}), sgTex);
302 if (sgTex) uniforms.flags |= (1u << 21);
303 }
304
305 // Detail normals: parameter overrides.
306 readFloat(getParam(this, {"material_detailNormalScale", "detailNormalScale"}), uniforms.detailDisplacementParams[0]);
307 {
308 Texture* detailTex = nullptr;
309 readTexture(getParam(this, {"texture_detailNormalMap"}), detailTex);
310 if (detailTex) uniforms.flags |= (1u << 22);
311 }
312
313 // Displacement: parameter overrides.
314 readFloat(getParam(this, {"material_displacementScale", "displacementScale"}), uniforms.detailDisplacementParams[1]);
315 readFloat(getParam(this, {"material_displacementBias", "displacementBias"}), uniforms.detailDisplacementParams[2]);
316 {
317 Texture* dispTex = nullptr;
318 readTexture(getParam(this, {"texture_displacementMap"}), dispTex);
319 if (dispTex) uniforms.flags |= (1u << 24);
320 }
321
322 // Pack per-texture UV transforms into 3×2 affine matrices.
323 packTransform(_baseColorTransform, uniforms.baseColorTransform0, uniforms.baseColorTransform1);
324 packTransform(_normalTransform, uniforms.normalTransform0, uniforms.normalTransform1);
325 packTransform(_metalRoughTransform, uniforms.metalRoughTransform0, uniforms.metalRoughTransform1);
326 packTransform(_occlusionTransform, uniforms.occlusionTransform0, uniforms.occlusionTransform1);
327 packTransform(_emissiveTransform, uniforms.emissiveTransform0, uniforms.emissiveTransform1);
328 }
329
330 void Material::getTextureSlots(std::vector<TextureSlot>& slots) const
331 {
332 slots.clear();
333
334 // Resolve textures from typed properties, with parameter overrides.
335 Texture* baseColorTex = _baseColorTexture;
336 readTexture(getParam(this, {"texture_baseColorMap", "texture_diffuseMap", "baseColorTexture"}), baseColorTex);
337 if (baseColorTex) slots.push_back({0, baseColorTex});
338
339 Texture* normalTex = _normalTexture;
340 readTexture(getParam(this, {"texture_normalMap", "normalTexture"}), normalTex);
341 if (normalTex) slots.push_back({1, normalTex});
342
343 Texture* mrTex = _metallicRoughnessTexture;
344 readTexture(getParam(this, {"texture_metallicRoughnessMap", "metallicRoughnessTexture"}), mrTex);
345 if (mrTex) slots.push_back({3, mrTex});
346
347 Texture* occlusionTex = _occlusionTexture;
348 readTexture(getParam(this, {"texture_occlusionMap", "occlusionTexture"}), occlusionTex);
349 if (occlusionTex) slots.push_back({4, occlusionTex});
350
351 Texture* emissiveTex = _emissiveTexture;
352 readTexture(getParam(this, {"texture_emissiveMap", "emissiveTexture"}), emissiveTex);
353 if (emissiveTex) slots.push_back({5, emissiveTex});
354 }
355
356 uint64_t Material::sortKey() const
357 {
358 const auto blendKey = static_cast<uint64_t>(_blendState ? _blendState->key() : 0);
359 const auto depthKey = static_cast<uint64_t>(_depthState ? _depthState->key() : 0);
360 const auto alphaModeKey = static_cast<uint64_t>(_alphaMode);
361 const auto baseTextureBit = _hasBaseColorTexture ? 1ull : 0ull;
362 const auto normalTextureBit = _hasNormalTexture ? 1ull : 0ull;
363 const auto mrTextureBit = _hasMetallicRoughnessTexture ? 1ull : 0ull;
364 const auto occlusionTextureBit = _hasOcclusionTexture ? 1ull : 0ull;
365 const auto emissiveTextureBit = _hasEmissiveTexture ? 1ull : 0ull;
366 const auto skyboxBit = _isSkybox ? 1ull : 0ull;
367 return (_shaderVariantKey << 32) ^ (blendKey << 16) ^ (depthKey << 4) ^ (alphaModeKey << 3) ^
368 (skyboxBit << 5) ^ (emissiveTextureBit << 4) ^ (occlusionTextureBit << 3) ^
369 (mrTextureBit << 2) ^ (normalTextureBit << 1) ^ baseTextureBit;
370 }
371
372 void setDefaultMaterial(const std::shared_ptr<GraphicsDevice>& device, const std::shared_ptr<Material>& material) {
373 assert(material != nullptr && "Cannot set null as default material");
374
375 defaultMaterials[device.get()] = material;
376
377 defaultMaterialDeviceCache.get<Material>(device, [material] {
378 return material;
379 });
380 }
381
382 std::shared_ptr<Material> getDefaultMaterial(const std::shared_ptr<GraphicsDevice>& device)
383 {
384 const auto it = defaultMaterials.find(device.get());
385 return it != defaultMaterials.end() ? it->second : nullptr;
386 }
387}
Base class for GPU materials — owns uniform data, texture bindings, blend/depth state,...
Definition material.h:143
virtual void getTextureSlots(std::vector< TextureSlot > &slots) const
Definition material.cpp:330
std::variant< float, int32_t, uint32_t, bool, Color, Vector2, Vector3, Vector4, Matrix4, Texture * > ParameterValue
Definition material.h:145
virtual void updateUniforms(MaterialUniforms &uniforms) const
Definition material.cpp:182
uint64_t sortKey() const
Definition material.cpp:356
bool removeParameter(const std::string &name)
Definition material.cpp:163
const std::string & name() const
Definition material.h:151
void setParameter(const std::string &name, const ParameterValue &value)
Definition material.cpp:155
bool occludeDirect() const
Definition material.h:214
const ParameterValue * parameter(const std::string &name) const
Definition material.cpp:176
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
constexpr uint32_t SPECOCC_GLOSSDEPENDENT
Definition constants.h:38
std::shared_ptr< Material > getDefaultMaterial(const std::shared_ptr< GraphicsDevice > &device)
Definition material.cpp:382
constexpr uint32_t SPECOCC_NONE
Definition constants.h:36
void setDefaultMaterial(const std::shared_ptr< GraphicsDevice > &device, const std::shared_ptr< Material > &material)
Definition material.cpp:372
std::unordered_map< GraphicsDevice *, std::shared_ptr< Material > > defaultMaterials
Definition material.cpp:21
constexpr float DEG_TO_RAD
Definition defines.h:49
DeviceCache defaultMaterialDeviceCache
Definition material.cpp:20