feat: add simple transparency

This commit is contained in:
2026-10-24 00:36:58 +02:00
parent 0b7cc38543
commit b798ed0378
13 changed files with 488 additions and 18 deletions
@@ -29,6 +29,11 @@ enum class TextureFilteringMode : std::uint32_t {
Anisotropic, Anisotropic,
}; };
enum class BlendMode : std::uint32_t {
Opaque,
Transparent,
};
// Metallic / roughness workflow, matching glTF: // Metallic / roughness workflow, matching glTF:
// metallic is read from the blue channel, roughness from green and occlusion from red, // metallic is read from the blue channel, roughness from green and occlusion from red,
// so a packed (occlusion)metallicRoughness texture can be used for all three slots. // so a packed (occlusion)metallicRoughness texture can be used for all three slots.
@@ -59,6 +64,9 @@ struct Material {
float normalScale{1.f}; float normalScale{1.f};
float occlusionStrength{1.f}; float occlusionStrength{1.f};
BlendMode blendMode{BlendMode::Opaque};
float opacity{1.f};
TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear}; TextureFilteringMode textureFilteringMode{TextureFilteringMode::Linear};
ImageID diffuseTexture{NULL_IMAGE_ID}; ImageID diffuseTexture{NULL_IMAGE_ID};
@@ -44,6 +44,14 @@ public:
const IndirectDrawList& drawList, const IndirectDrawList& drawList,
const IndirectDrawList::View& view); const IndirectDrawList::View& view);
void drawTransparent(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const IndirectDrawList& drawList,
const IndirectDrawList::View& view);
void cleanup(VkDevice device); void cleanup(VkDevice device);
void setRenderWireframe(bool wireframe) { void setRenderWireframe(bool wireframe) {
@@ -72,6 +80,7 @@ private:
private: private:
VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE}; VkPipelineLayout m_pipelineLayout{VK_NULL_HANDLE};
std::unique_ptr<Pipeline> m_pipeline; std::unique_ptr<Pipeline> m_pipeline;
std::unique_ptr<Pipeline> m_transparentPipeline;
std::unique_ptr<Pipeline> m_depthPrepassPipeline; std::unique_ptr<Pipeline> m_depthPrepassPipeline;
std::unique_ptr<Pipeline> m_selectionPrepassPipeline; std::unique_ptr<Pipeline> m_selectionPrepassPipeline;
@@ -183,6 +183,7 @@ private:
std::vector<MeshDrawCommand> meshDrawCommands; std::vector<MeshDrawCommand> meshDrawCommands;
std::vector<std::size_t> sortedMeshDrawCommands; std::vector<std::size_t> sortedMeshDrawCommands;
std::vector<std::size_t> transparentMeshDrawCommands;
IndirectDrawList drawList; IndirectDrawList drawList;
DrawStats lastDrawStats; DrawStats lastDrawStats;
@@ -414,6 +414,16 @@ void MeshRendererComponent::DrawMaterialEditor(RenderResources& resources, GameR
changed = finished = true; changed = finished = true;
} }
constexpr std::array<const char*, 2> blendModeNames{"Opaque", "Transparent"};
int blendMode = static_cast<int>(material.blendMode);
if (ImGui::Combo("Blend", &blendMode, blendModeNames.data(), static_cast<int>(blendModeNames.size()))) {
material.blendMode = static_cast<BlendMode>(blendMode);
changed = finished = true;
}
if (material.blendMode == BlendMode::Transparent) {
track(ImGui::SliderFloat("Opacity", &material.opacity, 0.0f, 1.0f));
}
ImGui::SeparatorText("Textures"); ImGui::SeparatorText("Textures");
for (const TextureSlot& slot : TextureSlots) { for (const TextureSlot& slot : TextureSlots) {
ImGui::PushID(slot.label); ImGui::PushID(slot.label);
@@ -184,7 +184,7 @@ MaterialData MaterialCache::toMaterialData(const Material& material) const
}; };
return MaterialData{ return MaterialData{
.baseColor = glm::vec4(material.baseColor, 1.0f), .baseColor = glm::vec4(material.baseColor, material.blendMode == BlendMode::Transparent ? material.opacity : 1.0f),
.emissiveColor = glm::vec4(material.emissiveColor, 0.0f), .emissiveColor = glm::vec4(material.emissiveColor, 0.0f),
.metallicRoughnessNormalOcclusion = glm::vec4{ .metallicRoughnessNormalOcclusion = glm::vec4{
@@ -97,16 +97,10 @@ IndirectDrawList::View IndirectDrawList::addView(
++view.instanceCount; ++view.instanceCount;
}; };
if (!sortedDrawCommands.empty()) {
for (std::size_t drawIndex : sortedDrawCommands) { for (std::size_t drawIndex : sortedDrawCommands) {
assert(drawIndex < drawCommands.size()); assert(drawIndex < drawCommands.size());
addDraw(drawCommands[drawIndex]); addDraw(drawCommands[drawIndex]);
} }
} else {
for (const MeshDrawCommand& dc : drawCommands) {
addDraw(dc);
}
}
return view; return view;
} }
@@ -131,6 +131,36 @@ void MeshPipeline::init(
pipelineConfig pipelineConfig
); );
} }
{
PipelineConfigInfo pipelineConfig{};
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
pipelineConfig.name = "Mesh Transparent Pipeline";
pipelineConfig.pipelineLayout = m_pipelineLayout;
pipelineConfig.vertexAttributeDescriptions = {};
pipelineConfig.vertexBindingDescriptions = {};
pipelineConfig.colorAttachments = {drawImageFormat};
pipelineConfig.depthAttachment = depthImageFormat;
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
pipelineConfig.colorBlendAttachment.blendEnable = VK_TRUE;
pipelineConfig.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
pipelineConfig.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
pipelineConfig.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
m_transparentPipeline = std::make_unique<Pipeline>(
gfxDevice,
vertexShader.string(),
fragShader.string(),
pipelineConfig
);
}
} }
catch (...) catch (...)
{ {
@@ -187,6 +217,29 @@ void MeshPipeline::draw(
view); view);
} }
void MeshPipeline::drawTransparent(
VkCommandBuffer cmd,
VkExtent2D renderExtent,
const RenderResources& resources,
const GPUBuffer& sceneDataBuffer,
const IndirectDrawList& drawList,
const IndirectDrawList::View& view)
{
if (!m_transparentPipeline || view.commandCount == 0)
{
return;
}
drawMeshes(
cmd,
*m_transparentPipeline,
renderExtent,
resources,
sceneDataBuffer,
drawList,
view);
}
void MeshPipeline::drawMeshes( void MeshPipeline::drawMeshes(
VkCommandBuffer cmd, VkCommandBuffer cmd,
const Pipeline& pipeline, const Pipeline& pipeline,
@@ -254,6 +307,7 @@ void MeshPipeline::drawMeshes(
void MeshPipeline::cleanup(VkDevice device) void MeshPipeline::cleanup(VkDevice device)
{ {
m_pipeline.reset(); m_pipeline.reset();
m_transparentPipeline.reset();
m_depthPrepassPipeline.reset(); m_depthPrepassPipeline.reset();
m_selectionPrepassPipeline.reset(); m_selectionPrepassPipeline.reset();
+51 -3
View File
@@ -101,13 +101,24 @@ void GameRenderer::endDrawing()
// Grouping by mesh lets IndirectDrawList merge each run into one instanced draw. // Grouping by mesh lets IndirectDrawList merge each run into one instanced draw.
// Material and vertex buffer are per instance, so they don't break a batch. // Material and vertex buffer are per instance, so they don't break a batch.
std::stable_sort( std::ranges::stable_sort(sortedMeshDrawCommands
sortedMeshDrawCommands.begin(), ,
sortedMeshDrawCommands.end(),
[this](std::size_t left, std::size_t right) [this](std::size_t left, std::size_t right)
{ {
return meshDrawCommands[left].meshId < meshDrawCommands[right].meshId; return meshDrawCommands[left].meshId < meshDrawCommands[right].meshId;
}); });
const auto& materials = resources->materials();
transparentMeshDrawCommands.clear();
std::erase_if(sortedMeshDrawCommands, [&](std::size_t index)
{
if (materials.getMaterial(meshDrawCommands[index].materialId).blendMode != BlendMode::Transparent)
{
return false;
}
transparentMeshDrawCommands.push_back(index);
return true;
});
} }
void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData) void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData)
@@ -161,7 +172,23 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
// Culling stays on the CPU: every view (camera, directional light, each spot light) // Culling stays on the CPU: every view (camera, directional light, each spot light)
// gets its own range of indirect commands in this frame's draw list. // gets its own range of indirect commands in this frame's draw list.
{
ZoneScopedN("Sort Transparent Draws");
const glm::vec3 cameraPos = camera.GetPosition();
const auto distanceSq = [&](std::size_t index)
{
const glm::vec3 offset = meshDrawCommands[index].worldBoundingSphere.center - cameraPos;
return glm::dot(offset, offset);
};
std::ranges::sort(transparentMeshDrawCommands, [&](std::size_t left, std::size_t right)
{
return distanceSq(left) > distanceSq(right);
});
}
IndirectDrawList::View mainView; IndirectDrawList::View mainView;
IndirectDrawList::View transparentView;
IndirectDrawList::View directionalShadowView; IndirectDrawList::View directionalShadowView;
std::vector<IndirectDrawList::View> spotShadowViews; std::vector<IndirectDrawList::View> spotShadowViews;
{ {
@@ -177,6 +204,13 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
edge::createFrustumFromCamera(camera), edge::createFrustumFromCamera(camera),
IndirectDrawList::Filter::All); IndirectDrawList::Filter::All);
transparentView = drawList.addView(
meshes,
meshDrawCommands,
transparentMeshDrawCommands,
edge::createFrustumFromCamera(camera),
IndirectDrawList::Filter::All);
if (shadowLightDirection) if (shadowLightDirection)
{ {
directionalShadowView = drawList.addView( directionalShadowView = drawList.addView(
@@ -416,6 +450,19 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
skyboxPipeline->draw(cmd, *resources, camera, sceneData.skyboxRotation); skyboxPipeline->draw(cmd, *resources, camera, sceneData.skyboxRotation);
} }
{
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "MeshPipeline::drawTransparent");
vkutil::ScopedCmdLabel label(cmd, "MeshPipeline::drawTransparent", vkutil::labelColors::opaque);
meshPipeline->drawTransparent(
cmd,
drawImage.getExtent2D(),
*resources,
sceneDataBuffer.getBuffer(),
drawList,
transparentView);
}
{ {
TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdEndRendering"); TracyVkZone(gfxDevice.getTracyVkCtx(), cmd, "vkCmdEndRendering");
@@ -600,6 +647,7 @@ void GameRenderer::cleanup(GfxDevice& gfxDevice)
pendingMaterialUploads.clear(); pendingMaterialUploads.clear();
meshDrawCommands.clear(); meshDrawCommands.clear();
sortedMeshDrawCommands.clear(); sortedMeshDrawCommands.clear();
transparentMeshDrawCommands.clear();
initialized = false; initialized = false;
GameState::GetInstance().SetRenderer(nullptr); GameState::GetInstance().SetRenderer(nullptr);
} }
@@ -43,6 +43,18 @@ namespace {
return TextureFilteringMode::Linear; return TextureFilteringMode::Linear;
} }
constexpr std::array<std::string_view, 2> BlendModeNames{"opaque", "transparent"};
[[nodiscard]] BlendMode ParseBlendMode(std::string_view name)
{
for (std::size_t i = 0; i < BlendModeNames.size(); ++i) {
if (BlendModeNames[i] == name) {
return static_cast<BlendMode>(i);
}
}
return BlendMode::Opaque;
}
[[nodiscard]] RenderResources* GetResources() [[nodiscard]] RenderResources* GetResources()
{ {
auto& state = GameState::GetInstance(); auto& state = GameState::GetInstance();
@@ -110,6 +122,8 @@ namespace MaterialSerialization {
{"normalScale", material.normalScale}, {"normalScale", material.normalScale},
{"occlusionStrength", material.occlusionStrength}, {"occlusionStrength", material.occlusionStrength},
{"filtering", FilteringNames.at(static_cast<std::size_t>(material.textureFilteringMode))}, {"filtering", FilteringNames.at(static_cast<std::size_t>(material.textureFilteringMode))},
{"blend", BlendModeNames.at(static_cast<std::size_t>(material.blendMode))},
{"opacity", material.opacity},
}; };
json textures = json::object(); json textures = json::object();
@@ -138,6 +152,8 @@ namespace MaterialSerialization {
material.normalScale = data.value("normalScale", material.normalScale); material.normalScale = data.value("normalScale", material.normalScale);
material.occlusionStrength = data.value("occlusionStrength", material.occlusionStrength); material.occlusionStrength = data.value("occlusionStrength", material.occlusionStrength);
material.textureFilteringMode = ParseFiltering(data.value("filtering", std::string{"linear"})); material.textureFilteringMode = ParseFiltering(data.value("filtering", std::string{"linear"}));
material.blendMode = ParseBlendMode(data.value("blend", std::string{"opaque"}));
material.opacity = data.value("opacity", material.opacity);
if (data.contains("textures")) { if (data.contains("textures")) {
const json& textures = data.at("textures"); const json& textures = data.at("textures");
Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

+3
View File
@@ -0,0 +1,3 @@
{
"guid": "a6c64648ac15e99110de13faea6b80e1"
}
+325 -1
View File
@@ -24,6 +24,7 @@
1.0, 1.0,
1.0 1.0
], ],
"blend": "opaque",
"emissiveColor": [ "emissiveColor": [
0.0, 0.0,
0.0, 0.0,
@@ -34,6 +35,7 @@
"name": "material_0", "name": "material_0",
"normalScale": 1.0, "normalScale": 1.0,
"occlusionStrength": 1.0, "occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 0.699999988079071, "roughness": 0.699999988079071,
"textures": { "textures": {
"diffuse": { "diffuse": {
@@ -43,12 +45,40 @@
} }
} }
}, },
"10": {
"baseColor": [
1.0,
1.0,
1.0
],
"blend": "transparent",
"emissiveColor": [
0.0,
0.0,
0.0
],
"filtering": "linear",
"metallic": 0.0,
"name": "Material",
"normalScale": 1.0,
"occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 1.0,
"textures": {
"diffuse": {
"guid": "a6c64648ac15e99110de13faea6b80e1",
"path": "game://kobe.png",
"srgb": true
}
}
},
"2": { "2": {
"baseColor": [ "baseColor": [
0.8999999761581421, 0.8999999761581421,
0.10000000149011612, 0.10000000149011612,
0.10000000149011612 0.10000000149011612
], ],
"blend": "opaque",
"emissiveColor": [ "emissiveColor": [
0.35999998450279236, 0.35999998450279236,
0.04000000283122063, 0.04000000283122063,
@@ -59,6 +89,7 @@
"name": "ButtonMaterial", "name": "ButtonMaterial",
"normalScale": 1.0, "normalScale": 1.0,
"occlusionStrength": 1.0, "occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 0.4000000059604645 "roughness": 0.4000000059604645
}, },
"3": { "3": {
@@ -67,6 +98,7 @@
0.949999988079071, 0.949999988079071,
0.8500000238418579 0.8500000238418579
], ],
"blend": "opaque",
"emissiveColor": [ "emissiveColor": [
1.0, 1.0,
0.8199999928474426, 0.8199999928474426,
@@ -77,6 +109,7 @@
"name": "BulbMaterial", "name": "BulbMaterial",
"normalScale": 1.0, "normalScale": 1.0,
"occlusionStrength": 1.0, "occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 1.0 "roughness": 1.0
}, },
"4": { "4": {
@@ -85,6 +118,7 @@
0.8999999761581421, 0.8999999761581421,
0.8999999761581421 0.8999999761581421
], ],
"blend": "opaque",
"emissiveColor": [ "emissiveColor": [
0.0, 0.0,
0.0, 0.0,
@@ -95,6 +129,7 @@
"name": "LightSwitchMaterial", "name": "LightSwitchMaterial",
"normalScale": 1.0, "normalScale": 1.0,
"occlusionStrength": 1.0, "occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 0.5 "roughness": 0.5
}, },
"5": { "5": {
@@ -103,6 +138,7 @@
0.10000000149011612, 0.10000000149011612,
0.10000000149011612 0.10000000149011612
], ],
"blend": "opaque",
"emissiveColor": [ "emissiveColor": [
0.35999998450279236, 0.35999998450279236,
0.04000000283122063, 0.04000000283122063,
@@ -113,7 +149,116 @@
"name": "WinButtonMaterial", "name": "WinButtonMaterial",
"normalScale": 1.0, "normalScale": 1.0,
"occlusionStrength": 1.0, "occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 0.4000000059604645 "roughness": 0.4000000059604645
},
"6": {
"baseColor": [
1.0,
1.0,
1.0
],
"blend": "transparent",
"emissiveColor": [
0.0,
0.0,
0.0
],
"filtering": "linear",
"metallic": 0.0,
"name": "Material",
"normalScale": 1.0,
"occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 1.0,
"textures": {
"diffuse": {
"guid": "a6c64648ac15e99110de13faea6b80e1",
"path": "game://kobe.png",
"srgb": true
}
}
},
"7": {
"baseColor": [
1.0,
1.0,
1.0
],
"blend": "transparent",
"emissiveColor": [
0.0,
0.0,
0.0
],
"filtering": "linear",
"metallic": 0.0,
"name": "Material",
"normalScale": 1.0,
"occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 1.0,
"textures": {
"diffuse": {
"guid": "a6c64648ac15e99110de13faea6b80e1",
"path": "game://kobe.png",
"srgb": true
}
}
},
"8": {
"baseColor": [
1.0,
1.0,
1.0
],
"blend": "transparent",
"emissiveColor": [
0.0,
0.0,
0.0
],
"filtering": "linear",
"metallic": 0.0,
"name": "Material",
"normalScale": 1.0,
"occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 1.0,
"textures": {
"diffuse": {
"guid": "a6c64648ac15e99110de13faea6b80e1",
"path": "game://kobe.png",
"srgb": true
}
}
},
"9": {
"baseColor": [
1.0,
1.0,
1.0
],
"blend": "transparent",
"emissiveColor": [
0.0,
0.0,
0.0
],
"filtering": "linear",
"metallic": 0.0,
"name": "Material",
"normalScale": 1.0,
"occlusionStrength": 1.0,
"opacity": 1.0,
"roughness": 1.0,
"textures": {
"diffuse": {
"guid": "a6c64648ac15e99110de13faea6b80e1",
"path": "game://kobe.png",
"srgb": true
}
}
} }
}, },
"name": "Main", "name": "Main",
@@ -7571,7 +7716,6 @@
{ {
"data": { "data": {
"clip": { "clip": {
"guid": "b21bcb92ef324239e14c407ad8b9ff39",
"path": "game://MontyOnTheRun.wav" "path": "game://MontyOnTheRun.wav"
}, },
"loop": false, "loop": false,
@@ -7607,6 +7751,186 @@
1.0 1.0
] ]
} }
},
{
"active": true,
"components": [
{
"data": {
"material": {
"localMaterial": "6"
},
"mesh": "prim://plane?size=1,1"
},
"enabled": true,
"type": "MeshRendererComponent"
}
],
"id": 121,
"name": "Decal",
"parent": 0,
"transform": {
"position": [
-4.743542194366455,
0.3649294674396515,
0.07782143354415894
],
"rotation": [
0.5,
0.5,
-0.5,
0.5
],
"scale": [
1.0,
0.9999995231628418,
0.9999995827674866
]
}
},
{
"active": true,
"components": [
{
"data": {
"material": {
"localMaterial": "7"
},
"mesh": "prim://plane?size=1,1"
},
"enabled": true,
"type": "MeshRendererComponent"
}
],
"id": 122,
"name": "Decal",
"parent": 0,
"transform": {
"position": [
-4.743542194366455,
1.1351869106292725,
2.188525676727295
],
"rotation": [
0.5,
0.5,
-0.5,
0.5
],
"scale": [
1.0,
0.9999995231628418,
0.9999995827674866
]
}
},
{
"active": true,
"components": [
{
"data": {
"material": {
"localMaterial": "8"
},
"mesh": "prim://plane?size=1,1"
},
"enabled": true,
"type": "MeshRendererComponent"
}
],
"id": 123,
"name": "Decal",
"parent": 0,
"transform": {
"position": [
-4.743542194366455,
1.5165855884552002,
0.601291835308075
],
"rotation": [
0.5,
0.5,
-0.5,
0.5
],
"scale": [
1.0,
0.9999995231628418,
0.9999995827674866
]
}
},
{
"active": true,
"components": [
{
"data": {
"material": {
"localMaterial": "9"
},
"mesh": "prim://plane?size=1,1"
},
"enabled": true,
"type": "MeshRendererComponent"
}
],
"id": 124,
"name": "Decal",
"parent": 0,
"transform": {
"position": [
-4.743542194366455,
0.5876348614692688,
-1.545495867729187
],
"rotation": [
0.5,
0.5,
-0.5,
0.5
],
"scale": [
1.0,
0.9999995231628418,
0.9999995827674866
]
}
},
{
"active": true,
"components": [
{
"data": {
"material": {
"localMaterial": "10"
},
"mesh": "prim://plane?size=1,1"
},
"enabled": true,
"type": "MeshRendererComponent"
}
],
"id": 125,
"name": "Decal",
"parent": 0,
"transform": {
"position": [
-4.743542194366455,
1.3765578269958496,
-2.816664457321167
],
"rotation": [
0.5,
0.5,
-0.5,
0.5
],
"scale": [
1.0,
0.9999995231628418,
0.9999995827674866
]
}
} }
], ],
"version": 3 "version": 3
+4 -1
View File
@@ -293,6 +293,8 @@ namespace {
material.roughnessFactor = 0.55f; material.roughnessFactor = 0.55f;
material.emissiveColor = {1.0f, 0.5f, 0.0f}; material.emissiveColor = {1.0f, 0.5f, 0.0f};
material.textureFilteringMode = TextureFilteringMode::Anisotropic; material.textureFilteringMode = TextureFilteringMode::Anisotropic;
material.blendMode = BlendMode::Transparent;
material.opacity = 0.25f;
material.diffuseTexture = 3; material.diffuseTexture = 3;
material.emissiveTexture = 7; material.emissiveTexture = 7;
material.normalTexture = 9; material.normalTexture = 9;
@@ -315,7 +317,8 @@ namespace {
check(loaded.name == "Crate" && loaded.baseColor == material.baseColor && check(loaded.name == "Crate" && loaded.baseColor == material.baseColor &&
close(loaded.metallicFactor, 0.3f) && close(loaded.roughnessFactor, 0.55f) && close(loaded.metallicFactor, 0.3f) && close(loaded.roughnessFactor, 0.55f) &&
loaded.emissiveColor == material.emissiveColor && loaded.emissiveColor == material.emissiveColor &&
loaded.textureFilteringMode == TextureFilteringMode::Anisotropic, loaded.textureFilteringMode == TextureFilteringMode::Anisotropic &&
loaded.blendMode == BlendMode::Transparent && close(loaded.opacity, 0.25f),
"material factors must round-trip through json"); "material factors must round-trip through json");
check(loaded.diffuseTexture == 30 && loaded.emissiveTexture == 70 && check(loaded.diffuseTexture == 30 && loaded.emissiveTexture == 70 &&
loaded.normalTexture == NULL_IMAGE_ID && loaded.metallicTexture == NULL_IMAGE_ID, loaded.normalTexture == NULL_IMAGE_ID && loaded.metallicTexture == NULL_IMAGE_ID,