render/shaders.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | #include "chrono_batch_render/render/shaders.h" | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <cstring> | ||
| 6 | #include <fstream> | ||
| 7 | #include <iterator> | ||
| 8 | #include <map> | ||
| 9 | #include <mutex> | ||
| 10 | |||
| 11 | #include "chrono_batch_render/device/slang/reflection.h" | ||
| 12 | #include "chrono_batch_render/material/codegen.h" | ||
| 13 | #include "chrono_batch_render/material/node.h" | ||
| 14 | #include "chrono_batch_render/render/kernels.h" | ||
| 15 | #include "chrono_batch_render/scene/scene_impl.h" | ||
| 16 | #include "chrono_batch_render/sensor_pipeline/node.h" | ||
| 17 | #include "chrono_batch_render/stage/schema/runtime.h" | ||
| 18 | #include "chrono_batch_render/util/disk_cache.h" | ||
| 19 | #include "chrono_batch_render/util/error.h" | ||
| 20 | #include "chrono_batch_render/util/log.h" | ||
| 21 | #include "chrono_batch_render/util/text.h" | ||
| 22 | |||
| 23 | namespace chrono::batch_render { | ||
| 24 | |||
| 25 | namespace { | ||
| 26 | |||
| 27 | 56 | template <class T> T Read(const std::vector<std::byte>& bytes) { | |
| 28 | 56 | T value{}; | |
| 29 | 56 | std::memcpy(&value, bytes.data(), std::min(bytes.size(), sizeof(T))); | |
| 30 | 56 | return value; | |
| 31 | } | ||
| 32 | |||
| 33 | 48 | RuntimeColumnDesc Column(const device::ReflectedField& field) { | |
| 34 | 48 | RuntimeColumnDesc column{.field = field.name, | |
| 35 | 48 | .role = field.color ? ColumnRole::Color : ColumnRole::None, | |
| 36 | 48 | .displayName = field.displayName, | |
| 37 | .group = "Inputs", | ||
| 38 | 48 | .minimum = field.minimum, | |
| 39 | 96 | .maximum = field.maximum}; | |
| 40 | 48 | switch (field.type) { | |
| 41 | 8 | case device::FieldType::Bool: | |
| 42 | 8 | column.type = ColumnType::Bool; | |
| 43 | 8 | column.fallback = Read<uint32_t>(field.fallback) != 0; | |
| 44 | 8 | break; | |
| 45 | 4 | case device::FieldType::Int: | |
| 46 | 4 | column.type = ColumnType::Int; | |
| 47 | 4 | column.fallback = Read<int32_t>(field.fallback); | |
| 48 | 4 | break; | |
| 49 | 20 | case device::FieldType::Float: | |
| 50 | 20 | column.type = ColumnType::Float; | |
| 51 | 20 | column.fallback = Read<float>(field.fallback); | |
| 52 | 20 | break; | |
| 53 | ✗ | case device::FieldType::Float2: | |
| 54 | ✗ | column.type = ColumnType::Float2; | |
| 55 | ✗ | column.fallback = Read<Float2>(field.fallback); | |
| 56 | ✗ | break; | |
| 57 | 15 | case device::FieldType::Float3: | |
| 58 | 15 | column.type = ColumnType::Float3; | |
| 59 | 15 | column.fallback = Read<Float3>(field.fallback); | |
| 60 | 15 | break; | |
| 61 | ✗ | case device::FieldType::Float4: | |
| 62 | ✗ | column.type = ColumnType::Float4; | |
| 63 | ✗ | column.fallback = Read<Float4>(field.fallback); | |
| 64 | ✗ | break; | |
| 65 | 1 | case device::FieldType::Spectral: | |
| 66 | 1 | column.type = ColumnType::Float3; | |
| 67 | 1 | column.fallback = Read<Float3>(field.fallback); | |
| 68 | 1 | break; | |
| 69 | } | ||
| 70 | 48 | return column; | |
| 71 | } | ||
| 72 | |||
| 73 | 50 | material::MaterialInputType InputType(device::FieldType type) { | |
| 74 | 50 | switch (type) { | |
| 75 | case device::FieldType::Bool: | ||
| 76 | return material::MaterialInputType::Bool; | ||
| 77 | ✗ | case device::FieldType::Int: | |
| 78 | ✗ | return material::MaterialInputType::Int; | |
| 79 | ✗ | case device::FieldType::Float2: | |
| 80 | ✗ | return material::MaterialInputType::Float2; | |
| 81 | 13 | case device::FieldType::Float3: | |
| 82 | 13 | return material::MaterialInputType::Float3; | |
| 83 | ✗ | case device::FieldType::Float4: | |
| 84 | ✗ | return material::MaterialInputType::Float4; | |
| 85 | 1 | case device::FieldType::Spectral: | |
| 86 | 1 | return material::MaterialInputType::Spectral; | |
| 87 | 32 | default: | |
| 88 | 32 | return material::MaterialInputType::Float; | |
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | 25 | struct LoadedFile { | |
| 93 | std::string digest; | ||
| 94 | std::vector<std::string> names; | ||
| 95 | }; | ||
| 96 | |||
| 97 | 3 | struct LoadedFiles { | |
| 98 | std::mutex mutex; | ||
| 99 | std::map<std::filesystem::path, LoadedFile> files; | ||
| 100 | }; | ||
| 101 | |||
| 102 | 118 | LoadedFiles& Loaded() { | |
| 103 | 118 | static LoadedFiles loaded; | |
| 104 | 118 | return loaded; | |
| 105 | } | ||
| 106 | |||
| 107 | 85 | std::string ReadSource(const std::filesystem::path& file, const std::filesystem::path& requested) { | |
| 108 | 85 | std::ifstream stream(file, std::ios::binary); | |
| 109 | 85 | if (!stream) { | |
| 110 | 4 | throw Error("Can't read shaders from {}", requested.string()); | |
| 111 | } | ||
| 112 | 166 | return std::string((std::istreambuf_iterator<char>(stream)), {}); | |
| 113 | 85 | } | |
| 114 | |||
| 115 | 5 | const bool kLoaderRegistered = [] { | |
| 116 | 5 | SetShaderLoader( | |
| 117 | 3 | [](Scene& scene, const std::filesystem::path& file) { return LoadShaders(scene, file); }); | |
| 118 | 5 | return true; | |
| 119 | }(); | ||
| 120 | |||
| 121 | } | ||
| 122 | |||
| 123 | 42 | std::vector<std::string> LoadShaders(Scene& scene, const std::filesystem::path& requested) { | |
| 124 | 42 | const auto sceneLock = SceneImpl::Get(scene).Lock(); | |
| 125 | 42 | const std::filesystem::path file = std::filesystem::weakly_canonical(requested); | |
| 126 | 42 | const std::string source = ReadSource(file, requested); | |
| 127 | 40 | const std::string digest = Digest(source); | |
| 128 | 40 | { | |
| 129 | 40 | LoadedFiles& loaded = Loaded(); | |
| 130 | 40 | const std::lock_guard lock(loaded.mutex); | |
| 131 | 40 | if (const auto found = loaded.files.find(file); | |
| 132 | 40 | found != loaded.files.end() && found->second.digest == digest) { | |
| 133 | 2 | return found->second.names; | |
| 134 | } | ||
| 135 | 2 | } | |
| 136 | 38 | const std::string module = "chrono_batch_render.nodes.n" + digest; | |
| 137 | 38 | const std::string path = ModulePath(module); | |
| 138 | 38 | const std::pair<std::string, std::string> origins[] = {{path, file.string()}, | |
| 139 | 304 | {module + ".slang", file.string()}}; | |
| 140 | 48 | const auto mapped = [&](const Error& error) { | |
| 141 | 20 | return Error("{}", MapOrigins(error.what(), origins)); | |
| 142 | 38 | }; | |
| 143 | 38 | const device::Backend backend = scene.GetDevice().Adapter().backend; | |
| 144 | 51 | auto sources = material::ClosurePrimitiveSources(material::ClosurePrimitives()); | |
| 145 | 38 | sources[path] = source; | |
| 146 | 38 | device::ReflectedNodes reflection; | |
| 147 | 38 | try { | |
| 148 | 114 | const DiskCache binaries(scene.GetDevice().KernelCacheDirectory() / "slang"); | |
| 149 | 38 | reflection = | |
| 150 | 76 | device::ReflectNodes(backend, &kRenderKernels, {sources.begin(), sources.end()}, module, | |
| 151 | 105 | file.string(), &binaries); | |
| 152 | 9 | } catch (const Error& error) { | |
| 153 | 9 | throw mapped(error); | |
| 154 | 9 | } | |
| 155 | 29 | if (!reflection.pipeline.empty() && !reflection.material.empty()) { | |
| 156 | 1 | throw Error("{} defines both pipeline nodes ({}) and material nodes ({}); put them in " | |
| 157 | "separate files", | ||
| 158 | 1 | file.string(), reflection.pipeline.front().name, | |
| 159 | 2 | reflection.material.front().name); | |
| 160 | } | ||
| 161 | 28 | if (!reflection.pipeline.empty()) { | |
| 162 | 8 | const std::vector<device::ReflectedSensorPipelineNode>& reflected = reflection.pipeline; | |
| 163 | 53 | const auto signal = [&](const std::string& node, const std::string& type) { | |
| 164 | 45 | const auto found = std::ranges::find(sensor_pipeline::kSignals, type, | |
| 165 | &sensor_pipeline::SignalInfo::slangType); | ||
| 166 | 45 | if (found == std::end(sensor_pipeline::kSignals)) { | |
| 167 | 1 | std::vector<std::string> types; | |
| 168 | 11 | for (const sensor_pipeline::SignalInfo& info : sensor_pipeline::kSignals) { | |
| 169 | 10 | types.emplace_back(info.slangType); | |
| 170 | } | ||
| 171 | 1 | throw Error("{} in {} uses {}, which isn't a pipeline signal; signals are {}", node, | |
| 172 | 2 | file.string(), type, Join(types)); | |
| 173 | 1 | } | |
| 174 | 44 | return sensor_pipeline::SignalType(found - std::begin(sensor_pipeline::kSignals)); | |
| 175 | 8 | }; | |
| 176 | 19 | for (const device::ReflectedSensorPipelineNode& node : reflected) { | |
| 177 | 12 | signal(node.name, node.result); | |
| 178 | 22 | for (const device::ReflectedPort& port : node.inputs) { | |
| 179 | 11 | signal(node.name, port.type); | |
| 180 | } | ||
| 181 | } | ||
| 182 | 7 | std::vector<std::string> names; | |
| 183 | 17 | for (const device::ReflectedSensorPipelineNode& node : reflected) { | |
| 184 | 11 | const sensor_pipeline::SignalType result = signal(node.name, node.result); | |
| 185 | 11 | sensor_pipeline::NodeDef def{ | |
| 186 | .id = node.name, | ||
| 187 | 11 | .kind = node.kind == device::SensorPipelineNodeKind::Stencil | |
| 188 | 11 | ? sensor_pipeline::NodeKind::Stencil | |
| 189 | : sensor_pipeline::NodeKind::Point, | ||
| 190 | 11 | .outputs = {{std::string(sensor_pipeline::Signal(result).port), result}}, | |
| 191 | 11 | .radius = node.radius, | |
| 192 | .code = {.module = module, | ||
| 193 | .type = node.name, | ||
| 194 | .file = file.string(), | ||
| 195 | .source = source, | ||
| 196 | .version = digest, | ||
| 197 | .applies = true, | ||
| 198 | 33 | .context = node.context}}; | |
| 199 | 22 | for (const device::ReflectedPort& port : node.inputs) { | |
| 200 | 22 | def.inputs.push_back({port.name, signal(node.name, port.type)}); | |
| 201 | } | ||
| 202 | 11 | RuntimeSchemaDesc desc{.name = node.name, | |
| 203 | .base = SchemaId::SensorNode, | ||
| 204 | 22 | .doc = "A sensor pipeline node from " + file.string() + ".", | |
| 205 | 33 | .inherited = {{"id", node.name}}}; | |
| 206 | 11 | std::vector<std::string> inputs; | |
| 207 | 29 | for (const device::ReflectedField& field : node.fields) { | |
| 208 | 18 | RuntimeColumnDesc column = Column(field); | |
| 209 | 18 | column.group = node.displayName.empty() ? node.name : node.displayName; | |
| 210 | 18 | desc.columns.push_back(std::move(column)); | |
| 211 | 18 | sensor_pipeline::ParameterDef parameter{.name = field.name}; | |
| 212 | 18 | switch (field.type) { | |
| 213 | 4 | case device::FieldType::Bool: | |
| 214 | 4 | parameter.type = sensor_pipeline::ParameterType::Bool; | |
| 215 | 8 | parameter.fallback = {Read<uint32_t>(field.fallback) != 0 ? 1.0f : 0.0f}; | |
| 216 | 4 | break; | |
| 217 | 4 | case device::FieldType::Int: | |
| 218 | 4 | parameter.type = sensor_pipeline::ParameterType::Int; | |
| 219 | 4 | parameter.fallback = {float(Read<int32_t>(field.fallback))}; | |
| 220 | 4 | break; | |
| 221 | 10 | default: | |
| 222 | 10 | parameter.words = uint32_t(field.fallback.size() / sizeof(float)); | |
| 223 | 10 | parameter.fallback.resize(parameter.words); | |
| 224 | 10 | std::memcpy(parameter.fallback.data(), field.fallback.data(), | |
| 225 | field.fallback.size()); | ||
| 226 | 10 | break; | |
| 227 | } | ||
| 228 | 18 | def.parameters.push_back(std::move(parameter)); | |
| 229 | 18 | inputs.push_back(field.name); | |
| 230 | 18 | } | |
| 231 | 11 | const std::shared_ptr<const sensor_pipeline::NodeRegistry> known = | |
| 232 | 12 | sensor_pipeline::SensorPipelineNodes(); | |
| 233 | 11 | const uint32_t found = known->Find(node.name); | |
| 234 | 11 | if (found != sensor_pipeline::kNoDef && (*known)[found].code.file.empty()) { | |
| 235 | 1 | throw Error("{} in {} has the name of a built-in pipeline node; rename it", | |
| 236 | 2 | node.name, file.string()); | |
| 237 | } | ||
| 238 | 10 | const SchemaId previous = FindSchemaByName(node.name); | |
| 239 | 10 | const bool reloading = found != sensor_pipeline::kNoDef && previous != SchemaId::None; | |
| 240 | 10 | const SchemaId schema = RegisterSchema(desc, reloading); | |
| 241 | 10 | if (reloading && previous != schema) { | |
| 242 | ✗ | Log(LogLevel::Info, | |
| 243 | "{} in {} changed its inputs, so its prims move to the new ones", node.name, | ||
| 244 | ✗ | file.string()); | |
| 245 | ✗ | RetypePrims(scene, previous, schema); | |
| 246 | } | ||
| 247 | 10 | sensor_pipeline::RegisterSensorPipelineNode(std::move(def)); | |
| 248 | 20 | Log(LogLevel::Debug, "loaded {} pipeline node {} from {} with inputs {}", | |
| 249 | 10 | node.kind == device::SensorPipelineNodeKind::Stencil ? "stencil" : "point", | |
| 250 | 31 | node.name, file.string(), inputs.empty() ? std::string("(none)") : Join(inputs)); | |
| 251 | 10 | names.push_back(node.name); | |
| 252 | 13 | } | |
| 253 | 6 | LoadedFiles& loaded = Loaded(); | |
| 254 | 6 | const std::lock_guard lock(loaded.mutex); | |
| 255 | 6 | loaded.files[file] = {digest, names}; | |
| 256 | 6 | return names; | |
| 257 | 7 | } | |
| 258 | 40 | std::vector<device::ReflectedNode> nodes = std::move(reflection.material); | |
| 259 | 19 | std::vector<std::string> names; | |
| 260 | 44 | const auto primitive = [](const device::ReflectedNode& node) { | |
| 261 | 24 | return node.role == device::NodeRole::Primitive; | |
| 262 | }; | ||
| 263 | 20 | if (std::ranges::any_of(nodes, primitive)) { | |
| 264 | 4 | if (!std::ranges::all_of(nodes, primitive) || | |
| 265 | 2 | source.find("chrono_batch_render.material") != std::string::npos) { | |
| 266 | ✗ | throw Error("{} defines a closure primitive, so it may only import " | |
| 267 | "chrono_batch_render.primitive " | ||
| 268 | "and define primitives", | ||
| 269 | ✗ | file.string()); | |
| 270 | } | ||
| 271 | 2 | material::ClosurePrimitiveList candidates = material::ClosurePrimitives(); | |
| 272 | 4 | for (const device::ReflectedNode& reflected : nodes) { | |
| 273 | 2 | candidates = | |
| 274 | 4 | material::WithClosurePrimitive(std::move(candidates), {.name = reflected.name, | |
| 275 | .file = file.string(), | ||
| 276 | .module = module, | ||
| 277 | 2 | .source = source}); | |
| 278 | } | ||
| 279 | 2 | const auto checked = material::ClosurePrimitiveSources(candidates); | |
| 280 | 2 | try { | |
| 281 | 6 | const DiskCache binaries(scene.GetDevice().KernelCacheDirectory() / "slang"); | |
| 282 | 3 | device::ReflectMaterialNodes(backend, &kRenderKernels, {checked.begin(), checked.end()}, | |
| 283 | "chrono_batch_render.render.kernels.material.registered", | ||
| 284 | 4 | file.string(), &binaries); | |
| 285 | 1 | } catch (const Error& error) { | |
| 286 | 1 | throw mapped(error); | |
| 287 | 1 | } | |
| 288 | 2 | for (const device::ReflectedNode& reflected : nodes) { | |
| 289 | 1 | material::RegisterClosurePrimitive({.name = reflected.name, | |
| 290 | .file = file.string(), | ||
| 291 | .module = module, | ||
| 292 | .source = source}); | ||
| 293 | 1 | const auto loaded = std::ranges::find(candidates, reflected.name, | |
| 294 | 1 | [](const auto& entry) { return entry->name; }); | |
| 295 | 1 | Log(LogLevel::Debug, "loaded closure primitive {} from {} as lobe kind {}", | |
| 296 | 2 | reflected.name, file.string(), (*loaded)->kind); | |
| 297 | 1 | names.push_back(reflected.name); | |
| 298 | } | ||
| 299 | 1 | nodes.clear(); | |
| 300 | 2 | } | |
| 301 | 39 | for (const device::ReflectedNode& reflected : nodes) { | |
| 302 | 20 | RuntimeSchemaDesc desc{ | |
| 303 | 20 | .name = reflected.name, | |
| 304 | .base = SchemaId::Shader, | ||
| 305 | 40 | .doc = "A material node from " + file.string() + ".", | |
| 306 | 20 | .inherited = {{"implementationSource", int32_t(ImplementationSource::SourceAsset)}, | |
| 307 | ✗ | {"slangSourceAsset", file.string()}, | |
| 308 | 140 | {"slangSourceAssetSubIdentifier", reflected.name}}}; | |
| 309 | 20 | material::MaterialNodeDefinition node{.name = reflected.name, | |
| 310 | 20 | .role = reflected.role == device::NodeRole::Pattern | |
| 311 | 20 | ? material::MaterialNodeRole::Pattern | |
| 312 | : reflected.role == device::NodeRole::Surface | ||
| 313 | 18 | ? material::MaterialNodeRole::Surface | |
| 314 | : material::MaterialNodeRole::Material, | ||
| 315 | .file = file.string(), | ||
| 316 | .module = module, | ||
| 317 | .source = source, | ||
| 318 | 40 | .result = InputType(reflected.result)}; | |
| 319 | 24 | for (const device::ReflectedParameter& parameter : reflected.parameters) { | |
| 320 | 4 | node.parameters.push_back( | |
| 321 | 4 | {.source = parameter.source == device::ParameterSource::Context | |
| 322 | 4 | ? material::MaterialNodeParameter::Source::Context | |
| 323 | : parameter.source == device::ParameterSource::Point | ||
| 324 | 3 | ? material::MaterialNodeParameter::Source::Point | |
| 325 | : material::MaterialNodeParameter::Source::Input, | ||
| 326 | 4 | .input = parameter.input}); | |
| 327 | } | ||
| 328 | 20 | std::vector<std::string> inputs; | |
| 329 | 50 | for (const device::ReflectedField& field : reflected.fields) { | |
| 330 | 60 | desc.columns.push_back(Column(field)); | |
| 331 | 30 | node.inputs.push_back({.name = field.name, .type = InputType(field.type)}); | |
| 332 | 30 | inputs.push_back(field.name); | |
| 333 | } | ||
| 334 | 20 | const std::shared_ptr<const material::MaterialNodeDefinition> previous = | |
| 335 | 20 | material::FindMaterialNode(reflected.name); | |
| 336 | 26 | const bool reloading = previous && previous->file == file.string(); | |
| 337 | 20 | const SchemaId schema = RegisterSchema(desc, reloading); | |
| 338 | 20 | if (reloading && SchemaId(previous->schema) != schema) { | |
| 339 | 2 | Log(LogLevel::Info, "{} in {} changed its inputs, so its prims move to the new ones", | |
| 340 | 2 | reflected.name, file.string()); | |
| 341 | 2 | RetypePrims(scene, SchemaId(previous->schema), schema); | |
| 342 | } | ||
| 343 | 20 | node.schema = uint32_t(schema); | |
| 344 | 20 | material::RegisterMaterialNode(std::move(node)); | |
| 345 | 40 | Log(LogLevel::Debug, "loaded {} {} from {} with inputs {}", | |
| 346 | 20 | reflected.role == device::NodeRole::Pattern ? "pattern" | |
| 347 | 18 | : reflected.role == device::NodeRole::Surface ? "surface" | |
| 348 | : "material node", | ||
| 349 | 60 | reflected.name, file.string(), inputs.empty() ? std::string("(none)") : Join(inputs)); | |
| 350 | 20 | names.push_back(reflected.name); | |
| 351 | 20 | } | |
| 352 | 19 | if (names.empty()) { | |
| 353 | 1 | Log(LogLevel::Warning, | |
| 354 | "{} has no nodes; a material node is a struct with a " | ||
| 355 | "Material(inout SurfaceInputs, ShadeContext) method or a function marked [Pattern], " | ||
| 356 | "and a pipeline node is a struct conforming to IPointNode or IStencilNode", | ||
| 357 | 3 | file.string()); | |
| 358 | } | ||
| 359 | 19 | { | |
| 360 | 19 | LoadedFiles& loaded = Loaded(); | |
| 361 | 19 | const std::lock_guard lock(loaded.mutex); | |
| 362 | 38 | loaded.files[file] = {digest, names}; | |
| 363 | ✗ | } | |
| 364 | 19 | return names; | |
| 365 | 419 | } | |
| 366 | |||
| 367 | 10 | std::vector<std::string> ReloadShaders(Scene& scene) { | |
| 368 | 10 | const auto sceneLock = SceneImpl::Get(scene).Lock(); | |
| 369 | 10 | std::vector<std::string> reloaded; | |
| 370 | 53 | for (const std::filesystem::path& file : LoadedShaderFiles()) { | |
| 371 | 43 | std::string digest; | |
| 372 | 43 | try { | |
| 373 | 43 | digest = Digest(ReadSource(file, file)); | |
| 374 | ✗ | } catch (const Error& error) { | |
| 375 | ✗ | Log(LogLevel::Warning, "{}; its nodes keep their last version", error.what()); | |
| 376 | ✗ | continue; | |
| 377 | ✗ | } | |
| 378 | 43 | { | |
| 379 | 43 | LoadedFiles& loaded = Loaded(); | |
| 380 | 43 | const std::lock_guard lock(loaded.mutex); | |
| 381 | 43 | if (loaded.files.at(file).digest == digest) { | |
| 382 | 36 | continue; | |
| 383 | } | ||
| 384 | 36 | } | |
| 385 | 7 | try { | |
| 386 | 7 | const std::vector<std::string> names = LoadShaders(scene, file); | |
| 387 | 5 | reloaded.insert(reloaded.end(), names.begin(), names.end()); | |
| 388 | 10 | Log(LogLevel::Info, "reloaded {} from {}", Join(names), file.string()); | |
| 389 | 7 | } catch (const Error& error) { | |
| 390 | 4 | Log(LogLevel::Error, "reloading {} failed, so its nodes keep their last version: {}", | |
| 391 | 2 | file.string(), error.what()); | |
| 392 | 2 | } | |
| 393 | 53 | } | |
| 394 | 10 | return reloaded; | |
| 395 | 10 | } | |
| 396 | |||
| 397 | ✗ | std::vector<SchemaId> SurfaceShaders() { | |
| 398 | ✗ | std::vector<SchemaId> surfaces = {SchemaId::UsdPreviewSurface, SchemaId::HapkeSurface, | |
| 399 | SchemaId::LommelSeeligerSurface, | ||
| 400 | ✗ | SchemaId::LunarLambertSurface, SchemaId::OrenNayarSurface}; | |
| 401 | ✗ | for (const auto& node : material::MaterialNodes()) { | |
| 402 | ✗ | if (node->role != material::MaterialNodeRole::Pattern) { | |
| 403 | ✗ | surfaces.push_back(SchemaId(node->schema)); | |
| 404 | } | ||
| 405 | ✗ | } | |
| 406 | ✗ | return surfaces; | |
| 407 | ✗ | } | |
| 408 | |||
| 409 | 10 | std::vector<std::filesystem::path> LoadedShaderFiles() { | |
| 410 | 10 | LoadedFiles& loaded = Loaded(); | |
| 411 | 10 | const std::lock_guard lock(loaded.mutex); | |
| 412 | 10 | std::vector<std::filesystem::path> files; | |
| 413 | 53 | for (const auto& [file, entry] : loaded.files) { | |
| 414 | 43 | files.push_back(file); | |
| 415 | } | ||
| 416 | 10 | return files; | |
| 417 | 10 | } | |
| 418 | |||
| 419 | } | ||
| 420 |