Bind shared lighting ABI and shade authored local lights
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Add lights to a 3D scene
|
||||||
|
|
||||||
|
Add a **Light** component to a scene entity. The entity's transform places a point
|
||||||
|
or spot light; its rotation aims a spot light along local negative Z. A directional
|
||||||
|
light uses the entity's orientation. Light colors and intensity contribute to the
|
||||||
|
mesh's linear PBR illumination before tone mapping. Sprites and UI retain their
|
||||||
|
unlit tint.
|
||||||
|
|
||||||
|
The version-1 `faset.light` component has three `kind` values:
|
||||||
|
|
||||||
|
| Kind | Position and direction | Useful fields |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `directional` | Direction from the entity transform | `color`, `intensity`, `casts_shadow` |
|
||||||
|
| `point` | Position from the entity transform; illuminates every direction | `color`, `intensity`, `range` |
|
||||||
|
| `spot` | Position and local negative-Z direction | `color`, `intensity`, `range`, `inner_angle`, `outer_angle` |
|
||||||
|
|
||||||
|
Angles are radians. A spot's inner angle must not exceed its outer angle. Intensity
|
||||||
|
must be nonnegative and range positive. `enabled: false` keeps the component in the
|
||||||
|
scene without contributing light. The `shadow_priority` integer is reserved for the
|
||||||
|
bounded local-shadow scheduler; it does not change brightness.
|
||||||
|
|
||||||
|
In the current rendering checkpoint, one enabled directional light can cast the
|
||||||
|
existing single-map shadow. Point and spot lights illuminate meshes but do not yet
|
||||||
|
cast shadows. The [P3 lighting plan](https://github.com/emil28092005/Faset_Engine/blob/main/docs/superpowers/plans/2026-09-24-p3-lighting.md)
|
||||||
|
tracks cascades and the bounded local-shadow atlas. A scene with no Light component
|
||||||
|
keeps the legacy white sun so older projects retain their appearance. Adding any
|
||||||
|
Light component, even a disabled one, turns off that compatibility fallback. If
|
||||||
|
several directionals are enabled, Faset chooses the one with the smallest stable
|
||||||
|
entity ID and reports a diagnostic for the others.
|
||||||
|
|
||||||
|
## Author a point light through MCP
|
||||||
|
|
||||||
|
Use `faset_schema` to inspect the current field IDs, then send a `faset_scene_edit`
|
||||||
|
batch with the document ID, current revision, and target entity ID. For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "REPLACE_WITH_DOCUMENT_ID",
|
||||||
|
"revision": 4,
|
||||||
|
"idempotency_key": "add-red-point-light",
|
||||||
|
"operations": [{
|
||||||
|
"op": "component.add",
|
||||||
|
"entity": "REPLACE_WITH_ENTITY_ID",
|
||||||
|
"type": "faset.light",
|
||||||
|
"fields": {
|
||||||
|
"kind": "point",
|
||||||
|
"color": [1, 0.15, 0.1, 1],
|
||||||
|
"intensity": 8,
|
||||||
|
"range": 6
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Move the entity with its Transform component. `component.add` fills any omitted
|
||||||
|
light fields from the version-1 schema; use `component.set` for later edits. See
|
||||||
|
[MCP and command line](mcp.md) for revision and retry handling.
|
||||||
|
|
||||||
|
## Supply lights directly from C++
|
||||||
|
|
||||||
|
When building a `faset::render::Snapshot` yourself, set
|
||||||
|
`authored_lights_present` to suppress the compatibility sun in a local-only scene.
|
||||||
|
Provide a stable ID for each light so future shadow scheduling remains independent
|
||||||
|
of submission order.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
faset::render::Snapshot snapshot;
|
||||||
|
snapshot.authored_lights_present = true;
|
||||||
|
|
||||||
|
faset::render::LocalLight point;
|
||||||
|
point.kind = faset::render::LocalLight::Kind::Point;
|
||||||
|
point.stable_id = "level/torch";
|
||||||
|
point.position = {-2, 1.5f, 0};
|
||||||
|
point.color = {1, 0.3f, 0.1f, 1};
|
||||||
|
point.intensity = 8;
|
||||||
|
point.range = 6;
|
||||||
|
snapshot.local_lights.push_back(point);
|
||||||
|
|
||||||
|
faset::render::LocalLight spot;
|
||||||
|
spot.kind = faset::render::LocalLight::Kind::Spot;
|
||||||
|
spot.stable_id = "level/lamp";
|
||||||
|
spot.position = {2, 3, 0};
|
||||||
|
spot.direction = {0, -1, 0};
|
||||||
|
spot.inner_angle = 0.25f;
|
||||||
|
spot.outer_angle = 0.55f;
|
||||||
|
spot.intensity = 5;
|
||||||
|
spot.range = 9;
|
||||||
|
snapshot.local_lights.push_back(spot);
|
||||||
|
```
|
||||||
|
|
||||||
|
The renderer submits at most 128 local lights per frame in stable-ID order. Later
|
||||||
|
P3 work adds explicit overflow diagnostics and measured light-list optimization.
|
||||||
@@ -44,6 +44,7 @@ nav:
|
|||||||
- Editor workspace: editor/workspace.md
|
- Editor workspace: editor/workspace.md
|
||||||
- Scene templates: editor/templates.md
|
- Scene templates: editor/templates.md
|
||||||
- Assets and Blender: editor/assets.md
|
- Assets and Blender: editor/assets.md
|
||||||
|
- Lighting: editor/lighting.md
|
||||||
- GPU visibility and mesh LOD: editor/visibility-lod.md
|
- GPU visibility and mesh LOD: editor/visibility-lod.md
|
||||||
- Build, Play, and export: editor/export.md
|
- Build, Play, and export: editor/export.md
|
||||||
- Profiling and measurements: editor/profiling.md
|
- Profiling and measurements: editor/profiling.md
|
||||||
|
|||||||
+85
-19
@@ -24,6 +24,32 @@ struct FrameParameters {
|
|||||||
[[vk::binding(1,0)]] SamplerState shadowSampler;
|
[[vk::binding(1,0)]] SamplerState shadowSampler;
|
||||||
[[vk::binding(2,0)]] Texture2D<float4> colorMap;
|
[[vk::binding(2,0)]] Texture2D<float4> colorMap;
|
||||||
[[vk::binding(3,0)]] SamplerState colorSampler;
|
[[vk::binding(3,0)]] SamplerState colorSampler;
|
||||||
|
// Shared Direct/P2 graphics ABI. The legacy material set remains set 0;
|
||||||
|
// GPU-only instance/visibility records occupy set 2.
|
||||||
|
struct LightingHeader {
|
||||||
|
uint4 counts; // local count, sun enabled, sun shadow enabled, view count
|
||||||
|
float4 sunDirectionIntensity; // xyz world-space ray direction, w intensity
|
||||||
|
float4 sunColor;
|
||||||
|
float4 cameraForwardShadowDistance;
|
||||||
|
float4 cascadeSplits;
|
||||||
|
};
|
||||||
|
struct LocalLightGpu {
|
||||||
|
float4 positionRange;
|
||||||
|
float4 directionCosOuter;
|
||||||
|
float4 colorIntensity;
|
||||||
|
float4 coneTypeShadowView; // cos(inner), 0=point/1=spot, shadow view, flags
|
||||||
|
float4 reserved;
|
||||||
|
};
|
||||||
|
struct ShadowViewGpu {
|
||||||
|
column_major float4x4 viewProjection;
|
||||||
|
float4 tileScaleOffset;
|
||||||
|
float4 guardedClamp;
|
||||||
|
float4 biasFlags;
|
||||||
|
};
|
||||||
|
[[vk::binding(0,1)]] StructuredBuffer<LightingHeader> lightingFrame;
|
||||||
|
[[vk::binding(1,1)]] StructuredBuffer<LocalLightGpu> localLights;
|
||||||
|
[[vk::binding(2,1)]] StructuredBuffer<ShadowViewGpu> shadowViews;
|
||||||
|
[[vk::binding(3,1)]] Texture2D<float> localShadowAtlas;
|
||||||
[shader("vertex")]
|
[shader("vertex")]
|
||||||
VertexOutput vertexMain(VertexInput v) {
|
VertexOutput vertexMain(VertexInput v) {
|
||||||
VertexOutput o;
|
VertexOutput o;
|
||||||
@@ -32,6 +58,22 @@ VertexOutput vertexMain(VertexInput v) {
|
|||||||
}
|
}
|
||||||
[shader("vertex")]
|
[shader("vertex")]
|
||||||
float4 shadowMain(VertexInput v) : SV_Position { return mul(frame.lightViewProjection, float4(v.world,1)); }
|
float4 shadowMain(VertexInput v) : SV_Position { return mul(frame.lightViewProjection, float4(v.world,1)); }
|
||||||
|
float3 directBRDF(float3 base, float rough, float metal, float3 n, float3 view, float3 l) {
|
||||||
|
const float pi = 3.14159265;
|
||||||
|
float nl = max(dot(n,l),0.0);
|
||||||
|
if (nl <= 0.0) return float3(0);
|
||||||
|
float3 halfVector = l + view;
|
||||||
|
float halfLengthSquared = dot(halfVector, halfVector);
|
||||||
|
float3 h = halfLengthSquared > 1e-8 ? halfVector * rsqrt(halfLengthSquared) : n;
|
||||||
|
float nv=max(dot(n,view),0.001), nh=max(dot(n,h),0.0), vh=max(dot(view,h),0.0);
|
||||||
|
float a=rough*rough, a2=a*a, denom=nh*nh*(a2-1.0)+1.0;
|
||||||
|
float d=a2/(pi*denom*denom+0.0001);
|
||||||
|
float k=(rough+1.0)*(rough+1.0)/8.0;
|
||||||
|
float g=(nl/(nl*(1.0-k)+k))*(nv/(nv*(1.0-k)+k));
|
||||||
|
float3 f0=lerp(float3(0.04),base,metal), fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
||||||
|
float3 spec=d*g*fresnel/max(4.0*nv*nl,0.001);
|
||||||
|
return ((1.0-fresnel)*(1.0-metal)*base/pi+spec)*nl;
|
||||||
|
}
|
||||||
[shader("fragment")]
|
[shader("fragment")]
|
||||||
float4 fragmentMain(VertexOutput v) : SV_Target {
|
float4 fragmentMain(VertexOutput v) : SV_Target {
|
||||||
float4 sampled = colorMap.Sample(colorSampler, v.uv);
|
float4 sampled = colorMap.Sample(colorSampler, v.uv);
|
||||||
@@ -41,28 +83,52 @@ float4 fragmentMain(VertexOutput v) : SV_Target {
|
|||||||
return v.color * sampled;
|
return v.color * sampled;
|
||||||
}
|
}
|
||||||
float4 base = v.color * sampled;
|
float4 base = v.color * sampled;
|
||||||
const float pi = 3.14159265;
|
LightingHeader lighting = lightingFrame[0];
|
||||||
float3 n=normalize(v.normal), l=normalize(-frame.lightDirection.xyz), view=normalize(frame.eye.xyz-v.world), h=normalize(l+view);
|
float3 n=normalize(v.normal);
|
||||||
float nl=max(dot(n,l),0.0), nv=max(dot(n,view),0.001), nh=max(dot(n,h),0.0), vh=max(dot(view,h),0.0);
|
float3 viewDelta=frame.eye.xyz-v.world;
|
||||||
|
float viewLengthSquared=dot(viewDelta,viewDelta);
|
||||||
|
float3 view=viewLengthSquared > 1e-8 ? viewDelta*rsqrt(viewLengthSquared) : n;
|
||||||
float rough=clamp(v.material.x,0.08,1.0), metal=saturate(v.material.y);
|
float rough=clamp(v.material.x,0.08,1.0), metal=saturate(v.material.y);
|
||||||
float a=rough*rough, a2=a*a, denom=nh*nh*(a2-1.0)+1.0;
|
float3 linear=base.rgb*.12;
|
||||||
float d=a2/(pi*denom*denom+0.0001);
|
if (lighting.counts.y != 0 && lighting.sunDirectionIntensity.w > 0) {
|
||||||
float k=(rough+1.0)*(rough+1.0)/8.0;
|
float3 l=normalize(-lighting.sunDirectionIntensity.xyz);
|
||||||
float g=(nl/(nl*(1.0-k)+k))*(nv/(nv*(1.0-k)+k));
|
float nl=max(dot(n,l),0.0);
|
||||||
float3 f0=lerp(float3(0.04),base.rgb,metal), fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
float visibility=1.0;
|
||||||
float3 spec=d*g*fresnel/max(4.0*nv*nl,0.001);
|
if (lighting.counts.z != 0 && nl > 0) {
|
||||||
float4 lightClip=mul(frame.lightViewProjection,float4(v.world,1));
|
float4 lightClip=mul(frame.lightViewProjection,float4(v.world,1));
|
||||||
float3 projected=lightClip.xyz/lightClip.w;
|
float3 projected=lightClip.xyz/lightClip.w;
|
||||||
float2 uv=projected.xy*.5+.5;
|
float2 uv=projected.xy*.5+.5;
|
||||||
float visibility=1.0;
|
if(all(uv>=0.0)&&all(uv<=1.0)&&projected.z>=0.0&&projected.z<=1.0) {
|
||||||
if(all(uv>=0.0)&&all(uv<=1.0)&&projected.z>=0.0&&projected.z<=1.0) {
|
visibility=0.0;
|
||||||
visibility=0.0;
|
for(int y=-1;y<=1;++y) for(int x=-1;x<=1;++x) {
|
||||||
for(int y=-1;y<=1;++y) for(int x=-1;x<=1;++x) {
|
float depth=shadowMap.SampleLevel(shadowSampler,uv+float2(x,y)/1024.0,0);
|
||||||
float depth=shadowMap.SampleLevel(shadowSampler,uv+float2(x,y)/1024.0,0);
|
visibility += projected.z-max(0.0008,0.003*(1.0-nl)) <= depth ? 1.0/9.0 : 0.0;
|
||||||
visibility += projected.z-max(0.0008,0.003*(1.0-nl)) <= depth ? 1.0/9.0 : 0.0;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
linear += directBRDF(base.rgb, rough, metal, n, view, l) *
|
||||||
|
lighting.sunColor.rgb * (lighting.sunDirectionIntensity.w * 3.0 * visibility);
|
||||||
|
}
|
||||||
|
for (uint i=0; i<lighting.counts.x; ++i) {
|
||||||
|
LocalLightGpu light=localLights[i];
|
||||||
|
float3 delta=light.positionRange.xyz-v.world;
|
||||||
|
float distanceSquared=max(dot(delta,delta),1e-6);
|
||||||
|
float distance=sqrt(distanceSquared);
|
||||||
|
float range=max(light.positionRange.w,1e-4);
|
||||||
|
if (distance >= range || light.colorIntensity.w <= 0) continue;
|
||||||
|
float3 l=delta/distance;
|
||||||
|
float relative=distance/range;
|
||||||
|
float cutoff=1.0-relative*relative*relative*relative;
|
||||||
|
float attenuation=cutoff*cutoff/(1.0+distanceSquared);
|
||||||
|
if (light.coneTypeShadowView.y > 0.5) {
|
||||||
|
float cosAngle=dot(-l,normalize(light.directionCosOuter.xyz));
|
||||||
|
float denominator=max(light.coneTypeShadowView.x-light.directionCosOuter.w,1e-4);
|
||||||
|
float cone=saturate((cosAngle-light.directionCosOuter.w)/denominator);
|
||||||
|
attenuation *= cone*cone*(3.0-2.0*cone);
|
||||||
|
}
|
||||||
|
linear += directBRDF(base.rgb, rough, metal, n, view, l) *
|
||||||
|
light.colorIntensity.rgb * (light.colorIntensity.w * attenuation);
|
||||||
}
|
}
|
||||||
float3 linear=base.rgb*.12 + ((1.0-fresnel)*(1.0-metal)*base.rgb/pi+spec)*nl*3.0*visibility;
|
|
||||||
linear=linear/(1.0+linear);
|
linear=linear/(1.0+linear);
|
||||||
return float4(pow(max(linear,0),float3(1.0/2.2)),base.a);
|
return float4(pow(max(linear,0),float3(1.0/2.2)),base.a);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ struct GpuFrameParameters {
|
|||||||
uint4 drawInfo; // x=visible ID range base; firstInstance is always zero
|
uint4 drawInfo; // x=visible ID range base; firstInstance is always zero
|
||||||
};
|
};
|
||||||
[[vk::push_constant]] ConstantBuffer<GpuFrameParameters> gpuFrame;
|
[[vk::push_constant]] ConstantBuffer<GpuFrameParameters> gpuFrame;
|
||||||
[[vk::binding(0,1)]] StructuredBuffer<InstanceRecord> gfxInstances;
|
[[vk::binding(0,2)]] StructuredBuffer<InstanceRecord> gfxInstances;
|
||||||
[[vk::binding(1,1)]] StructuredBuffer<uint> gfxVisibleIds;
|
[[vk::binding(1,2)]] StructuredBuffer<uint> gfxVisibleIds;
|
||||||
[[vk::binding(2,1)]] StructuredBuffer<ViewRecord> gfxViews;
|
[[vk::binding(2,2)]] StructuredBuffer<ViewRecord> gfxViews;
|
||||||
|
|
||||||
// All indirect commands use firstInstance=0. The raw Vulkan index avoids the
|
// All indirect commands use firstInstance=0. The raw Vulkan index avoids the
|
||||||
// BaseInstance read that Slang adds for SV_InstanceID (DrawParameters feature).
|
// BaseInstance read that Slang adds for SV_InstanceID (DrawParameters feature).
|
||||||
|
|||||||
@@ -156,6 +156,15 @@ void SchemaRegistry::validate_component(const Json& component) const {
|
|||||||
for (const auto& [id, value] : component["fields"].items())
|
for (const auto& [id, value] : component["fields"].items())
|
||||||
if (metadata["fields"].contains(id))
|
if (metadata["fields"].contains(id))
|
||||||
validate_field(value, metadata["fields"][id]);
|
validate_field(value, metadata["fields"][id]);
|
||||||
|
if (type == "faset.light") {
|
||||||
|
auto effective = default_fields(type);
|
||||||
|
effective.update(component.at("fields"));
|
||||||
|
if (effective.at("kind") == "spot")
|
||||||
|
require(effective.at("inner_angle").get<double>() <=
|
||||||
|
effective.at("outer_angle").get<double>(),
|
||||||
|
"validation.light_cone",
|
||||||
|
"Spotlight inner_angle must not exceed outer_angle");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void SchemaRegistry::add_migration(const std::string& type, int from_version, Json rules) {
|
void SchemaRegistry::add_migration(const std::string& type, int from_version, Json rules) {
|
||||||
require(contains(type) && from_version > 0 && from_version < schema(type).value("version", 1) &&
|
require(contains(type) && from_version > 0 && from_version < schema(type).value("version", 1) &&
|
||||||
@@ -267,7 +276,10 @@ SchemaRegistry builtin_schemas() {
|
|||||||
{"default", 0.7}, {"min", 0.001}, {"max", 1.55},
|
{"default", 0.7}, {"min", 0.001}, {"max", 1.55},
|
||||||
{"unit", "radians"}}},
|
{"unit", "radians"}}},
|
||||||
{"casts_shadow", field("boolean", true)},
|
{"casts_shadow", field("boolean", true)},
|
||||||
{"shadow_priority", field("integer", 0)}});
|
{"shadow_priority", Json{{"type", "integer"},
|
||||||
|
{"default", 0},
|
||||||
|
{"min", std::numeric_limits<int>::min()},
|
||||||
|
{"max", std::numeric_limits<int>::max()}}}});
|
||||||
for (int dimension : {2, 3}) {
|
for (int dimension : {2, 3}) {
|
||||||
Json vector = dimension == 2 ? Json{0, 0} : Json{0, 0, 0};
|
Json vector = dimension == 2 ? Json{0, 0} : Json{0, 0, 0};
|
||||||
Json extents = dimension == 2 ? Json{0.5, 0.5} : Json{0.5, 0.5, 0.5};
|
Json extents = dimension == 2 ? Json{0.5, 0.5} : Json{0.5, 0.5, 0.5};
|
||||||
|
|||||||
@@ -397,7 +397,9 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting
|
|||||||
for (const auto coordinate : local.position)
|
for (const auto coordinate : local.position)
|
||||||
if (!std::isfinite(coordinate))
|
if (!std::isfinite(coordinate))
|
||||||
invalid("position");
|
invalid("position");
|
||||||
local.direction = normalized(direction(model, {0, 0, -1}), id, "direction");
|
if (local.kind == render::LocalLight::Kind::Spot)
|
||||||
|
local.direction = normalized(direction(model, {0, 0, -1}), id,
|
||||||
|
"direction");
|
||||||
local.color = color;
|
local.color = color;
|
||||||
local.intensity = intensity;
|
local.intensity = intensity;
|
||||||
local.range = number("range", 10);
|
local.range = number("range", 10);
|
||||||
@@ -405,13 +407,19 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting
|
|||||||
invalid("range");
|
invalid("range");
|
||||||
local.inner_angle = number("inner_angle", 0.35f);
|
local.inner_angle = number("inner_angle", 0.35f);
|
||||||
local.outer_angle = number("outer_angle", 0.7f);
|
local.outer_angle = number("outer_angle", 0.7f);
|
||||||
if (local.inner_angle < 0 || local.inner_angle > local.outer_angle ||
|
if (local.kind == render::LocalLight::Kind::Spot &&
|
||||||
local.outer_angle <= 0 || local.outer_angle >= std::numbers::pi_v<float> / 2)
|
(local.inner_angle < 0 || local.inner_angle > local.outer_angle ||
|
||||||
|
local.outer_angle <= 0 ||
|
||||||
|
local.outer_angle >= std::numbers::pi_v<float> / 2))
|
||||||
invalid("inner_angle/outer_angle");
|
invalid("inner_angle/outer_angle");
|
||||||
local.casts_shadow = castsShadow;
|
local.casts_shadow = castsShadow;
|
||||||
if (fields.contains("shadow_priority")) {
|
if (fields.contains("shadow_priority")) {
|
||||||
if (!fields.at("shadow_priority").is_number_integer())
|
if (!fields.at("shadow_priority").is_number_integer())
|
||||||
invalid("shadow_priority");
|
invalid("shadow_priority");
|
||||||
|
const auto priority = fields.at("shadow_priority").get<double>();
|
||||||
|
if (priority < std::numeric_limits<int>::min() ||
|
||||||
|
priority > std::numeric_limits<int>::max())
|
||||||
|
invalid("shadow_priority");
|
||||||
local.shadow_priority = fields.at("shadow_priority").get<int>();
|
local.shadow_priority = fields.at("shadow_priority").get<int>();
|
||||||
}
|
}
|
||||||
out.local_lights.push_back(std::move(local));
|
out.local_lights.push_back(std::move(local));
|
||||||
|
|||||||
+192
-20
@@ -6,6 +6,7 @@
|
|||||||
#include <bit>
|
#include <bit>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <faset/core/io.hpp>
|
#include <faset/core/io.hpp>
|
||||||
#include <faset/render/render_graph.hpp>
|
#include <faset/render/render_graph.hpp>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <numbers>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -72,6 +74,40 @@ struct ScenePush {
|
|||||||
std::array<std::uint32_t, 4> draw_info;
|
std::array<std::uint32_t, 4> draw_info;
|
||||||
};
|
};
|
||||||
static_assert(sizeof(ScenePush) == 112);
|
static_assert(sizeof(ScenePush) == 112);
|
||||||
|
struct LightingHeaderGpu {
|
||||||
|
std::array<std::uint32_t, 4> counts{};
|
||||||
|
std::array<float, 4> sun_direction_intensity{};
|
||||||
|
std::array<float, 4> sun_color{};
|
||||||
|
std::array<float, 4> camera_forward_shadow_distance{};
|
||||||
|
std::array<float, 4> cascade_splits{};
|
||||||
|
};
|
||||||
|
struct LocalLightGpu {
|
||||||
|
std::array<float, 4> position_range{};
|
||||||
|
std::array<float, 4> direction_cos_outer{};
|
||||||
|
std::array<float, 4> color_intensity{};
|
||||||
|
std::array<float, 4> cone_type_shadow_view{};
|
||||||
|
std::array<float, 4> reserved{};
|
||||||
|
};
|
||||||
|
struct ShadowViewGpu {
|
||||||
|
Mat4 view_projection{identity};
|
||||||
|
std::array<float, 4> tile_scale_offset{};
|
||||||
|
std::array<float, 4> guarded_clamp{};
|
||||||
|
std::array<float, 4> bias_flags{};
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LightingHeaderGpu) == 80 &&
|
||||||
|
offsetof(LightingHeaderGpu, sun_direction_intensity) == 16 &&
|
||||||
|
offsetof(LightingHeaderGpu, sun_color) == 32 &&
|
||||||
|
offsetof(LightingHeaderGpu, camera_forward_shadow_distance) == 48 &&
|
||||||
|
offsetof(LightingHeaderGpu, cascade_splits) == 64);
|
||||||
|
static_assert(sizeof(LocalLightGpu) == 80 &&
|
||||||
|
offsetof(LocalLightGpu, direction_cos_outer) == 16 &&
|
||||||
|
offsetof(LocalLightGpu, color_intensity) == 32 &&
|
||||||
|
offsetof(LocalLightGpu, cone_type_shadow_view) == 48 &&
|
||||||
|
offsetof(LocalLightGpu, reserved) == 64);
|
||||||
|
static_assert(sizeof(ShadowViewGpu) == 112 &&
|
||||||
|
offsetof(ShadowViewGpu, tile_scale_offset) == 64 &&
|
||||||
|
offsetof(ShadowViewGpu, guarded_clamp) == 80 &&
|
||||||
|
offsetof(ShadowViewGpu, bias_flags) == 96);
|
||||||
std::array<float, 4> point(const Mat4& m, std::array<float, 4> p) {
|
std::array<float, 4> point(const Mat4& m, std::array<float, 4> p) {
|
||||||
std::array<float, 4> o{};
|
std::array<float, 4> o{};
|
||||||
for (int r = 0; r < 4; ++r)
|
for (int r = 0; r < 4; ++r)
|
||||||
@@ -196,6 +232,7 @@ struct Renderer::Impl {
|
|||||||
std::vector<VkImageLayout> swap_layouts;
|
std::vector<VkImageLayout> swap_layouts;
|
||||||
Image color, depth, shadow;
|
Image color, depth, shadow;
|
||||||
Buffer vertices, readback;
|
Buffer vertices, readback;
|
||||||
|
Buffer lighting_header, lighting_locals, lighting_views;
|
||||||
SceneResources scene;
|
SceneResources scene;
|
||||||
InstanceTracker instance_tracker;
|
InstanceTracker instance_tracker;
|
||||||
std::unordered_map<std::string, std::size_t> previous_lods;
|
std::unordered_map<std::string, std::size_t> previous_lods;
|
||||||
@@ -212,6 +249,9 @@ struct Renderer::Impl {
|
|||||||
std::unordered_map<const Texture*, CachedOpacity> opacity_cache;
|
std::unordered_map<const Texture*, CachedOpacity> opacity_cache;
|
||||||
VkDescriptorSetLayout descriptor_layout{};
|
VkDescriptorSetLayout descriptor_layout{};
|
||||||
VkDescriptorPool descriptor_pool{};
|
VkDescriptorPool descriptor_pool{};
|
||||||
|
VkDescriptorSetLayout lighting_layout{};
|
||||||
|
VkDescriptorPool lighting_pool{};
|
||||||
|
VkDescriptorSet lighting_set{};
|
||||||
VkSampler shadow_sampler{}, color_sampler{};
|
VkSampler shadow_sampler{}, color_sampler{};
|
||||||
VkPipelineLayout pipeline_layout{};
|
VkPipelineLayout pipeline_layout{};
|
||||||
VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{};
|
VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{};
|
||||||
@@ -316,6 +356,9 @@ struct Renderer::Impl {
|
|||||||
destroy(scene.hzb[1]);
|
destroy(scene.hzb[1]);
|
||||||
destroy(vertices);
|
destroy(vertices);
|
||||||
destroy(readback);
|
destroy(readback);
|
||||||
|
destroy(lighting_header);
|
||||||
|
destroy(lighting_locals);
|
||||||
|
destroy(lighting_views);
|
||||||
destroy(color);
|
destroy(color);
|
||||||
destroy(depth);
|
destroy(depth);
|
||||||
destroy(shadow);
|
destroy(shadow);
|
||||||
@@ -333,8 +376,12 @@ struct Renderer::Impl {
|
|||||||
vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
|
vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
|
||||||
if (descriptor_pool)
|
if (descriptor_pool)
|
||||||
vkDestroyDescriptorPool(device, descriptor_pool, nullptr);
|
vkDestroyDescriptorPool(device, descriptor_pool, nullptr);
|
||||||
|
if (lighting_pool)
|
||||||
|
vkDestroyDescriptorPool(device, lighting_pool, nullptr);
|
||||||
if (descriptor_layout)
|
if (descriptor_layout)
|
||||||
vkDestroyDescriptorSetLayout(device, descriptor_layout, nullptr);
|
vkDestroyDescriptorSetLayout(device, descriptor_layout, nullptr);
|
||||||
|
if (lighting_layout)
|
||||||
|
vkDestroyDescriptorSetLayout(device, lighting_layout, nullptr);
|
||||||
if (shadow_sampler)
|
if (shadow_sampler)
|
||||||
vkDestroySampler(device, shadow_sampler, nullptr);
|
vkDestroySampler(device, shadow_sampler, nullptr);
|
||||||
if (color_sampler)
|
if (color_sampler)
|
||||||
@@ -889,6 +936,31 @@ struct Renderer::Impl {
|
|||||||
pi.pPoolSizes = sizes;
|
pi.pPoolSizes = sizes;
|
||||||
check(vkCreateDescriptorPool(device, &pi, nullptr, &descriptor_pool),
|
check(vkCreateDescriptorPool(device, &pi, nullptr, &descriptor_pool),
|
||||||
"Create descriptor pool");
|
"Create descriptor pool");
|
||||||
|
std::array<VkDescriptorSetLayoutBinding, 4> lighting_bindings{};
|
||||||
|
for (std::uint32_t i = 0; i < lighting_bindings.size(); ++i)
|
||||||
|
lighting_bindings[i] = {i,
|
||||||
|
i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
|
||||||
|
: VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||||
|
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||||
|
li.bindingCount = static_cast<std::uint32_t>(lighting_bindings.size());
|
||||||
|
li.pBindings = lighting_bindings.data();
|
||||||
|
check(vkCreateDescriptorSetLayout(device, &li, nullptr, &lighting_layout),
|
||||||
|
"Create lighting descriptor layout");
|
||||||
|
VkDescriptorPoolSize lighting_sizes[] = {
|
||||||
|
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1}};
|
||||||
|
pi.flags = 0;
|
||||||
|
pi.maxSets = 1;
|
||||||
|
pi.poolSizeCount = 2;
|
||||||
|
pi.pPoolSizes = lighting_sizes;
|
||||||
|
check(vkCreateDescriptorPool(device, &pi, nullptr, &lighting_pool),
|
||||||
|
"Create lighting descriptor pool");
|
||||||
|
VkDescriptorSetAllocateInfo lighting_allocation{};
|
||||||
|
lighting_allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||||
|
lighting_allocation.descriptorPool = lighting_pool;
|
||||||
|
lighting_allocation.descriptorSetCount = 1;
|
||||||
|
lighting_allocation.pSetLayouts = &lighting_layout;
|
||||||
|
check(vkAllocateDescriptorSets(device, &lighting_allocation, &lighting_set),
|
||||||
|
"Allocate lighting descriptors");
|
||||||
VkSamplerCreateInfo si{};
|
VkSamplerCreateInfo si{};
|
||||||
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
si.magFilter = si.minFilter = VK_FILTER_NEAREST;
|
si.magFilter = si.minFilter = VK_FILTER_NEAREST;
|
||||||
@@ -1009,8 +1081,9 @@ struct Renderer::Impl {
|
|||||||
sizeof(Push)};
|
sizeof(Push)};
|
||||||
VkPipelineLayoutCreateInfo li{};
|
VkPipelineLayoutCreateInfo li{};
|
||||||
li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||||
li.setLayoutCount = 1;
|
const std::array<VkDescriptorSetLayout, 2> set_layouts{descriptor_layout, lighting_layout};
|
||||||
li.pSetLayouts = &descriptor_layout;
|
li.setLayoutCount = static_cast<std::uint32_t>(set_layouts.size());
|
||||||
|
li.pSetLayouts = set_layouts.data();
|
||||||
li.pushConstantRangeCount = 1;
|
li.pushConstantRangeCount = 1;
|
||||||
li.pPushConstantRanges = &push;
|
li.pPushConstantRanges = &push;
|
||||||
check(vkCreatePipelineLayout(device, &li, nullptr, &pipeline_layout),
|
check(vkCreatePipelineLayout(device, &li, nullptr, &pipeline_layout),
|
||||||
@@ -1220,14 +1293,14 @@ struct Renderer::Impl {
|
|||||||
layout.pBindings = hzb.data();
|
layout.pBindings = hzb.data();
|
||||||
check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &scene.hzb_layout),
|
check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &scene.hzb_layout),
|
||||||
"Create HZB descriptor layout");
|
"Create HZB descriptor layout");
|
||||||
const std::array<VkDescriptorSetLayout, 2> scene_layouts{descriptor_layout,
|
const std::array<VkDescriptorSetLayout, 3> scene_layouts{
|
||||||
scene.graphics_layout};
|
descriptor_layout, lighting_layout, scene.graphics_layout};
|
||||||
VkPushConstantRange graphics_push{VK_SHADER_STAGE_VERTEX_BIT |
|
VkPushConstantRange graphics_push{VK_SHADER_STAGE_VERTEX_BIT |
|
||||||
VK_SHADER_STAGE_FRAGMENT_BIT,
|
VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
0, sizeof(ScenePush)};
|
0, sizeof(ScenePush)};
|
||||||
VkPipelineLayoutCreateInfo pipeline_info{};
|
VkPipelineLayoutCreateInfo pipeline_info{};
|
||||||
pipeline_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
pipeline_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||||
pipeline_info.setLayoutCount = 2;
|
pipeline_info.setLayoutCount = static_cast<std::uint32_t>(scene_layouts.size());
|
||||||
pipeline_info.pSetLayouts = scene_layouts.data();
|
pipeline_info.pSetLayouts = scene_layouts.data();
|
||||||
pipeline_info.pushConstantRangeCount = 1;
|
pipeline_info.pushConstantRangeCount = 1;
|
||||||
pipeline_info.pPushConstantRanges = &graphics_push;
|
pipeline_info.pPushConstantRanges = &graphics_push;
|
||||||
@@ -1533,6 +1606,29 @@ struct Renderer::Impl {
|
|||||||
VkBufferUsageFlags usage = 0) {
|
VkBufferUsageFlags usage = 0) {
|
||||||
upload_scene_buffer(buffer, values.data(), values.size() * sizeof(T), usage);
|
upload_scene_buffer(buffer, values.data(), values.size() * sizeof(T), usage);
|
||||||
}
|
}
|
||||||
|
void update_lighting_descriptors() {
|
||||||
|
const std::array<VkDescriptorBufferInfo, 3> buffers{{
|
||||||
|
{lighting_header.handle, 0, lighting_header.size},
|
||||||
|
{lighting_locals.handle, 0, lighting_locals.size},
|
||||||
|
{lighting_views.handle, 0, lighting_views.size}}};
|
||||||
|
const VkDescriptorImageInfo atlas{VK_NULL_HANDLE, shadow.view,
|
||||||
|
VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL};
|
||||||
|
std::array<VkWriteDescriptorSet, 4> writes{};
|
||||||
|
for (std::uint32_t i = 0; i < writes.size(); ++i) {
|
||||||
|
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
writes[i].dstSet = lighting_set;
|
||||||
|
writes[i].dstBinding = i;
|
||||||
|
writes[i].descriptorCount = 1;
|
||||||
|
writes[i].descriptorType = i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
|
||||||
|
: VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
|
if (i == 3)
|
||||||
|
writes[i].pImageInfo = &atlas;
|
||||||
|
else
|
||||||
|
writes[i].pBufferInfo = &buffers[i];
|
||||||
|
}
|
||||||
|
vkUpdateDescriptorSets(device, static_cast<std::uint32_t>(writes.size()),
|
||||||
|
writes.data(), 0, nullptr);
|
||||||
|
}
|
||||||
void update_scene_descriptors(bool occlusion) {
|
void update_scene_descriptors(bool occlusion) {
|
||||||
auto write_buffers = [&](VkDescriptorSet set, std::span<const Buffer* const> buffers,
|
auto write_buffers = [&](VkDescriptorSet set, std::span<const Buffer* const> buffers,
|
||||||
std::uint32_t first_binding) {
|
std::uint32_t first_binding) {
|
||||||
@@ -2017,15 +2113,87 @@ struct Renderer::Impl {
|
|||||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||||
update_scene_descriptors(occlusion);
|
update_scene_descriptors(occlusion);
|
||||||
}
|
}
|
||||||
Vec3 direction = snapshot.light_direction;
|
std::optional<SunLight> sun = snapshot.sun;
|
||||||
|
if (!sun && !snapshot.authored_lights_present && snapshot.local_lights.empty())
|
||||||
|
sun = SunLight{"legacy-sun", snapshot.light_direction, {1, 1, 1, 1}, 1, true};
|
||||||
|
Vec3 direction = sun ? sun->direction : snapshot.light_direction;
|
||||||
float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] +
|
float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] +
|
||||||
direction[2] * direction[2]);
|
direction[2] * direction[2]);
|
||||||
if (length < 1e-5f) {
|
if (!std::isfinite(length) || length < 1e-5f) {
|
||||||
|
if (sun && sun->stable_id != "legacy-sun")
|
||||||
|
throw std::invalid_argument("Authored sun direction must be finite and nonzero");
|
||||||
direction = {-.5f, -1, -.3f};
|
direction = {-.5f, -1, -.3f};
|
||||||
length = std::sqrt(1.34f);
|
length = std::sqrt(1.34f);
|
||||||
}
|
}
|
||||||
for (auto& v : direction)
|
for (auto& v : direction)
|
||||||
v /= length;
|
v /= length;
|
||||||
|
LightingHeaderGpu lighting{};
|
||||||
|
lighting.counts[1] = sun ? 1u : 0u;
|
||||||
|
lighting.counts[2] = sun && sun->casts_shadow ? 1u : 0u;
|
||||||
|
lighting.sun_direction_intensity = {direction[0], direction[1], direction[2],
|
||||||
|
sun ? sun->intensity : 0};
|
||||||
|
lighting.sun_color = sun ? sun->color : Color{0, 0, 0, 1};
|
||||||
|
if (sun && (!std::isfinite(sun->intensity) || sun->intensity < 0 ||
|
||||||
|
std::any_of(sun->color.begin(), sun->color.end(),
|
||||||
|
[](float v) { return !std::isfinite(v) || v < 0; })))
|
||||||
|
throw std::invalid_argument("Authored sun radiance must be finite and nonnegative");
|
||||||
|
lighting.camera_forward_shadow_distance = {0, 0, -1, 80};
|
||||||
|
if (snapshot.camera_frustum) {
|
||||||
|
const auto& view = snapshot.camera_frustum->view;
|
||||||
|
lighting.camera_forward_shadow_distance = {-view[2], -view[6], -view[10], 80};
|
||||||
|
}
|
||||||
|
auto sorted_lights = snapshot.local_lights;
|
||||||
|
std::stable_sort(sorted_lights.begin(), sorted_lights.end(),
|
||||||
|
[](const auto& a, const auto& b) { return a.stable_id < b.stable_id; });
|
||||||
|
constexpr std::size_t max_local_lights = 128;
|
||||||
|
std::vector<LocalLightGpu> gpu_lights;
|
||||||
|
gpu_lights.reserve(std::min(sorted_lights.size(), max_local_lights));
|
||||||
|
for (const auto& local : sorted_lights) {
|
||||||
|
if (gpu_lights.size() == max_local_lights)
|
||||||
|
break;
|
||||||
|
const auto finite_color = std::all_of(local.color.begin(), local.color.end(),
|
||||||
|
[](float v) { return std::isfinite(v) && v >= 0; });
|
||||||
|
const auto finite_position = std::all_of(local.position.begin(), local.position.end(),
|
||||||
|
[](float v) { return std::isfinite(v); });
|
||||||
|
if (!finite_color || !finite_position || !std::isfinite(local.intensity) ||
|
||||||
|
local.intensity < 0 || !std::isfinite(local.range) || local.range <= 0)
|
||||||
|
throw std::invalid_argument("Local light radiance, position and range must be finite");
|
||||||
|
if (local.kind == LocalLight::Kind::Spot &&
|
||||||
|
(!std::isfinite(local.inner_angle) || !std::isfinite(local.outer_angle) ||
|
||||||
|
local.inner_angle < 0 || local.inner_angle > local.outer_angle ||
|
||||||
|
local.outer_angle >= std::numbers::pi_v<float> / 2))
|
||||||
|
throw std::invalid_argument("Spotlight cone angles are invalid");
|
||||||
|
auto spot_direction = local.direction;
|
||||||
|
float spot_length = std::hypot(spot_direction[0], spot_direction[1],
|
||||||
|
spot_direction[2]);
|
||||||
|
if (!std::isfinite(spot_length) || spot_length < 1e-6f) {
|
||||||
|
if (local.kind == LocalLight::Kind::Spot)
|
||||||
|
throw std::invalid_argument("Spotlight direction must be finite and nonzero");
|
||||||
|
spot_direction = {0, 0, -1};
|
||||||
|
spot_length = 1;
|
||||||
|
}
|
||||||
|
for (auto& axis : spot_direction)
|
||||||
|
axis /= spot_length;
|
||||||
|
LocalLightGpu gpu{};
|
||||||
|
gpu.position_range = {local.position[0], local.position[1], local.position[2],
|
||||||
|
local.range};
|
||||||
|
const bool spot = local.kind == LocalLight::Kind::Spot;
|
||||||
|
gpu.direction_cos_outer = {spot_direction[0], spot_direction[1], spot_direction[2],
|
||||||
|
spot ? std::cos(local.outer_angle) : 0.f};
|
||||||
|
gpu.color_intensity = {local.color[0], local.color[1], local.color[2],
|
||||||
|
local.intensity};
|
||||||
|
gpu.cone_type_shadow_view = {spot ? std::cos(local.inner_angle) : 1.f,
|
||||||
|
spot ? 1.f : 0.f, -1, 0};
|
||||||
|
gpu_lights.push_back(gpu);
|
||||||
|
}
|
||||||
|
lighting.counts[0] = static_cast<std::uint32_t>(gpu_lights.size());
|
||||||
|
if (gpu_lights.empty())
|
||||||
|
gpu_lights.push_back({}); // Descriptors always point at a full initialized record.
|
||||||
|
const ShadowViewGpu empty_shadow_view{};
|
||||||
|
upload_scene_buffer(lighting_header, &lighting, sizeof(lighting), 0);
|
||||||
|
upload_scene_vector(lighting_locals, gpu_lights);
|
||||||
|
upload_scene_buffer(lighting_views, &empty_shadow_view, sizeof(empty_shadow_view), 0);
|
||||||
|
update_lighting_descriptors();
|
||||||
Vec3 light_eye{-direction[0] * 30, -direction[1] * 30, -direction[2] * 30};
|
Vec3 light_eye{-direction[0] * 30, -direction[1] * 30, -direction[2] * 30};
|
||||||
Vec3 light_up = std::abs(direction[1]) > .98f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
|
Vec3 light_up = std::abs(direction[1]) > .98f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
|
||||||
Push push{multiply(orthographic(-20, 20, -20, 20, .1f, 80),
|
Push push{multiply(orthographic(-20, 20, -20, 20, .1f, 80),
|
||||||
@@ -2078,6 +2246,12 @@ struct Renderer::Impl {
|
|||||||
vkCmdSetViewport(command, 0, 1, &viewport);
|
vkCmdSetViewport(command, 0, 1, &viewport);
|
||||||
vkCmdSetScissor(command, 0, 1, &scissor);
|
vkCmdSetScissor(command, 0, 1, &scissor);
|
||||||
};
|
};
|
||||||
|
auto bind_material = [&](VkDescriptorSet material) {
|
||||||
|
const std::array<VkDescriptorSet, 2> sets{material, lighting_set};
|
||||||
|
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
||||||
|
0, static_cast<std::uint32_t>(sets.size()), sets.data(),
|
||||||
|
0, nullptr);
|
||||||
|
};
|
||||||
auto draw_transparent = [&] {
|
auto draw_transparent = [&] {
|
||||||
if (transparent_batches.empty())
|
if (transparent_batches.empty())
|
||||||
return;
|
return;
|
||||||
@@ -2088,8 +2262,7 @@ struct Renderer::Impl {
|
|||||||
0, sizeof(push), &push);
|
0, sizeof(push), &push);
|
||||||
for (auto batch : transparent_batches) {
|
for (auto batch : transparent_batches) {
|
||||||
auto descriptor = textures.at(batch.texture).descriptor;
|
auto descriptor = textures.at(batch.texture).descriptor;
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
bind_material(descriptor);
|
||||||
pipeline_layout, 0, 1, &descriptor, 0, nullptr);
|
|
||||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||||
++statistics.draw_calls;
|
++statistics.draw_calls;
|
||||||
}
|
}
|
||||||
@@ -2102,8 +2275,7 @@ struct Renderer::Impl {
|
|||||||
sizeof(push), &push);
|
sizeof(push), &push);
|
||||||
for (auto batch : sprite_batches) {
|
for (auto batch : sprite_batches) {
|
||||||
auto descriptor = textures.at(batch.texture).descriptor;
|
auto descriptor = textures.at(batch.texture).descriptor;
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
bind_material(descriptor);
|
||||||
0, 1, &descriptor, 0, nullptr);
|
|
||||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||||
++statistics.draw_calls;
|
++statistics.draw_calls;
|
||||||
}
|
}
|
||||||
@@ -2125,8 +2297,7 @@ struct Renderer::Impl {
|
|||||||
continue;
|
continue;
|
||||||
vkCmdSetScissor(command, 0, 1, &scissor);
|
vkCmdSetScissor(command, 0, 1, &scissor);
|
||||||
auto descriptor = textures.at(batch.texture).descriptor;
|
auto descriptor = textures.at(batch.texture).descriptor;
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
bind_material(descriptor);
|
||||||
0, 1, &descriptor, 0, nullptr);
|
|
||||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||||
++statistics.draw_calls;
|
++statistics.draw_calls;
|
||||||
}
|
}
|
||||||
@@ -2313,8 +2484,7 @@ struct Renderer::Impl {
|
|||||||
vkCmdPushConstants(command, pipeline_layout,
|
vkCmdPushConstants(command, pipeline_layout,
|
||||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
|
||||||
sizeof(push), &push);
|
sizeof(push), &push);
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1,
|
bind_material(white_descriptor);
|
||||||
&white_descriptor, 0, nullptr);
|
|
||||||
if (gpu_active && !gpu_frame.bins.empty()) {
|
if (gpu_active && !gpu_frame.bins.empty()) {
|
||||||
VkDeviceSize scene_offset{};
|
VkDeviceSize scene_offset{};
|
||||||
vkCmdBindVertexBuffers(command, 0, 1, &scene.vertices.handle, &scene_offset);
|
vkCmdBindVertexBuffers(command, 0, 1, &scene.vertices.handle, &scene_offset);
|
||||||
@@ -2322,7 +2492,7 @@ struct Renderer::Impl {
|
|||||||
scene.graphics_pipeline);
|
scene.graphics_pipeline);
|
||||||
for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) {
|
for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) {
|
||||||
auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor;
|
auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor;
|
||||||
const std::array<VkDescriptorSet, 2> sets{descriptor,
|
const std::array<VkDescriptorSet, 3> sets{descriptor, lighting_set,
|
||||||
scene.graphics_main};
|
scene.graphics_main};
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
scene.graphics_pipeline_layout, 0, sets.size(),
|
scene.graphics_pipeline_layout, 0, sets.size(),
|
||||||
@@ -2346,8 +2516,7 @@ struct Renderer::Impl {
|
|||||||
}
|
}
|
||||||
for (auto batch : scene_batches) {
|
for (auto batch : scene_batches) {
|
||||||
auto descriptor = textures.at(batch.texture).descriptor;
|
auto descriptor = textures.at(batch.texture).descriptor;
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
bind_material(descriptor);
|
||||||
0, 1, &descriptor, 0, nullptr);
|
|
||||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||||
++statistics.draw_calls;
|
++statistics.draw_calls;
|
||||||
}
|
}
|
||||||
@@ -2446,7 +2615,7 @@ struct Renderer::Impl {
|
|||||||
scene.graphics_pipeline);
|
scene.graphics_pipeline);
|
||||||
for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) {
|
for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) {
|
||||||
auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor;
|
auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor;
|
||||||
const std::array<VkDescriptorSet, 2> sets{descriptor,
|
const std::array<VkDescriptorSet, 3> sets{descriptor, lighting_set,
|
||||||
scene.graphics_post};
|
scene.graphics_post};
|
||||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
scene.graphics_pipeline_layout, 0,
|
scene.graphics_pipeline_layout, 0,
|
||||||
@@ -2618,7 +2787,10 @@ struct Renderer::Impl {
|
|||||||
++statistics.frame;
|
++statistics.frame;
|
||||||
statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size +
|
statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size +
|
||||||
color.allocation_size + depth.allocation_size +
|
color.allocation_size + depth.allocation_size +
|
||||||
shadow.allocation_size;
|
shadow.allocation_size +
|
||||||
|
lighting_header.allocation_size +
|
||||||
|
lighting_locals.allocation_size +
|
||||||
|
lighting_views.allocation_size;
|
||||||
statistics.texture_count = static_cast<std::uint32_t>(textures.size());
|
statistics.texture_count = static_cast<std::uint32_t>(textures.size());
|
||||||
for (const auto& [_, texture] : textures)
|
for (const auto& [_, texture] : textures)
|
||||||
statistics.gpu_allocated_bytes += texture.image.allocation_size;
|
statistics.gpu_allocated_bytes += texture.image.allocation_size;
|
||||||
|
|||||||
@@ -34,13 +34,21 @@ void validate_layout(const Json& layout, std::string_view entry) {
|
|||||||
const bool fragment = entry == "fragmentMain";
|
const bool fragment = entry == "fragmentMain";
|
||||||
require(layout.at("stage") == (fragment ? "fragment" : "vertex"), "shader stage changed");
|
require(layout.at("stage") == (fragment ? "fragment" : "vertex"), "shader stage changed");
|
||||||
const auto& descriptors = layout.at("descriptors");
|
const auto& descriptors = layout.at("descriptors");
|
||||||
require(descriptors.is_array() && descriptors.size() == 4, "descriptor count changed");
|
require(descriptors.is_array() && descriptors.size() == 8, "descriptor count changed");
|
||||||
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
||||||
const auto& binding = descriptors[i];
|
const auto& binding = descriptors[i];
|
||||||
require(binding.at("set") == 0 && binding.at("binding") == i && binding.at("count") == 1,
|
const auto set = i < 4 ? 0 : 1;
|
||||||
|
const auto slot = i % 4;
|
||||||
|
require(binding.at("set") == set && binding.at("binding") == slot &&
|
||||||
|
binding.at("count") == 1,
|
||||||
"descriptor set, binding or array count changed");
|
"descriptor set, binding or array count changed");
|
||||||
require(binding.at("type") == (i % 2 ? "sampler" : "sampled_image_2d"),
|
const auto* expected_type = set == 0 ? (slot % 2 ? "sampler" : "sampled_image_2d")
|
||||||
|
: slot == 3 ? "sampled_image_2d" : "storage_buffer";
|
||||||
|
require(binding.at("type") == expected_type,
|
||||||
"descriptor type changed");
|
"descriptor type changed");
|
||||||
|
if (set == 1 && slot < 3)
|
||||||
|
require(binding.at("element_stride") == (slot == 2 ? 112 : 80),
|
||||||
|
"lighting storage record stride changed");
|
||||||
require(fragment || !binding.at("used").get<bool>(),
|
require(fragment || !binding.at("used").get<bool>(),
|
||||||
"vertex texture bindings are unsupported");
|
"vertex texture bindings are unsupported");
|
||||||
}
|
}
|
||||||
@@ -98,7 +106,7 @@ void validate_gpu_layout(const Json& layout, std::string_view entry) {
|
|||||||
const std::array<int, 3> graphics_strides{224, 4, 208};
|
const std::array<int, 3> graphics_strides{224, 4, 208};
|
||||||
for (std::size_t i = 0; i < expected_count; ++i) {
|
for (std::size_t i = 0; i < expected_count; ++i) {
|
||||||
const auto& binding = descriptors[i];
|
const auto& binding = descriptors[i];
|
||||||
require(binding.at("set") == (graphics ? 1 : 0) && binding.at("binding") == i &&
|
require(binding.at("set") == (graphics ? 2 : 0) && binding.at("binding") == i &&
|
||||||
binding.at("count") == 1,
|
binding.at("count") == 1,
|
||||||
"GPU descriptor set, binding or count changed");
|
"GPU descriptor set, binding or count changed");
|
||||||
const int stride = graphics ? graphics_strides[i] : hzb ? 0 : compute_strides[i];
|
const int stride = graphics ? graphics_strides[i] : hzb ? 0 : compute_strides[i];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <faset/authoring/templates.hpp>
|
#include <faset/authoring/templates.hpp>
|
||||||
#include <faset/authoring/transforms.hpp>
|
#include <faset/authoring/transforms.hpp>
|
||||||
#include <faset/core/io.hpp>
|
#include <faset/core/io.hpp>
|
||||||
|
#include <cstdint>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#define CHECK(x) \
|
#define CHECK(x) \
|
||||||
@@ -44,6 +45,13 @@ int main() {
|
|||||||
light_component["fields"]["range"] = 10;
|
light_component["fields"]["range"] = 10;
|
||||||
light_component["fields"]["intensity"] = -1;
|
light_component["fields"]["intensity"] = -1;
|
||||||
fails([&] { schemas.validate_component(light_component); }, "validation.minimum");
|
fails([&] { schemas.validate_component(light_component); }, "validation.minimum");
|
||||||
|
light_component["fields"] = {{"kind", "spot"}, {"inner_angle", 0.9}};
|
||||||
|
fails([&] { schemas.validate_component(light_component); }, "validation.light_cone");
|
||||||
|
light_component["fields"] = {{"kind", "spot"},
|
||||||
|
{"inner_angle", 0.2},
|
||||||
|
{"outer_angle", 0.5},
|
||||||
|
{"shadow_priority", std::int64_t{2147483648}}};
|
||||||
|
fails([&] { schemas.validate_component(light_component); }, "validation.maximum");
|
||||||
AuthoringService service(root, schemas);
|
AuthoringService service(root, schemas);
|
||||||
auto created = service.create("Courtyard", 3);
|
auto created = service.create("Courtyard", 3);
|
||||||
const std::string id = created["id"];
|
const std::string id = created["id"];
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ int main() {
|
|||||||
faset::atomic_write_json(reflection_file, metadata);
|
faset::atomic_write_json(reflection_file, metadata);
|
||||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
"A consistently rehashed but incompatible GPU record stride must be rejected");
|
"A consistently rehashed but incompatible GPU record stride must be rejected");
|
||||||
|
faset::atomic_write_json(reflection_file,
|
||||||
|
faset::read_json(original / "gpuPostCullMain.reflection.json"));
|
||||||
|
reflection_file = temporary / "gpuVertexMain.reflection.json";
|
||||||
|
metadata = faset::read_json(original / "gpuVertexMain.reflection.json");
|
||||||
|
metadata["layout"]["descriptors"][0]["set"] = 1;
|
||||||
|
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
||||||
|
faset::atomic_write_json(reflection_file, metadata);
|
||||||
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
|
"GPU graphics scene buffers must stay in descriptor set two");
|
||||||
fs::remove(temporary / "gpuHzbMain.spv");
|
fs::remove(temporary / "gpuHzbMain.spv");
|
||||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
"Missing P2 entry must be rejected");
|
"Missing P2 entry must be rejected");
|
||||||
|
|||||||
@@ -59,6 +59,27 @@ int main() {
|
|||||||
const auto original_reflection = read_text(bundle / "fragmentMain.reflection.json");
|
const auto original_reflection = read_text(bundle / "fragmentMain.reflection.json");
|
||||||
const auto original_fingerprint = Json::parse(original_reflection).at("layout_fingerprint");
|
const auto original_fingerprint = Json::parse(original_reflection).at("layout_fingerprint");
|
||||||
render::validate_shader_bundle(bundle);
|
render::validate_shader_bundle(bundle);
|
||||||
|
auto bad_lighting_stride = Json::parse(original_reflection);
|
||||||
|
auto& lighting_descriptors = bad_lighting_stride["layout"]["descriptors"];
|
||||||
|
bool found_local_buffer = false;
|
||||||
|
for (auto& descriptor : lighting_descriptors)
|
||||||
|
if (descriptor["set"] == 1 && descriptor["binding"] == 1) {
|
||||||
|
descriptor["element_stride"] = 96;
|
||||||
|
found_local_buffer = true;
|
||||||
|
}
|
||||||
|
require(found_local_buffer, "Lighting stride fixture exists");
|
||||||
|
bad_lighting_stride["layout_fingerprint"] =
|
||||||
|
sha256(bad_lighting_stride["layout"].dump());
|
||||||
|
atomic_write_json(bundle / "fragmentMain.reflection.json", bad_lighting_stride);
|
||||||
|
bool rejected_lighting_stride = false;
|
||||||
|
try {
|
||||||
|
render::validate_shader_bundle(bundle);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
rejected_lighting_stride = true;
|
||||||
|
}
|
||||||
|
require(rejected_lighting_stride,
|
||||||
|
"Rehashed incompatible local-light element stride must be rejected");
|
||||||
|
atomic_write(bundle / "fragmentMain.reflection.json", original_reflection);
|
||||||
render::RendererConfig configuration;
|
render::RendererConfig configuration;
|
||||||
configuration.width = configuration.height = 64;
|
configuration.width = configuration.height = 64;
|
||||||
configuration.headless = true;
|
configuration.headless = true;
|
||||||
@@ -161,6 +182,18 @@ int main() {
|
|||||||
atomic_write(bundle / "fragmentMain.spv", "damaged bytecode");
|
atomic_write(bundle / "fragmentMain.spv", "damaged bytecode");
|
||||||
retained();
|
retained();
|
||||||
restore();
|
restore();
|
||||||
|
auto incompatible = original_source;
|
||||||
|
auto at = incompatible.find(" float4 reserved;");
|
||||||
|
require(at != std::string::npos, "Local-light stride fixture exists");
|
||||||
|
incompatible.replace(at, std::string(" float4 reserved;").size(),
|
||||||
|
" float4 reserved;\n float4 incompatibleExtraLane;");
|
||||||
|
atomic_write(source, incompatible);
|
||||||
|
require(compile(source, bundle) == 0, "Compile incompatible light-buffer stride");
|
||||||
|
require(read_json(bundle / "fragmentMain.reflection.json").at("layout_fingerprint") !=
|
||||||
|
original_fingerprint,
|
||||||
|
"Lighting stride edit changes normalized layout fingerprint");
|
||||||
|
retained();
|
||||||
|
restore();
|
||||||
auto malformed = original_spirv;
|
auto malformed = original_spirv;
|
||||||
for (int i = 0; i < 4; ++i)
|
for (int i = 0; i < 4; ++i)
|
||||||
malformed[20 + i] = 0; // zero-word SPIR-V instruction
|
malformed[20 + i] = 0; // zero-word SPIR-V instruction
|
||||||
@@ -170,8 +203,8 @@ int main() {
|
|||||||
atomic_write_json(bundle / "fragmentMain.reflection.json", metadata);
|
atomic_write_json(bundle / "fragmentMain.reflection.json", metadata);
|
||||||
retained();
|
retained();
|
||||||
restore();
|
restore();
|
||||||
auto incompatible = original_source;
|
incompatible = original_source;
|
||||||
auto at = incompatible.find("[[vk::binding(2,0)]]");
|
at = incompatible.find("[[vk::binding(2,0)]]");
|
||||||
require(at != std::string::npos, "Shader descriptor fixture exists");
|
require(at != std::string::npos, "Shader descriptor fixture exists");
|
||||||
incompatible.replace(at, std::string("[[vk::binding(2,0)]]").size(),
|
incompatible.replace(at, std::string("[[vk::binding(2,0)]]").size(),
|
||||||
"[[vk::binding(7,0)]]");
|
"[[vk::binding(7,0)]]");
|
||||||
|
|||||||
@@ -134,6 +134,65 @@ int main(int argc, char** argv) {
|
|||||||
renderer.render(scene);
|
renderer.render(scene);
|
||||||
pixels = renderer.pixels();
|
pixels = renderer.pixels();
|
||||||
require(pixels[index + 2] > 220, "Texture revision upload");
|
require(pixels[index + 2] > 220, "Texture revision upload");
|
||||||
|
Snapshot two_lights;
|
||||||
|
two_lights.eye = {0, 0, 6};
|
||||||
|
two_lights.projection = perspective(.85f, 320.f / 240.f, .1f, 30.f);
|
||||||
|
two_lights.view_projection =
|
||||||
|
multiply(two_lights.projection, look_at(two_lights.eye, {0, 0, 0}));
|
||||||
|
two_lights.authored_lights_present = true;
|
||||||
|
two_lights.draws.push_back(
|
||||||
|
{cube_mesh(), transform({-1.4f, 0, 0}), {.5f, .5f, .5f, 1}, .6f, 0, false});
|
||||||
|
two_lights.draws.back().instance_key = "left-light-receiver";
|
||||||
|
two_lights.draws.push_back(
|
||||||
|
{cube_mesh(), transform({1.4f, 0, 0}), {.5f, .5f, .5f, 1}, .6f, 0, false});
|
||||||
|
two_lights.draws.back().instance_key = "right-light-receiver";
|
||||||
|
two_lights.ui_quads.push_back({8, 8, 40, 20, {.8f, .1f, .15f, 1}});
|
||||||
|
for (auto mode : {VisibilityMode::Direct, VisibilityMode::GpuFrustum}) {
|
||||||
|
renderer.set_visibility_mode(mode);
|
||||||
|
renderer.render(two_lights);
|
||||||
|
const auto dark = renderer.pixels();
|
||||||
|
require(renderer.stats().validation_errors == 0,
|
||||||
|
"Zero-local-light descriptors are initialized");
|
||||||
|
auto legacy_lights = two_lights;
|
||||||
|
legacy_lights.authored_lights_present = false;
|
||||||
|
renderer.render(legacy_lights);
|
||||||
|
const auto legacy = renderer.pixels();
|
||||||
|
const auto left = (120 * 320 + 99) * 4;
|
||||||
|
require(legacy[left] > dark[left] + 15,
|
||||||
|
"Authored-light presence suppresses the legacy sun even without a local light");
|
||||||
|
two_lights.local_lights = {
|
||||||
|
{LocalLight::Kind::Point, "red", {-1.4f, 0, 1.4f}, {0, 0, -1},
|
||||||
|
{1, 0, 0, 1}, 8, 2.2f, .35f, .7f, false, 0},
|
||||||
|
{LocalLight::Kind::Point, "blue", {1.4f, 0, 1.4f}, {0, 0, -1},
|
||||||
|
{0, 0, 1, 1}, 8, 2.5f, .35f, .7f, false, 0}};
|
||||||
|
renderer.render(two_lights);
|
||||||
|
const auto lit = renderer.pixels();
|
||||||
|
const auto right = (120 * 320 + 221) * 4;
|
||||||
|
const auto ui = (10 * 320 + 10) * 4;
|
||||||
|
require(lit[left] > dark[left] + 20 && lit[right + 2] > dark[right + 2] + 20,
|
||||||
|
"Separated red and blue point lights illuminate their receivers");
|
||||||
|
require(std::abs(int(lit[left + 2]) - int(dark[left + 2])) < 6 &&
|
||||||
|
std::abs(int(lit[right]) - int(dark[right])) < 6,
|
||||||
|
"Local light range keeps the opposite colored light off each receiver");
|
||||||
|
for (int channel = 0; channel < 4; ++channel)
|
||||||
|
require(lit[ui + channel] == dark[ui + channel],
|
||||||
|
"Lighting changes leave UI tint unchanged");
|
||||||
|
require(renderer.stats().validation_errors == 0,
|
||||||
|
"Direct and GPU local lighting report no Vulkan errors");
|
||||||
|
if (mode == VisibilityMode::GpuFrustum)
|
||||||
|
require(renderer.stats().effective_visibility_mode == VisibilityMode::GpuFrustum,
|
||||||
|
"Local light image test actually exercises the GPU visibility path");
|
||||||
|
two_lights.local_lights[1].kind = LocalLight::Kind::Spot;
|
||||||
|
renderer.render(two_lights);
|
||||||
|
const auto aimed = renderer.pixels();
|
||||||
|
two_lights.local_lights[1].direction = {1, 0, 0};
|
||||||
|
renderer.render(two_lights);
|
||||||
|
const auto turned = renderer.pixels();
|
||||||
|
require(aimed[right + 2] > turned[right + 2] + 20,
|
||||||
|
"Spotlight cone direction changes receiver illumination");
|
||||||
|
two_lights.local_lights.clear();
|
||||||
|
}
|
||||||
|
renderer.set_visibility_mode(VisibilityMode::Direct);
|
||||||
if (argc > 2)
|
if (argc > 2)
|
||||||
renderer.capture(argv[2]);
|
renderer.capture(argv[2]);
|
||||||
renderer.resize(400, 300);
|
renderer.resize(400, 300);
|
||||||
|
|||||||
@@ -134,6 +134,18 @@ void lightingExtraction(faset::player::SceneView& view) {
|
|||||||
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "inner_angle");
|
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "inner_angle");
|
||||||
scene["entities"][0]["components"][0]["fields"] = {{"kind", "area"}};
|
scene["entities"][0]["components"][0]["fields"] = {{"kind", "area"}};
|
||||||
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "kind");
|
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "kind");
|
||||||
|
scene["entities"][0]["components"][0]["fields"] =
|
||||||
|
{{"kind", "point"}, {"shadow_priority", std::int64_t{2147483648}}};
|
||||||
|
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "shadow_priority");
|
||||||
|
scene["entities"][0]["components"][0]["fields"] = {{"kind", "point"}};
|
||||||
|
scene["entities"][0]["components"].insert(
|
||||||
|
scene["entities"][0]["components"].begin(),
|
||||||
|
component("faset.transform", {{"scale", {1, 1, 0}}}));
|
||||||
|
const auto flatPoint = view.build(scene, 1);
|
||||||
|
check(flatPoint.local_lights.size() == 1 &&
|
||||||
|
flatPoint.local_lights[0].kind == faset::render::LocalLight::Kind::Point,
|
||||||
|
"Point light accepts a zero Z scale because it needs only a position");
|
||||||
|
scene["entities"][0]["components"].erase(scene["entities"][0]["components"].begin());
|
||||||
scene["entities"][0]["components"][0]["fields"] =
|
scene["entities"][0]["components"][0]["fields"] =
|
||||||
{{"kind", "point"},
|
{{"kind", "point"},
|
||||||
{"color", Json::array({1, std::numeric_limits<double>::quiet_NaN(), 1, 1})}};
|
{"color", Json::array({1, std::numeric_limits<double>::quiet_NaN(), 1, 1})}};
|
||||||
|
|||||||
@@ -36,6 +36,35 @@ def parameter(name: str, index: int, shape: str, access: str, stride: int | None
|
|||||||
|
|
||||||
|
|
||||||
class ReflectionTests(unittest.TestCase):
|
class ReflectionTests(unittest.TestCase):
|
||||||
|
def test_graphics_lighting_abi(self):
|
||||||
|
compiler = os.environ["FASET_TEST_SLANGC"]
|
||||||
|
with tempfile.TemporaryDirectory(prefix="faset-lighting-abi-") as directory:
|
||||||
|
for source, entry, defines in (
|
||||||
|
("baseline.slang", "fragmentMain", []),
|
||||||
|
("gpu_scene.slang", "gpuVertexMain", ["--define", "FASET_GPU_GRAPHICS=1"]),
|
||||||
|
):
|
||||||
|
process = subprocess.run(
|
||||||
|
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
|
||||||
|
str(SCRIPT.parents[1] / "shaders" / source), "--entry", entry,
|
||||||
|
*defines, "--output", directory],
|
||||||
|
capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(process.returncode, 0, process.stderr)
|
||||||
|
fragment = json.loads((Path(directory) / "fragmentMain.reflection.json").read_text())
|
||||||
|
gpu_vertex = json.loads((Path(directory) / "gpuVertexMain.reflection.json").read_text())
|
||||||
|
lighting = {
|
||||||
|
(d["set"], d["binding"]): (d["type"], d.get("element_stride"))
|
||||||
|
for d in fragment["layout"]["descriptors"]
|
||||||
|
}
|
||||||
|
self.assertEqual([lighting[1, i] for i in range(4)],
|
||||||
|
[("storage_buffer", 80), ("storage_buffer", 80),
|
||||||
|
("storage_buffer", 112), ("sampled_image_2d", None)])
|
||||||
|
graphics = {
|
||||||
|
(d["set"], d["binding"]): d["element_stride"]
|
||||||
|
for d in gpu_vertex["layout"]["descriptors"]
|
||||||
|
}
|
||||||
|
self.assertEqual([graphics[2, i] for i in range(3)], [224, 4, 208])
|
||||||
|
|
||||||
def test_gpu_vertex_paths_do_not_require_shader_draw_parameters(self):
|
def test_gpu_vertex_paths_do_not_require_shader_draw_parameters(self):
|
||||||
# SV_InstanceID makes Slang subtract BaseInstance and emit DrawParameters.
|
# SV_InstanceID makes Slang subtract BaseInstance and emit DrawParameters.
|
||||||
# Our indirect commands always use firstInstance=0, so the Vulkan instance
|
# Our indirect commands always use firstInstance=0, so the Vulkan instance
|
||||||
|
|||||||
Reference in New Issue
Block a user