This commit is contained in:
Scott Duensing 2026-09-05 19:07:04 -05:00
parent 0060116835
commit 3fdee3d9b6
11 changed files with 3926 additions and 1723 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ docs/.asciidoctor/
/Menu.sh /Menu.sh
/Menu.bat /Menu.bat
/data/ /data/
screenshots/

View file

@ -21,7 +21,8 @@ API Changes
- 3D scenes. A game can draw a 3D scene between the disc video and the - 3D scenes. A game can draw a 3D scene between the disc video and the
overlay: primitive meshes and script-built geometry, materials with overlay: primitive meshes and script-built geometry, materials with
colour, textures from sprites, the disc or a loaded video, metallic and colour, textures from sprites, the disc or a loaded video, metallic and
roughness, up to eight lights, any node as the camera, and glTF 2.0 roughness, up to eight lights each able to cast shadows (cube maps for
point lights), any node as the camera, and glTF 2.0
models (.glb, self-contained) with node animation and skinning, placed models (.glb, self-contained) with node animation and skinning, placed
any number of times. Everything is a node in one tree. New calls: any number of times. Everything is a node in one tree. New calls:
scene*, node*, mesh*, material*, light*, camera*, model*, animation*, scene*, node*, mesh*, material*, light*, camera*, model*, animation*,

View file

@ -759,7 +759,10 @@ Up to eight lights shine at once: `LIGHT_DIRECTIONAL` (a sun, position
irrelevant), `LIGHT_POINT` (a bulb, fading with distance, optionally out to irrelevant), `LIGHT_POINT` (a bulb, fading with distance, optionally out to
a range), and `LIGHT_SPOT` (a cone with inner and outer angles). Lights are a range), and `LIGHT_SPOT` (a cone with inner and outer angles). Lights are
nodes, so they parent and animate like anything else. `sceneSetAmbient` nodes, so they parent and animate like anything else. `sceneSetAmbient`
lights everything a little from everywhere. Any node can be the camera lights everything a little from everywhere. Any light can cast soft-edged
shadows from everything opaque (`lightSetShadow`): directional and spot
lights through a shadow map, point lights through a cube map that reaches
every direction; `sceneSetShadowSize` trades sharpness for speed. Any node can be the camera
(`cameraSet`); without one, the scene is viewed from `(0, 0, 5)` looking at (`cameraSet`); without one, the scene is viewed from `(0, 0, 5)` looking at
the origin. Perspective is the default; `cameraSetOrthographic` is there the origin. Perspective is the default; `cameraSetOrthographic` is there
for diagrams and HUD-like scenes. for diagrams and HUD-like scenes.
@ -2143,6 +2146,31 @@ How far a point or spot light reaches before fading to nothing, in world units;
*Since:* 3.00. *Since:* 3.00.
*See also:* <<lightsetintensity,lightSetIntensity>> *See also:* <<lightsetintensity,lightSetIntensity>>
[#lightsetshadow]
==== lightSetShadow
[source,text]
----
lightSetShadow(node, shadow)
----
Makes the light cast shadows from everything opaque in the scene, or stops it. Any of the eight lights may cast, in any mix. A directional or spot light gets one shadow map; a point light gets a cube map, six views, which covers every direction (a bulb inside a room) at six times the cost.
*Notes:* Each shadow map costs one extra pass over the scene per frame, and each cube map six; the maps' size is set by `sceneSetShadowSize`. A modern integrated GPU manages eight casters comfortably; a Raspberry Pi wants one or two at `512`.
*Since:* 3.00.
*See also:* <<scenesetshadowsize,sceneSetShadowSize>>, <<lightnew,lightNew>>
.Example
[source,lua]
----
-- The sun casts shadows; the lamps only light.
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 3, 6, 4)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
----
[#material] [#material]
=== Material === Material
@ -3374,6 +3402,19 @@ The colour the layer clears to each frame. `a` below `255` lets the video show t
*Since:* 3.00. *Since:* 3.00.
*See also:* <<sceneenable,sceneEnable>> *See also:* <<sceneenable,sceneEnable>>
[#scenesetshadowsize]
==== sceneSetShadowSize
[source,text]
----
sceneSetShadowSize(size)
----
Texels per side of every shadow map and cube face, `256` to `4096` (default `1024`). Larger is sharper and slower; `512` is a fair choice on a Raspberry Pi.
*Since:* 3.00.
*See also:* <<lightsetshadow,lightSetShadow>>, <<scenesetantialias,sceneSetAntialias>>
[#script] [#script]
=== Script === Script

View file

@ -205,6 +205,22 @@ Mat4T mat4Orthographic(float width, float height, float near, float far) {
} }
// An off-centre parallel projection (a shadow map fitted to what it covers).
Mat4T mat4OrthographicBounds(float left, float right, float bottom, float top, float near, float far) {
Mat4T out;
memset(&out, 0, sizeof(out));
out.m[0] = 2.0f / (right - left);
out.m[5] = 2.0f / (top - bottom);
out.m[10] = -1.0f / (far - near);
out.m[12] = -(right + left) / (right - left);
out.m[13] = -(top + bottom) / (top - bottom);
out.m[14] = -near / (far - near);
out.m[15] = 1.0f;
return out;
}
Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far) { Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far) {
Mat4T out; Mat4T out;
float f = 1.0f / tanf(DEGREES_TO_RADIANS(fovDegrees) / 2.0f); float f = 1.0f / tanf(DEGREES_TO_RADIANS(fovDegrees) / 2.0f);

View file

@ -62,6 +62,7 @@ bool mat4Invert(Mat4T a, Mat4T *out);
Mat4T mat4LookAt(Vec3T eye, Vec3T target, Vec3T up); Mat4T mat4LookAt(Vec3T eye, Vec3T target, Vec3T up);
Mat4T mat4Multiply(Mat4T a, Mat4T b); Mat4T mat4Multiply(Mat4T a, Mat4T b);
Mat4T mat4Orthographic(float width, float height, float near, float far); Mat4T mat4Orthographic(float width, float height, float near, float far);
Mat4T mat4OrthographicBounds(float left, float right, float bottom, float top, float near, float far);
Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far); Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far);
Vec3T mat4TransformPoint(Mat4T a, Vec3T p); Vec3T mat4TransformPoint(Mat4T a, Vec3T p);
Vec3T mat4TransformVector(Mat4T a, Vec3T v); Vec3T mat4TransformVector(Mat4T a, Vec3T v);

View file

@ -52,6 +52,18 @@
#define MIN_SEGMENTS 3 #define MIN_SEGMENTS 3
#define PI 3.14159265358979323846f #define PI 3.14159265358979323846f
#define NO_HANDLE -1 #define NO_HANDLE -1
#define SHADOW_PIPELINES 4 // skinned x double sided
#define MAX_SHADOWS MAX_LIGHTS // Every light may cast; the arrays are sized to the lights in use
#define CUBE_FACES 6
#define SHADOW_NONE 0
#define SHADOW_MAP 1
#define SHADOW_CUBE 2
#define SHADOW_SIZE 1024
#define SHADOW_SIZE_MIN 256
#define SHADOW_SIZE_MAX 4096
#define SHADOW_BIAS 0.0015f
#define SHADOW_MARGIN 1.05f // The fitted light frustum, a little larger than the scene
#define SKIN_BOUNDS_GROW 1.5f // A skinned mesh moves beyond its bind pose
// Matches DrawUniforms in scene.hlsl. // Matches DrawUniforms in scene.hlsl.
@ -76,13 +88,29 @@ typedef struct FragmentUniformsS {
float emissive[4]; float emissive[4];
float material[4]; float material[4];
float counts[4]; float counts[4];
float shadowParams[4];
Mat4T shadowMatrix[MAX_SHADOWS];
float shadowInfo[MAX_SHADOWS][4];
LightUniformT lights[MAX_LIGHTS]; LightUniformT lights[MAX_LIGHTS];
} FragmentUniformsT; } FragmentUniformsT;
// One light's shadow for this frame.
typedef struct ShadowS {
int32_t node;
int32_t type; // SHADOW_MAP or SHADOW_CUBE
int32_t layer; // First layer in the shadow array (a point light uses six)
Mat4T matrix; // Map: the light's view-projection
Mat4T faces[CUBE_FACES]; // Cube: one per face
float near;
float far;
} ShadowT;
typedef struct MeshS { typedef struct MeshS {
SDL_GPUBuffer *vertexBuffer; SDL_GPUBuffer *vertexBuffer;
SDL_GPUBuffer *indexBuffer; SDL_GPUBuffer *indexBuffer;
uint32_t indexCount; uint32_t indexCount;
Vec3T boundsMin; // Of the vertices, for fitting the shadow map
Vec3T boundsMax;
bool skinned; bool skinned;
bool used; bool used;
} MeshT; } MeshT;
@ -130,6 +158,7 @@ typedef struct NodeS {
int32_t mesh; int32_t mesh;
int32_t material; int32_t material;
LightT light; LightT light;
bool castsShadow;
int32_t *skinJoints; // Nodes whose world matrices drive a skinned mesh int32_t *skinJoints; // Nodes whose world matrices drive a skinned mesh
Mat4T *skinInverseBind; Mat4T *skinInverseBind;
int32_t skinCount; int32_t skinCount;
@ -163,8 +192,18 @@ typedef struct SceneS {
SDL_GPUShader *vertexStatic; SDL_GPUShader *vertexStatic;
SDL_GPUShader *vertexSkinned; SDL_GPUShader *vertexSkinned;
SDL_GPUShader *fragment; SDL_GPUShader *fragment;
SDL_GPUShader *depthFragment; // Empty; the shadow pass writes depth only
SDL_GPUGraphicsPipeline *pipelines[PIPELINE_COUNT]; SDL_GPUGraphicsPipeline *pipelines[PIPELINE_COUNT];
SDL_GPUGraphicsPipeline *shadowPipelines[SHADOW_PIPELINES];
SDL_GPUSampler *sampler; SDL_GPUSampler *sampler;
SDL_GPUSampler *shadowSampler;
SDL_GPUTexture *shadowMaps; // 2D array: a layer per directional or spot shadow, six per point light
SDL_GPUTexture *shadowMapsNone; // 1x1 stand-in bound when nothing casts, so the shader's slot is filled
int32_t shadowMapLayers;
SDL_GPUTextureFormat shadowFormat;
int32_t shadowSize;
ShadowT shadows[MAX_SHADOWS];
int32_t shadowCount;
SDL_GPUTexture *white; // 1x1 stand-in for untextured materials SDL_GPUTexture *white; // 1x1 stand-in for untextured materials
SDL_FColor background; SDL_FColor background;
Vec3T ambient; Vec3T ambient;
@ -198,13 +237,20 @@ static int32_t _allocNode(void);
static void _attach(int32_t node, int32_t parent); static void _attach(int32_t node, int32_t parent);
static int32_t _compareDraws(const void *a, const void *b); static int32_t _compareDraws(const void *a, const void *b);
static bool _createPipeline(int32_t variant); static bool _createPipeline(int32_t variant);
static SDL_GPUTexture *_createShadowArray(SDL_GPUTextureType type, int32_t layers, int32_t size);
static bool _createShadowMaps(int32_t layers);
static bool _createShadowPipeline(int32_t variant);
static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms); static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms);
static bool _createShaders(void); static bool _createShaders(void);
static SDL_GPUTextureFormat _depthFormat(void); static SDL_GPUTextureFormat _depthFormat(void);
static SDL_GPUTextureFormat _shadowFormat(void);
static void _destroyTargets(void); static void _destroyTargets(void);
static void _detach(int32_t node); static void _detach(int32_t node);
static void _destroyPipelines(void); static void _destroyPipelines(void);
static void _destroyShadowMaps(void);
static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, FragmentUniformsT *fragmentUniforms);
static void _fillLights(FragmentUniformsT *uniforms); static void _fillLights(FragmentUniformsT *uniforms);
static void _fitShadows(int32_t drawCount);
static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms); static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms);
static void _freeFeed(FeedT *feed); static void _freeFeed(FeedT *feed);
static void _freeSkin(NodeT *node); static void _freeSkin(NodeT *node);
@ -257,7 +303,24 @@ static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const
mesh->indexCount = (uint32_t)indexCount; mesh->indexCount = (uint32_t)indexCount;
mesh->skinned = skinned; mesh->skinned = skinned;
mesh->used = true; mesh->used = true;
return x; mesh->boundsMin = vec3(vertices[0].position[0], vertices[0].position[1], vertices[0].position[2]);
mesh->boundsMax = mesh->boundsMin;
for (x = 1; x < vertexCount; x++) {
mesh->boundsMin.x = SDL_min(mesh->boundsMin.x, vertices[x].position[0]);
mesh->boundsMin.y = SDL_min(mesh->boundsMin.y, vertices[x].position[1]);
mesh->boundsMin.z = SDL_min(mesh->boundsMin.z, vertices[x].position[2]);
mesh->boundsMax.x = SDL_max(mesh->boundsMax.x, vertices[x].position[0]);
mesh->boundsMax.y = SDL_max(mesh->boundsMax.y, vertices[x].position[1]);
mesh->boundsMax.z = SDL_max(mesh->boundsMax.z, vertices[x].position[2]);
}
if (skinned) {
Vec3T centre = vec3Scale(vec3Add(mesh->boundsMin, mesh->boundsMax), 0.5f);
Vec3T half = vec3Scale(vec3Subtract(mesh->boundsMax, mesh->boundsMin), 0.5f * SKIN_BOUNDS_GROW);
mesh->boundsMin = vec3Subtract(centre, half);
mesh->boundsMax = vec3Add(centre, half);
}
return (int32_t)(mesh - _scene.meshes);
} }
@ -490,8 +553,101 @@ static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderSta
static bool _createShaders(void) { static bool _createShaders(void) {
_scene.vertexStatic = _createShader(&sceneShaderVertexStatic, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1); _scene.vertexStatic = _createShader(&sceneShaderVertexStatic, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1);
_scene.vertexSkinned = _createShader(&sceneShaderVertexSkinned, SDL_GPU_SHADERSTAGE_VERTEX, 0, 2); _scene.vertexSkinned = _createShader(&sceneShaderVertexSkinned, SDL_GPU_SHADERSTAGE_VERTEX, 0, 2);
_scene.fragment = _createShader(&sceneShaderFragmentMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); _scene.fragment = _createShader(&sceneShaderFragmentMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 2, 1);
return (_scene.vertexStatic != NULL) && (_scene.vertexSkinned != NULL) && (_scene.fragment != NULL); _scene.depthFragment = _createShader(&sceneShaderDepthMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0);
return (_scene.vertexStatic != NULL) && (_scene.vertexSkinned != NULL) && (_scene.fragment != NULL) && (_scene.depthFragment != NULL);
}
// A depth texture array the shadow passes render into and the main pass samples.
static SDL_GPUTexture *_createShadowArray(SDL_GPUTextureType type, int32_t layers, int32_t size) {
SDL_GPUTextureCreateInfo info;
SDL_GPUTexture *texture;
memset(&info, 0, sizeof(info));
info.type = type;
info.format = _scene.shadowFormat;
info.usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
info.width = (Uint32)size;
info.height = (Uint32)size;
info.layer_count_or_depth = (Uint32)layers;
info.num_levels = 1;
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture = SDL_CreateGPUTexture(_scene.device, &info);
if (texture == NULL) {
utilTrace("Scene: shadow map: %s", SDL_GetError());
}
return texture;
}
// Makes sure the array has room for this frame's shadows; it grows to the most used so far.
static bool _createShadowMaps(int32_t layers) {
if (layers > _scene.shadowMapLayers) {
if (_scene.shadowMaps != NULL) {
SDL_ReleaseGPUTexture(_scene.device, _scene.shadowMaps);
}
_scene.shadowMaps = _createShadowArray(SDL_GPU_TEXTURETYPE_2D_ARRAY, layers, _scene.shadowSize);
_scene.shadowMapLayers = (_scene.shadowMaps != NULL) ? layers : 0;
}
return _scene.shadowMaps != NULL;
}
// Depth-only pipelines for the shadow pass: the scene's vertex shaders with an empty fragment
// shader, no colour target, and a depth bias against self-shadowing.
static bool _createShadowPipeline(int32_t variant) {
SDL_GPUGraphicsPipelineCreateInfo info;
SDL_GPUVertexBufferDescription buffers[1];
SDL_GPUVertexAttribute attributes[5];
memset(&info, 0, sizeof(info));
memset(buffers, 0, sizeof(buffers));
memset(attributes, 0, sizeof(attributes));
buffers[0].slot = 0;
buffers[0].pitch = sizeof(SceneVertexT);
buffers[0].input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
attributes[0].location = 0;
attributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3;
attributes[0].offset = offsetof(SceneVertexT, position);
attributes[1].location = 1;
attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3;
attributes[1].offset = offsetof(SceneVertexT, normal);
attributes[2].location = 2;
attributes[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attributes[2].offset = offsetof(SceneVertexT, uv);
attributes[3].location = 3;
attributes[3].format = SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4;
attributes[3].offset = offsetof(SceneVertexT, joints);
attributes[4].location = 4;
attributes[4].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
attributes[4].offset = offsetof(SceneVertexT, weights);
info.vertex_shader = (variant & PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic;
info.fragment_shader = _scene.depthFragment;
info.vertex_input_state.vertex_buffer_descriptions = buffers;
info.vertex_input_state.num_vertex_buffers = 1;
info.vertex_input_state.vertex_attributes = attributes;
info.vertex_input_state.num_vertex_attributes = 5;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = (variant & PIPELINE_TWO_SIDED) ? SDL_GPU_CULLMODE_NONE : SDL_GPU_CULLMODE_BACK;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
info.rasterizer_state.enable_depth_bias = true;
info.rasterizer_state.depth_bias_constant_factor = 2.0f;
info.rasterizer_state.depth_bias_slope_factor = 2.0f;
info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL;
info.depth_stencil_state.enable_depth_test = true;
info.depth_stencil_state.enable_depth_write = true;
info.target_info.num_color_targets = 0;
info.target_info.depth_stencil_format = _scene.shadowFormat;
info.target_info.has_depth_stencil_target = true;
_scene.shadowPipelines[variant] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info);
if (_scene.shadowPipelines[variant] == NULL) {
utilTrace("Scene: shadow pipeline %d: %s", variant, SDL_GetError());
return false;
}
return true;
} }
@ -520,6 +676,21 @@ static void _destroyPipelines(void) {
_scene.pipelines[x] = NULL; _scene.pipelines[x] = NULL;
} }
} }
for (x = 0; x < SHADOW_PIPELINES; x++) {
if (_scene.shadowPipelines[x] != NULL) {
SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.shadowPipelines[x]);
_scene.shadowPipelines[x] = NULL;
}
}
}
static void _destroyShadowMaps(void) {
if (_scene.shadowMaps != NULL) {
SDL_ReleaseGPUTexture(_scene.device, _scene.shadowMaps);
_scene.shadowMaps = NULL;
}
_scene.shadowMapLayers = 0;
} }
@ -546,6 +717,108 @@ static void _destroyTargets(void) {
} }
// Issues the collected draws into a pass: the shadow pass with the light's view-projection and
// depth-only pipelines (blended meshes cast nothing), or the main pass with the camera's and the
// full material.
static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, FragmentUniformsT *fragmentUniforms) {
SDL_GPUBufferBinding binding;
SDL_GPUTextureSamplerBinding samplerBindings[2];
DrawUniformsT drawUniforms;
SkinUniformsT *skinUniforms = NULL;
Mat4T identity = mat4Identity();
Mat4T inverse;
int32_t x;
int32_t lastPipeline = NO_HANDLE;
int32_t variant;
NodeT *node;
MeshT *mesh;
MaterialT *material;
MaterialT defaultMaterial;
SDL_GPUGraphicsPipeline *pipeline;
memset(&defaultMaterial, 0, sizeof(defaultMaterial));
defaultMaterial.baseColor.x = 1.0f;
defaultMaterial.baseColor.y = 1.0f;
defaultMaterial.baseColor.z = 1.0f;
defaultMaterial.baseColor.w = 1.0f;
defaultMaterial.roughness = 0.5f;
defaultMaterial.feed = NO_HANDLE;
for (x = 0; x < drawCount; x++) {
node = &_scene.nodes[_scene.draws[x].node];
mesh = &_scene.meshes[node->mesh];
material = (node->material != NO_HANDLE) ? &_scene.materials[node->material] : &defaultMaterial;
variant = _lookupPipeline(_scene.draws[x].node);
if (variant == NO_HANDLE) {
continue;
}
if (shadowPass) {
if (material->blend) {
continue;
}
variant &= PIPELINE_SKINNED | PIPELINE_TWO_SIDED;
variant = (variant & PIPELINE_SKINNED ? 1 : 0) | (variant & PIPELINE_TWO_SIDED ? 2 : 0);
if ((_scene.shadowPipelines[variant] == NULL) && !_createShadowPipeline(variant)) {
continue;
}
pipeline = _scene.shadowPipelines[variant];
} else {
pipeline = _scene.pipelines[variant];
}
if (variant != lastPipeline) {
SDL_BindGPUGraphicsPipeline(pass, pipeline);
lastPipeline = variant;
}
drawUniforms.modelViewProjection = mat4Multiply(*viewProjection, node->world);
drawUniforms.model = node->world;
if (mat4Invert(node->world, &inverse)) {
drawUniforms.normalMatrix = mat4Transpose(inverse);
} else {
drawUniforms.normalMatrix = identity;
}
SDL_PushGPUVertexUniformData(commands, 0, &drawUniforms, sizeof(drawUniforms));
if (mesh->skinned && (node->skinCount > 0)) {
// 8 KB per skinned draw; allocated once per pass that needs it.
if (skinUniforms == NULL) {
skinUniforms = SDL_malloc(sizeof(SkinUniformsT));
if (skinUniforms == NULL) {
utilDie("Out of memory posing a skin.");
}
}
_fillSkin(node, skinUniforms);
SDL_PushGPUVertexUniformData(commands, 1, skinUniforms, sizeof(SkinUniformsT));
}
if (!shadowPass) {
fragmentUniforms->baseColor[0] = material->baseColor.x;
fragmentUniforms->baseColor[1] = material->baseColor.y;
fragmentUniforms->baseColor[2] = material->baseColor.z;
fragmentUniforms->baseColor[3] = material->baseColor.w;
fragmentUniforms->emissive[0] = material->emissive.x;
fragmentUniforms->emissive[1] = material->emissive.y;
fragmentUniforms->emissive[2] = material->emissive.z;
fragmentUniforms->emissive[3] = 1.0f;
fragmentUniforms->material[0] = material->metallic;
fragmentUniforms->material[1] = material->roughness;
fragmentUniforms->material[2] = material->unlit ? 1.0f : 0.0f;
fragmentUniforms->material[3] = (_materialTexture(material) != NULL) ? 1.0f : 0.0f;
SDL_PushGPUFragmentUniformData(commands, 0, fragmentUniforms, sizeof(FragmentUniformsT));
memset(samplerBindings, 0, sizeof(samplerBindings));
samplerBindings[0].texture = (_materialTexture(material) != NULL) ? _materialTexture(material) : _scene.white;
samplerBindings[0].sampler = _scene.sampler;
samplerBindings[1].texture = (_scene.shadowMaps != NULL) ? _scene.shadowMaps : _scene.shadowMapsNone;
samplerBindings[1].sampler = _scene.shadowSampler;
SDL_BindGPUFragmentSamplers(pass, 0, samplerBindings, 2);
}
memset(&binding, 0, sizeof(binding));
binding.buffer = mesh->vertexBuffer;
SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
binding.buffer = mesh->indexBuffer;
SDL_BindGPUIndexBuffer(pass, &binding, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_DrawGPUIndexedPrimitives(pass, mesh->indexCount, 1, 0, 0, 0);
}
SDL_free(skinUniforms);
}
// Unlinks node from its parent's child list. // Unlinks node from its parent's child list.
static void _detach(int32_t node) { static void _detach(int32_t node) {
int32_t parent = _scene.nodes[node].parent; int32_t parent = _scene.nodes[node].parent;
@ -570,10 +843,12 @@ static void _detach(int32_t node) {
} }
// The first MAX_LIGHTS visible lights, in world space. // The first MAX_LIGHTS visible lights, in world space, and the shadow slots: every one of them
// flagged to cast gets a slot (a map for directional and spot lights, a cube for point lights).
static void _fillLights(FragmentUniformsT *uniforms) { static void _fillLights(FragmentUniformsT *uniforms) {
int32_t x; int32_t x;
int32_t count = 0; int32_t count = 0;
int32_t layers = 0;
NodeT *node; NodeT *node;
LightUniformT *light; LightUniformT *light;
Vec3T position; Vec3T position;
@ -603,6 +878,22 @@ static void _fillLights(FragmentUniformsT *uniforms) {
light->cone[1] = cosf(node->light.outerDegrees * PI / 180.0f); light->cone[1] = cosf(node->light.outerDegrees * PI / 180.0f);
light->cone[2] = 0.0f; light->cone[2] = 0.0f;
light->cone[3] = 0.0f; light->cone[3] = 0.0f;
if (node->castsShadow && (_scene.shadowCount < MAX_SHADOWS)) {
ShadowT *shadow = &_scene.shadows[_scene.shadowCount];
memset(shadow, 0, sizeof(*shadow));
shadow->node = x;
shadow->layer = layers;
if (node->light.type == LIGHT_POINT) {
shadow->type = SHADOW_CUBE;
layers += CUBE_FACES;
} else {
shadow->type = SHADOW_MAP;
layers++;
}
light->cone[2] = (float)(_scene.shadowCount + 1);
_scene.shadowCount++;
}
count++; count++;
} }
uniforms->counts[0] = (float)count; uniforms->counts[0] = (float)count;
@ -633,6 +924,102 @@ static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms) {
} }
// Every shadow's projection, fitted to what is drawn: a directional light gets a parallel box
// round the scene's bounds, a spot light its own cone, a point light six 90 degree faces out to
// its range or the far edge of the scene.
static void _fitShadows(int32_t drawCount) {
Vec3T corners[8];
Vec3T boundsMin = vec3(0.0f, 0.0f, 0.0f);
Vec3T boundsMax = vec3(0.0f, 0.0f, 0.0f);
Vec3T centre;
float radius;
int32_t x;
int32_t c;
int32_t slot;
bool any = false;
// The six faces of a point light's shadow: the direction each looks and its up. The fragment
// shader rebuilds the same frames to look them up, so these need only match it.
Vec3T faceForward[CUBE_FACES] = { { 1.0f, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } };
Vec3T faceUp[CUBE_FACES] = { { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } };
// World bounds of everything drawn.
for (x = 0; x < drawCount; x++) {
NodeT *node = &_scene.nodes[_scene.draws[x].node];
MeshT *mesh = &_scene.meshes[node->mesh];
for (c = 0; c < 8; c++) {
Vec3T corner = vec3((c & 1) ? mesh->boundsMax.x : mesh->boundsMin.x, (c & 2) ? mesh->boundsMax.y : mesh->boundsMin.y, (c & 4) ? mesh->boundsMax.z : mesh->boundsMin.z);
corner = mat4TransformPoint(node->world, corner);
if (!any) {
boundsMin = corner;
boundsMax = corner;
any = true;
} else {
boundsMin = vec3(SDL_min(boundsMin.x, corner.x), SDL_min(boundsMin.y, corner.y), SDL_min(boundsMin.z, corner.z));
boundsMax = vec3(SDL_max(boundsMax.x, corner.x), SDL_max(boundsMax.y, corner.y), SDL_max(boundsMax.z, corner.z));
}
}
}
centre = vec3Scale(vec3Add(boundsMin, boundsMax), 0.5f);
radius = vec3Length(vec3Subtract(boundsMax, centre)) * SHADOW_MARGIN + 0.001f;
for (c = 0; c < 8; c++) {
corners[c] = vec3((c & 1) ? boundsMax.x : boundsMin.x, (c & 2) ? boundsMax.y : boundsMin.y, (c & 4) ? boundsMax.z : boundsMin.z);
}
for (slot = 0; slot < _scene.shadowCount; slot++) {
ShadowT *shadow = &_scene.shadows[slot];
NodeT *light = &_scene.nodes[shadow->node];
Vec3T direction = vec3Normalize(mat4TransformVector(light->world, vec3(0.0f, 0.0f, -1.0f)));
Vec3T position = mat4TransformPoint(light->world, vec3(0.0f, 0.0f, 0.0f));
Vec3T up = (fabsf(direction.y) < 0.99f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f);
Mat4T view;
float far;
float minX = 0.0f;
float maxX = 0.0f;
float minY = 0.0f;
float maxY = 0.0f;
float minZ = 0.0f;
float maxZ = 0.0f;
if (light->light.type == LIGHT_DIRECTIONAL) {
view = mat4LookAt(vec3Subtract(centre, vec3Scale(direction, radius * 2.0f)), centre, up);
for (c = 0; c < 8; c++) {
Vec3T v = mat4TransformPoint(view, corners[c]);
if (c == 0) {
minX = v.x;
maxX = v.x;
minY = v.y;
maxY = v.y;
minZ = v.z;
maxZ = v.z;
} else {
minX = SDL_min(minX, v.x);
maxX = SDL_max(maxX, v.x);
minY = SDL_min(minY, v.y);
maxY = SDL_max(maxY, v.y);
minZ = SDL_min(minZ, v.z);
maxZ = SDL_max(maxZ, v.z);
}
}
// View space looks down -Z: the nearest point has the largest z.
shadow->matrix = mat4Multiply(mat4OrthographicBounds(minX * SHADOW_MARGIN, maxX * SHADOW_MARGIN, minY * SHADOW_MARGIN, maxY * SHADOW_MARGIN, SDL_max(-maxZ / SHADOW_MARGIN, 0.01f), -minZ * SHADOW_MARGIN), view);
} else if (light->light.type == LIGHT_SPOT) {
view = mat4LookAt(position, vec3Add(position, direction), up);
far = (light->light.range > 0.0f) ? light->light.range : vec3Length(vec3Subtract(centre, position)) + radius;
shadow->matrix = mat4Multiply(mat4Perspective(SDL_min(light->light.outerDegrees * 2.0f * SHADOW_MARGIN, 170.0f), 1.0f, SDL_max(far * 0.005f, 0.01f), far), view);
} else {
far = (light->light.range > 0.0f) ? light->light.range : vec3Length(vec3Subtract(centre, position)) + radius;
shadow->near = SDL_max(far * 0.005f, 0.01f);
shadow->far = far;
for (c = 0; c < CUBE_FACES; c++) {
shadow->faces[c] = mat4Multiply(mat4Perspective(90.0f, 1.0f, shadow->near, shadow->far), mat4LookAt(position, vec3Add(position, faceForward[c]), faceUp[c]));
}
}
}
}
static void _freeFeed(FeedT *feed) { static void _freeFeed(FeedT *feed) {
if (feed->target != NULL) { if (feed->target != NULL) {
SDL_DestroyTexture(feed->target); SDL_DestroyTexture(feed->target);
@ -772,6 +1159,20 @@ static Mat4T _projection(void) {
} }
// A depth format the shadow map can be both rendered into and sampled from.
static SDL_GPUTextureFormat _shadowFormat(void) {
SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D16_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM };
int32_t x;
for (x = 0; x < (int32_t)SDL_arraysize(wanted); x++) {
if (SDL_GPUTextureSupportsFormat(_scene.device, wanted[x], SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
return wanted[x];
}
}
return SDL_GPU_TEXTUREFORMAT_D16_UNORM;
}
// World matrices and inherited visibility, depth first. // World matrices and inherited visibility, depth first.
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) { static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) {
NodeT *n = &_scene.nodes[node]; NodeT *n = &_scene.nodes[node];
@ -1040,6 +1441,17 @@ bool lightSetRange(int32_t node, float range) {
} }
// Whether this light casts shadows. Every light may; a point light's cost six passes to a
// directional or spot light's one.
bool lightSetShadow(int32_t node, bool shadow) {
if (!nodeValid(node) || !_scene.nodes[node].hasLight) {
return false;
}
_scene.nodes[node].castsShadow = shadow;
return true;
}
// ===== Materials ===== // ===== Materials =====
bool materialDelete(int32_t material) { bool materialDelete(int32_t material) {
@ -1801,10 +2213,12 @@ bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer) {
_scene.ambient = vec3(0.1f, 0.1f, 0.1f); _scene.ambient = vec3(0.1f, 0.1f, 0.1f);
_scene.antialias = true; _scene.antialias = true;
_scene.sampleCount = SDL_GPU_SAMPLECOUNT_1; _scene.sampleCount = SDL_GPU_SAMPLECOUNT_1;
_scene.shadowSize = SHADOW_SIZE;
if (device == NULL) { if (device == NULL) {
return false; return false;
} }
_scene.depthFormat = _depthFormat(); _scene.depthFormat = _depthFormat();
_scene.shadowFormat = _shadowFormat();
_allocNode(); _allocNode();
nodeSetName(SCENE_ROOT_NODE, "root"); nodeSetName(SCENE_ROOT_NODE, "root");
if (!_createShaders()) { if (!_createShaders()) {
@ -1819,13 +2233,22 @@ bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer) {
samplerInfo.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_REPEAT; samplerInfo.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
samplerInfo.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_REPEAT; samplerInfo.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
_scene.sampler = SDL_CreateGPUSampler(device, &samplerInfo); _scene.sampler = SDL_CreateGPUSampler(device, &samplerInfo);
// The shadow map is compared texel by texel, so no filtering, and clamped so its edge holds.
samplerInfo.min_filter = SDL_GPU_FILTER_NEAREST;
samplerInfo.mag_filter = SDL_GPU_FILTER_NEAREST;
samplerInfo.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
samplerInfo.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
samplerInfo.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
samplerInfo.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
_scene.shadowSampler = SDL_CreateGPUSampler(device, &samplerInfo);
pixel = SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32); pixel = SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32);
if (pixel != NULL) { if (pixel != NULL) {
SDL_FillSurfaceRect(pixel, NULL, 0xFFFFFFFFu); SDL_FillSurfaceRect(pixel, NULL, 0xFFFFFFFFu);
_scene.white = _uploadTexture(pixel); _scene.white = _uploadTexture(pixel);
SDL_DestroySurface(pixel); SDL_DestroySurface(pixel);
} }
if ((_scene.sampler == NULL) || (_scene.white == NULL)) { _scene.shadowMapsNone = _createShadowArray(SDL_GPU_TEXTURETYPE_2D_ARRAY, 1, 1);
if ((_scene.sampler == NULL) || (_scene.shadowSampler == NULL) || (_scene.white == NULL) || (_scene.shadowMapsNone == NULL)) {
utilTrace("Scene: %s", SDL_GetError()); utilTrace("Scene: %s", SDL_GetError());
sceneQuit(); sceneQuit();
return false; return false;
@ -1880,9 +2303,19 @@ void sceneQuit(void) {
if (_scene.fragment != NULL) { if (_scene.fragment != NULL) {
SDL_ReleaseGPUShader(_scene.device, _scene.fragment); SDL_ReleaseGPUShader(_scene.device, _scene.fragment);
} }
if (_scene.depthFragment != NULL) {
SDL_ReleaseGPUShader(_scene.device, _scene.depthFragment);
}
if (_scene.sampler != NULL) { if (_scene.sampler != NULL) {
SDL_ReleaseGPUSampler(_scene.device, _scene.sampler); SDL_ReleaseGPUSampler(_scene.device, _scene.sampler);
} }
if (_scene.shadowSampler != NULL) {
SDL_ReleaseGPUSampler(_scene.device, _scene.shadowSampler);
}
_destroyShadowMaps();
if (_scene.shadowMapsNone != NULL) {
SDL_ReleaseGPUTexture(_scene.device, _scene.shadowMapsNone);
}
if (_scene.white != NULL) { if (_scene.white != NULL) {
SDL_ReleaseGPUTexture(_scene.device, _scene.white); SDL_ReleaseGPUTexture(_scene.device, _scene.white);
} }
@ -1909,24 +2342,17 @@ SDL_Texture *sceneRender(void) {
SDL_GPUDepthStencilTargetInfo depth; SDL_GPUDepthStencilTargetInfo depth;
SDL_GPUCommandBuffer *commands; SDL_GPUCommandBuffer *commands;
SDL_GPURenderPass *pass; SDL_GPURenderPass *pass;
SDL_GPUBufferBinding binding;
SDL_GPUTextureSamplerBinding samplerBinding;
DrawUniformsT drawUniforms;
SkinUniformsT *skinUniforms = NULL;
FragmentUniformsT fragmentUniforms; FragmentUniformsT fragmentUniforms;
Mat4T identity = mat4Identity(); Mat4T identity = mat4Identity();
Mat4T view; Mat4T view;
Mat4T inverse;
Vec3T eye; Vec3T eye;
int32_t x; int32_t x;
int32_t drawCount = 0; int32_t drawCount = 0;
int32_t opaqueCount = 0; int32_t opaqueCount = 0;
int32_t lastPipeline = NO_HANDLE; int32_t slot;
int32_t variant; int32_t face;
int32_t layers = 0;
NodeT *node; NodeT *node;
MeshT *mesh;
MaterialT *material;
MaterialT defaultMaterial;
if (!_scene.enabled || (_scene.colour == NULL)) { if (!_scene.enabled || (_scene.colour == NULL)) {
return NULL; return NULL;
@ -1945,6 +2371,7 @@ SDL_Texture *sceneRender(void) {
fragmentUniforms.ambient[1] = _scene.ambient.y; fragmentUniforms.ambient[1] = _scene.ambient.y;
fragmentUniforms.ambient[2] = _scene.ambient.z; fragmentUniforms.ambient[2] = _scene.ambient.z;
fragmentUniforms.ambient[3] = 1.0f; fragmentUniforms.ambient[3] = 1.0f;
_scene.shadowCount = 0;
_fillLights(&fragmentUniforms); _fillLights(&fragmentUniforms);
// Collect what to draw: opaque first in node order, then blended back to front. // Collect what to draw: opaque first in node order, then blended back to front.
if (_scene.drawCapacity < _scene.nodeCount) { if (_scene.drawCapacity < _scene.nodeCount) {
@ -1980,12 +2407,57 @@ SDL_Texture *sceneRender(void) {
if (drawCount > opaqueCount) { if (drawCount > opaqueCount) {
qsort(&_scene.draws[opaqueCount], (size_t)(drawCount - opaqueCount), sizeof(DrawT), _compareDraws); qsort(&_scene.draws[opaqueCount], (size_t)(drawCount - opaqueCount), sizeof(DrawT), _compareDraws);
} }
// Record the pass.
commands = SDL_AcquireGPUCommandBuffer(_scene.device); commands = SDL_AcquireGPUCommandBuffer(_scene.device);
if (commands == NULL) { if (commands == NULL) {
utilTrace("Scene: %s", SDL_GetError()); utilTrace("Scene: %s", SDL_GetError());
return NULL; return NULL;
} }
// The shadow passes: depth from every shadow light into its map layer, or its six cube faces.
for (slot = 0; slot < _scene.shadowCount; slot++) {
layers += (_scene.shadows[slot].type == SHADOW_CUBE) ? CUBE_FACES : 1;
}
if ((_scene.shadowCount > 0) && (drawCount > 0) && _createShadowMaps(layers)) {
_fitShadows(drawCount);
fragmentUniforms.shadowParams[0] = SHADOW_BIAS;
fragmentUniforms.shadowParams[1] = 1.0f / (float)_scene.shadowSize;
fragmentUniforms.shadowParams[2] = (float)_scene.shadowCount;
memset(&depth, 0, sizeof(depth));
depth.clear_depth = 1.0f;
depth.load_op = SDL_GPU_LOADOP_CLEAR;
depth.store_op = SDL_GPU_STOREOP_STORE;
depth.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
for (slot = 0; slot < _scene.shadowCount; slot++) {
ShadowT *shadow = &_scene.shadows[slot];
fragmentUniforms.shadowInfo[slot][0] = (float)shadow->type;
fragmentUniforms.shadowInfo[slot][1] = (float)shadow->layer;
fragmentUniforms.shadowInfo[slot][2] = shadow->near;
fragmentUniforms.shadowInfo[slot][3] = shadow->far;
if (shadow->type == SHADOW_CUBE) {
for (face = 0; face < CUBE_FACES; face++) {
depth.texture = _scene.shadowMaps;
depth.layer = (Uint8)(shadow->layer + face);
pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth);
_drawList(commands, pass, drawCount, &shadow->faces[face], true, NULL);
SDL_EndGPURenderPass(pass);
}
} else {
fragmentUniforms.shadowMatrix[slot] = shadow->matrix;
depth.texture = _scene.shadowMaps;
depth.layer = (Uint8)shadow->layer;
pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth);
_drawList(commands, pass, drawCount, &shadow->matrix, true, NULL);
SDL_EndGPURenderPass(pass);
}
}
} else {
// Lights flagged to cast but nothing to draw into: no slots this frame.
for (x = 0; x < MAX_LIGHTS; x++) {
fragmentUniforms.lights[x].cone[2] = 0.0f;
}
}
// The main pass.
memset(&colour, 0, sizeof(colour)); memset(&colour, 0, sizeof(colour));
colour.clear_color = _scene.background; colour.clear_color = _scene.background;
colour.load_op = SDL_GPU_LOADOP_CLEAR; colour.load_op = SDL_GPU_LOADOP_CLEAR;
@ -2005,71 +2477,9 @@ SDL_Texture *sceneRender(void) {
depth.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; depth.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
pass = SDL_BeginGPURenderPass(commands, &colour, 1, &depth); pass = SDL_BeginGPURenderPass(commands, &colour, 1, &depth);
memset(&defaultMaterial, 0, sizeof(defaultMaterial)); _drawList(commands, pass, drawCount, &_scene.viewProjection, false, &fragmentUniforms);
defaultMaterial.feed = NO_HANDLE;
defaultMaterial.baseColor.x = 1.0f;
defaultMaterial.baseColor.y = 1.0f;
defaultMaterial.baseColor.z = 1.0f;
defaultMaterial.baseColor.w = 1.0f;
defaultMaterial.roughness = 0.5f;
for (x = 0; x < drawCount; x++) {
node = &_scene.nodes[_scene.draws[x].node];
mesh = &_scene.meshes[node->mesh];
material = (node->material != NO_HANDLE) ? &_scene.materials[node->material] : &defaultMaterial;
variant = _lookupPipeline(_scene.draws[x].node);
if (variant == NO_HANDLE) {
continue;
}
if (variant != lastPipeline) {
SDL_BindGPUGraphicsPipeline(pass, _scene.pipelines[variant]);
lastPipeline = variant;
}
drawUniforms.modelViewProjection = mat4Multiply(_scene.viewProjection, node->world);
drawUniforms.model = node->world;
if (mat4Invert(node->world, &inverse)) {
drawUniforms.normalMatrix = mat4Transpose(inverse);
} else {
drawUniforms.normalMatrix = identity;
}
SDL_PushGPUVertexUniformData(commands, 0, &drawUniforms, sizeof(drawUniforms));
if (variant & PIPELINE_SKINNED) {
// 8 KB per skinned draw; allocated once per frame that needs it.
if (skinUniforms == NULL) {
skinUniforms = SDL_malloc(sizeof(SkinUniformsT));
if (skinUniforms == NULL) {
utilDie("Out of memory posing a skin.");
}
}
_fillSkin(node, skinUniforms);
SDL_PushGPUVertexUniformData(commands, 1, skinUniforms, sizeof(SkinUniformsT));
}
fragmentUniforms.baseColor[0] = material->baseColor.x;
fragmentUniforms.baseColor[1] = material->baseColor.y;
fragmentUniforms.baseColor[2] = material->baseColor.z;
fragmentUniforms.baseColor[3] = material->baseColor.w;
fragmentUniforms.emissive[0] = material->emissive.x;
fragmentUniforms.emissive[1] = material->emissive.y;
fragmentUniforms.emissive[2] = material->emissive.z;
fragmentUniforms.emissive[3] = 1.0f;
fragmentUniforms.material[0] = material->metallic;
fragmentUniforms.material[1] = material->roughness;
fragmentUniforms.material[2] = material->unlit ? 1.0f : 0.0f;
fragmentUniforms.material[3] = (_materialTexture(material) != NULL) ? 1.0f : 0.0f;
SDL_PushGPUFragmentUniformData(commands, 0, &fragmentUniforms, sizeof(fragmentUniforms));
memset(&samplerBinding, 0, sizeof(samplerBinding));
samplerBinding.texture = (_materialTexture(material) != NULL) ? _materialTexture(material) : _scene.white;
samplerBinding.sampler = _scene.sampler;
SDL_BindGPUFragmentSamplers(pass, 0, &samplerBinding, 1);
memset(&binding, 0, sizeof(binding));
binding.buffer = mesh->vertexBuffer;
SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
binding.buffer = mesh->indexBuffer;
SDL_BindGPUIndexBuffer(pass, &binding, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_DrawGPUIndexedPrimitives(pass, mesh->indexCount, 1, 0, 0, 0);
}
SDL_EndGPURenderPass(pass); SDL_EndGPURenderPass(pass);
SDL_SubmitGPUCommandBuffer(commands); SDL_SubmitGPUCommandBuffer(commands);
SDL_free(skinUniforms);
return _scene.composite; return _scene.composite;
} }
@ -2206,6 +2616,14 @@ void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
} }
// The shadow map's size in texels per side (default 1024, clamped to 256..4096); larger is
// sharper and slower. The map is rebuilt on the next frame that needs it.
void sceneSetShadowSize(int32_t size) {
_scene.shadowSize = SDL_clamp(size, SHADOW_SIZE_MIN, SHADOW_SIZE_MAX);
_destroyShadowMaps();
}
// Overlay coordinates back to a world point: the point on the ray through that pixel at the given // Overlay coordinates back to a world point: the point on the ray through that pixel at the given
// distance from the camera's near plane, using the last rendered frame's camera. Two distances // distance from the camera's near plane, using the last rendered frame's camera. Two distances
// give a ray for picking. // give a ray for picking.

View file

@ -63,6 +63,7 @@ void sceneSetAntialias(bool antialias);
void sceneSetAmbient(uint8_t r, uint8_t g, uint8_t b); void sceneSetAmbient(uint8_t r, uint8_t g, uint8_t b);
void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount); void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a); void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a);
void sceneSetShadowSize(int32_t size);
Vec3T sceneUnproject(float x, float y, float distance); Vec3T sceneUnproject(float x, float y, float distance);
void sceneUpdateVideo(SceneVideoSourceFn source); void sceneUpdateVideo(SceneVideoSourceFn source);
@ -76,6 +77,7 @@ bool lightSetColor(int32_t node, uint8_t r, uint8_t g, uint8_t b);
bool lightSetCone(int32_t node, float innerDegrees, float outerDegrees); bool lightSetCone(int32_t node, float innerDegrees, float outerDegrees);
bool lightSetIntensity(int32_t node, float intensity); bool lightSetIntensity(int32_t node, float intensity);
bool lightSetRange(int32_t node, float range); bool lightSetRange(int32_t node, float range);
bool lightSetShadow(int32_t node, bool shadow);
bool materialDelete(int32_t material); bool materialDelete(int32_t material);
int32_t materialNew(void); int32_t materialNew(void);

View file

@ -14,7 +14,7 @@ OUT=sceneShaders.h
TMP=$(mktemp -d) TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT trap 'rm -rf "$TMP"' EXIT
ENTRIES="vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment" ENTRIES="vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment"
emit() { emit() {
# emit NAME FILE: a C array from a binary file # emit NAME FILE: a C array from a binary file

View file

@ -1,5 +1,6 @@
// Singe 3 scene shaders: one vertex shader for static meshes, one for skinned meshes, one // Singe 3 scene shaders: one vertex shader for static meshes, one for skinned meshes, one
// fragment shader for both. Compiled offline by src/shaders/build.sh (SDL_shadercross) into the // fragment shader for both, and an empty fragment shader for the depth-only shadow pass (which
// reuses the vertex shaders with the light's view-projection). Compiled offline by src/shaders/build.sh (SDL_shadercross) into the
// SPIR-V, DXIL and MSL blobs in sceneShaders.h; the engine build never compiles shaders. // SPIR-V, DXIL and MSL blobs in sceneShaders.h; the engine build never compiles shaders.
// //
// Resource bindings follow SDL_GPU's HLSL convention: vertex uniforms in space1, fragment // Resource bindings follow SDL_GPU's HLSL convention: vertex uniforms in space1, fragment
@ -7,6 +8,11 @@
#define MAX_LIGHTS 8 #define MAX_LIGHTS 8
#define MAX_JOINTS 128 #define MAX_JOINTS 128
#define MAX_SHADOWS 8
#define SHADOW_NONE 0
#define SHADOW_MAP 1
#define SHADOW_CUBE 2
#define LIGHT_DIRECTIONAL 0 #define LIGHT_DIRECTIONAL 0
#define LIGHT_POINT 1 #define LIGHT_POINT 1
@ -81,7 +87,7 @@ struct Light {
float4 positionType; // xyz position (point, spot) or unused; w = type float4 positionType; // xyz position (point, spot) or unused; w = type
float4 directionRange; // xyz direction the light shines in (directional, spot); w = range (0 = infinite) float4 directionRange; // xyz direction the light shines in (directional, spot); w = range (0 = infinite)
float4 color; // rgb already multiplied by intensity float4 color; // rgb already multiplied by intensity
float4 cone; // x = cos(inner), y = cos(outer) float4 cone; // x = cos(inner), y = cos(outer), z = shadow slot + 1 (0 = casts none)
}; };
cbuffer FragmentUniforms : register(b0, space3) { cbuffer FragmentUniforms : register(b0, space3) {
@ -91,11 +97,102 @@ cbuffer FragmentUniforms : register(b0, space3) {
float4 emissive; float4 emissive;
float4 material; // x = metallic, y = roughness, z = unlit (1/0), w = textured (1/0) float4 material; // x = metallic, y = roughness, z = unlit (1/0), w = textured (1/0)
float4 counts; // x = light count float4 counts; // x = light count
float4 shadowParams; // x = depth bias, y = 1 / map size, z = shadow count
float4x4 shadowMatrix[MAX_SHADOWS]; // Light view-projection per slot (maps)
float4 shadowInfo[MAX_SHADOWS]; // x = SHADOW_MAP / SHADOW_CUBE, y = layer, z = near, w = far (cubes)
Light lights[MAX_LIGHTS]; Light lights[MAX_LIGHTS];
}; };
Texture2D<float4> baseTexture : register(t0, space2); Texture2D<float4> baseTexture : register(t0, space2);
SamplerState baseSampler : register(s0, space2); SamplerState baseSampler : register(s0, space2);
Texture2DArray<float> shadowMaps : register(t1, space2); // A layer per directional or spot shadow, six per point light
SamplerState shadowSampler : register(s1, space2);
// How lit a point is by a directional or spot light's shadow map: its depth from the light against
// the map, averaged over a 3x3 block of texels so edges soften. Points outside the map are lit.
float shadowFactorMap(int slot, float3 worldPosition, float3 normal, float3 toLight) {
float4 lightSpace = mul(shadowMatrix[slot], float4(worldPosition + normal * shadowParams.x * 8.0, 1.0));
float layer = shadowInfo[slot].y;
float2 uv;
float depth;
float texel = shadowParams.y;
float lit;
lightSpace.xyz /= lightSpace.w;
uv = float2(lightSpace.x * 0.5 + 0.5, 0.5 - lightSpace.y * 0.5);
depth = lightSpace.z - shadowParams.x * (1.0 + 2.0 * (1.0 - saturate(dot(normal, toLight))));
if ((uv.x < 0.0) || (uv.x > 1.0) || (uv.y < 0.0) || (uv.y > 1.0) || (depth > 1.0)) {
return 1.0;
}
// Written out because DXC crashes on loops inside a function called from a loop.
lit = (shadowMaps.Sample(shadowSampler, float3(uv + float2(-texel, -texel), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(0.0, -texel), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(texel, -texel), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(-texel, 0.0), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv, layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(texel, 0.0), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(-texel, texel), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(0.0, texel), layer)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(texel, texel), layer)).r >= depth) ? 1.0 : 0.0;
return lit / 9.0;
}
// How lit a point is by a point light's six shadow faces. The face is picked by the direction's
// major axis and looked up with the same frame the engine rendered it with (forward, up, right =
// forward x up, a 90 degree perspective), so no cube-map convention is involved; the point's own
// projected depth is compared over five taps.
float shadowFactorCube(int slot, float3 worldPosition, float3 normal, float3 lightPosition) {
float base = shadowInfo[slot].y;
float near = shadowInfo[slot].z;
float far = shadowInfo[slot].w;
float3 dir = (worldPosition + normal * shadowParams.x * 8.0) - lightPosition;
float3 ad = abs(dir);
float texel = shadowParams.y;
float ma;
float face;
float3 forward;
float3 up;
float3 right;
float2 uv;
float depth;
float lit;
if ((ad.x >= ad.y) && (ad.x >= ad.z)) {
ma = ad.x;
face = (dir.x > 0.0) ? 0.0 : 1.0;
forward = float3((dir.x > 0.0) ? 1.0 : -1.0, 0.0, 0.0);
up = float3(0.0, 1.0, 0.0);
} else if (ad.y >= ad.z) {
ma = ad.y;
face = (dir.y > 0.0) ? 2.0 : 3.0;
forward = float3(0.0, (dir.y > 0.0) ? 1.0 : -1.0, 0.0);
up = float3(0.0, 0.0, (dir.y > 0.0) ? -1.0 : 1.0);
} else {
ma = ad.z;
face = (dir.z > 0.0) ? 4.0 : 5.0;
forward = float3(0.0, 0.0, (dir.z > 0.0) ? 1.0 : -1.0);
up = float3(0.0, 1.0, 0.0);
}
right = cross(forward, up);
uv = float2(0.5 + 0.5 * dot(dir, right) / ma, 0.5 - 0.5 * dot(dir, up) / ma);
depth = far / (far - near) - near * far / ((far - near) * ma) - shadowParams.x * 2.0;
if (depth > 1.0) {
return 1.0;
}
lit = (shadowMaps.Sample(shadowSampler, float3(uv, base + face)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(texel, 0.0), base + face)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv - float2(texel, 0.0), base + face)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv + float2(0.0, texel), base + face)).r >= depth) ? 1.0 : 0.0;
lit += (shadowMaps.Sample(shadowSampler, float3(uv - float2(0.0, texel), base + face)).r >= depth) ? 1.0 : 0.0;
return lit / 5.0;
}
// The shadow pass draws depth only.
void depthMain(VertexOutput input) {
}
// Named fragmentMain because "fragment" is a keyword in Metal and the MSL entry point keeps this name. // Named fragmentMain because "fragment" is a keyword in Metal and the MSL entry point keeps this name.
@ -157,6 +254,15 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
attenuation *= smoothstep(lights[x].cone.y, lights[x].cone.x, cosAngle); attenuation *= smoothstep(lights[x].cone.y, lights[x].cone.x, cosAngle);
} }
} }
if (lights[x].cone.z > 0.5) {
int slot = (int)lights[x].cone.z - 1;
if (shadowInfo[slot].x == SHADOW_CUBE) {
attenuation *= shadowFactorCube(slot, input.worldPosition, normal, lights[x].positionType.xyz);
} else {
attenuation *= shadowFactorMap(slot, input.worldPosition, normal, toLight);
}
}
diffuse = saturate(dot(normal, toLight)); diffuse = saturate(dot(normal, toLight));
halfway = normalize(toLight + view); halfway = normalize(toLight + view);
specular = pow(saturate(dot(normal, halfway)), shininess) * (1.0 - roughness * 0.5); specular = pow(saturate(dot(normal, halfway)), shininess) * (1.0 - roughness * 0.5);

File diff suppressed because it is too large Load diff

View file

@ -573,6 +573,7 @@ static int32_t apiLightSetColor(lua_State *L);
static int32_t apiLightSetCone(lua_State *L); static int32_t apiLightSetCone(lua_State *L);
static int32_t apiLightSetIntensity(lua_State *L); static int32_t apiLightSetIntensity(lua_State *L);
static int32_t apiLightSetRange(lua_State *L); static int32_t apiLightSetRange(lua_State *L);
static int32_t apiLightSetShadow(lua_State *L);
static int32_t apiMaterialDelete(lua_State *L); static int32_t apiMaterialDelete(lua_State *L);
static int32_t apiMaterialNew(lua_State *L); static int32_t apiMaterialNew(lua_State *L);
static int32_t apiMaterialSetBlend(lua_State *L); static int32_t apiMaterialSetBlend(lua_State *L);
@ -640,6 +641,7 @@ static int32_t apiSceneProject(lua_State *L);
static int32_t apiSceneSetAmbient(lua_State *L); static int32_t apiSceneSetAmbient(lua_State *L);
static int32_t apiSceneSetAntialias(lua_State *L); static int32_t apiSceneSetAntialias(lua_State *L);
static int32_t apiSceneSetBackground(lua_State *L); static int32_t apiSceneSetBackground(lua_State *L);
static int32_t apiSceneSetShadowSize(lua_State *L);
static int32_t apiSceneUnproject(lua_State *L); static int32_t apiSceneUnproject(lua_State *L);
static int32_t apiScriptExecute(lua_State *L); static int32_t apiScriptExecute(lua_State *L);
static int32_t apiScriptPush(lua_State *L); static int32_t apiScriptPush(lua_State *L);
@ -3401,6 +3403,19 @@ static int32_t apiLightSetRange(lua_State *L) {
} }
// lightSetShadow(node, bool): this light casts shadows (a point light's cost six passes)
static int32_t apiLightSetShadow(lua_State *L) {
int32_t node;
_argCheck(L, "lightSetShadow", 2, 2);
node = _argNode(L, "lightSetShadow", 1);
if (!lightSetShadow(node, _argBoolean(L, "lightSetShadow", 2))) {
_luaDie(L, "lightSetShadow", "Node %d is not a light.", node);
}
return 0;
}
// materialDelete(material) // materialDelete(material)
static int32_t apiMaterialDelete(lua_State *L) { static int32_t apiMaterialDelete(lua_State *L) {
_argCheck(L, "materialDelete", 1, 1); _argCheck(L, "materialDelete", 1, 1);
@ -4491,6 +4506,14 @@ static int32_t apiSceneSetBackground(lua_State *L) {
} }
// sceneSetShadowSize(size): shadow map texels per side, 256 to 4096 (default 1024)
static int32_t apiSceneSetShadowSize(lua_State *L) {
_argCheck(L, "sceneSetShadowSize", 1, 1);
sceneSetShadowSize(_argInteger(L, "sceneSetShadowSize", 1));
return 0;
}
// x, y, z = sceneUnproject(sx, sy, distance): the world point that far along the ray through an overlay point // x, y, z = sceneUnproject(sx, sy, distance): the world point that far along the ray through an overlay point
static int32_t apiSceneUnproject(lua_State *L) { static int32_t apiSceneUnproject(lua_State *L) {
_argCheck(L, "sceneUnproject", 3, 3); _argCheck(L, "sceneUnproject", 3, 3);
@ -5973,6 +5996,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
lua_register(_global.luaContext, "lightSetCone", apiLightSetCone); // 3.00 lua_register(_global.luaContext, "lightSetCone", apiLightSetCone); // 3.00
lua_register(_global.luaContext, "lightSetIntensity", apiLightSetIntensity); // 3.00 lua_register(_global.luaContext, "lightSetIntensity", apiLightSetIntensity); // 3.00
lua_register(_global.luaContext, "lightSetRange", apiLightSetRange); // 3.00 lua_register(_global.luaContext, "lightSetRange", apiLightSetRange); // 3.00
lua_register(_global.luaContext, "lightSetShadow", apiLightSetShadow); // 3.00
lua_register(_global.luaContext, "materialDelete", apiMaterialDelete); // 3.00 lua_register(_global.luaContext, "materialDelete", apiMaterialDelete); // 3.00
lua_register(_global.luaContext, "materialNew", apiMaterialNew); // 3.00 lua_register(_global.luaContext, "materialNew", apiMaterialNew); // 3.00
lua_register(_global.luaContext, "materialSetBlend", apiMaterialSetBlend); // 3.00 lua_register(_global.luaContext, "materialSetBlend", apiMaterialSetBlend); // 3.00
@ -6041,6 +6065,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
lua_register(_global.luaContext, "sceneSetAmbient", apiSceneSetAmbient); // 3.00 lua_register(_global.luaContext, "sceneSetAmbient", apiSceneSetAmbient); // 3.00
lua_register(_global.luaContext, "sceneSetAntialias", apiSceneSetAntialias); // 3.00 lua_register(_global.luaContext, "sceneSetAntialias", apiSceneSetAntialias); // 3.00
lua_register(_global.luaContext, "sceneSetBackground", apiSceneSetBackground); // 3.00 lua_register(_global.luaContext, "sceneSetBackground", apiSceneSetBackground); // 3.00
lua_register(_global.luaContext, "sceneSetShadowSize", apiSceneSetShadowSize); // 3.00
lua_register(_global.luaContext, "sceneUnproject", apiSceneUnproject); // 3.00 lua_register(_global.luaContext, "sceneUnproject", apiSceneUnproject); // 3.00
lua_register(_global.luaContext, "scriptExecute", apiScriptExecute); // 2.00 lua_register(_global.luaContext, "scriptExecute", apiScriptExecute); // 2.00
lua_register(_global.luaContext, "scriptPush", apiScriptPush); // 2.00 lua_register(_global.luaContext, "scriptPush", apiScriptPush); // 2.00