3713 lines
128 KiB
C
3713 lines
128 KiB
C
/*
|
|
*
|
|
* Singe 3
|
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
// The 3D scene: a layer drawn between the disc video and the 2D overlay. It renders on the
|
|
// SDL_GPU device the 2D renderer was created on, into its own colour and depth textures; the colour
|
|
// texture is wrapped as an SDL_Texture so the frame loop composites it like any other layer.
|
|
//
|
|
// Everything in the scene is a node in one tree (node 0 is the root): a node has a transform, and
|
|
// optionally a mesh with a material, or a light. Meshes, materials and nodes are addressed from
|
|
// Lua by integer handles that index the arrays below; a freed slot is reused. Shaders come
|
|
// precompiled from sceneShaders.h (see src/shaders/build.sh).
|
|
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "util.h"
|
|
#include "scene.h"
|
|
#include "shaders/sceneShaders.h"
|
|
#include "particles.h"
|
|
|
|
|
|
#define COLOUR_MAX 255.0f
|
|
#define MAX_LIGHTS 8
|
|
#define MAX_JOINTS 128
|
|
#define INITIAL_CAPACITY 64
|
|
#define PIPELINE_COUNT 8 // skinned x blend x double sided
|
|
#define PIPELINE_SKINNED 1
|
|
#define PIPELINE_BLEND 2
|
|
#define PIPELINE_TWO_SIDED 4
|
|
#define PARTICLE_PIPELINES 2 // PARTICLE_ALPHA, PARTICLE_ADD
|
|
#define PARTICLE_VERTICES 6 // Two triangles per particle, unindexed
|
|
#define PARTICLE_DRAW_MAX 64 // 3D emitters drawn per frame
|
|
#define DEFAULT_FOV 60.0f
|
|
#define DEFAULT_NEAR 0.1f
|
|
#define DEFAULT_FAR 1000.0f
|
|
#define DEFAULT_EYE_Z 5.0f
|
|
#define MIN_SEGMENTS 3
|
|
#define PI 3.14159265358979323846f
|
|
#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 SHADOW_NEAR_MIN 0.01f
|
|
#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
|
|
#define MAX_MORPHS 8 // Active morph targets per draw (the shader's limit)
|
|
#define MORPH_FLOATS 8 // Per target per vertex: position delta xyz + pad, normal delta xyz + pad
|
|
|
|
|
|
// Matches DrawUniforms in scene.hlsl.
|
|
typedef struct DrawUniformsS {
|
|
Mat4T modelViewProjection;
|
|
Mat4T model;
|
|
Mat4T normalMatrix;
|
|
float morphWeights[MAX_MORPHS];
|
|
int32_t morphTargets[MAX_MORPHS];
|
|
int32_t morphInfo[4];
|
|
} DrawUniformsT;
|
|
|
|
// Matches Light and FragmentUniforms in scene.hlsl.
|
|
typedef struct LightUniformS {
|
|
float positionType[4];
|
|
float directionRange[4];
|
|
float color[4];
|
|
float cone[4];
|
|
} LightUniformT;
|
|
|
|
typedef struct FragmentUniformsS {
|
|
float cameraPosition[4];
|
|
float ambient[4];
|
|
float baseColor[4];
|
|
float emissive[4];
|
|
float material[4];
|
|
float counts[4];
|
|
float shadowParams[4];
|
|
Mat4T shadowMatrix[MAX_SHADOWS];
|
|
float shadowInfo[MAX_SHADOWS][4];
|
|
LightUniformT lights[MAX_LIGHTS];
|
|
} 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
|
|
Mat4T faceViews[CUBE_FACES]; // Cube: each face's view alone, for culling
|
|
float near;
|
|
float far;
|
|
} ShadowT;
|
|
|
|
typedef struct MeshS {
|
|
SDL_GPUBuffer *vertexBuffer;
|
|
SDL_GPUBuffer *indexBuffer;
|
|
uint32_t indexCount;
|
|
Vec3T boundsMin; // Of the vertices, for fitting the shadow map
|
|
Vec3T boundsMax;
|
|
float *positions; // A CPU copy of the geometry (x, y, z per vertex) for physics shapes
|
|
uint32_t *indices;
|
|
SceneVertexT *vertices; // The whole vertex array, for meshes that are rewritten (soft bodies)
|
|
SDL_GPUTransferBuffer *transfer; // For those rewrites, made on first use
|
|
int32_t vertexCount;
|
|
SDL_GPUBuffer *morphBuffer; // Morph target deltas, MORPH_FLOATS per vertex per target
|
|
char **morphNames;
|
|
int32_t morphCount;
|
|
bool skinned;
|
|
bool used;
|
|
} MeshT;
|
|
|
|
// A video player's frames as a texture: the 2D renderer copies the player's (YUV) texture into
|
|
// an RGBA target every frame, and materials sample that.
|
|
typedef struct FeedS {
|
|
int32_t player;
|
|
SDL_Texture *target; // Owned; created once the source's size is known
|
|
SDL_GPUTexture *gpu; // The target as the scene samples it (not owned)
|
|
bool used;
|
|
} FeedT;
|
|
|
|
typedef struct MaterialS {
|
|
Vec4T baseColor;
|
|
Vec3T emissive;
|
|
float metallic;
|
|
float roughness;
|
|
SDL_GPUTexture *texture; // Owned; NULL means untextured
|
|
int32_t feed; // A video feed instead of the texture, NO_HANDLE for none
|
|
bool unlit;
|
|
bool doubleSided;
|
|
bool blend;
|
|
bool used;
|
|
} MaterialT;
|
|
|
|
typedef struct LightS {
|
|
LightTypeE type;
|
|
Vec3T color;
|
|
float intensity;
|
|
float range;
|
|
float innerDegrees;
|
|
float outerDegrees;
|
|
} LightT;
|
|
|
|
typedef struct NodeS {
|
|
char *name;
|
|
int32_t parent;
|
|
int32_t firstChild;
|
|
int32_t nextSibling;
|
|
Vec3T translation;
|
|
QuatT rotation;
|
|
Vec3T scale;
|
|
Mat4T world; // Rebuilt every frame
|
|
int32_t mesh;
|
|
int32_t material;
|
|
LightT light;
|
|
bool castsShadow;
|
|
float *morphWeights; // One per target of the node's mesh
|
|
int32_t morphCount;
|
|
int32_t *skinJoints; // Nodes whose world matrices drive a skinned mesh
|
|
Mat4T *skinInverseBind;
|
|
int32_t skinCount;
|
|
uint32_t generation; // Counts reuses of this slot, so stale handles can be told apart
|
|
bool hasLight;
|
|
bool visible;
|
|
bool worldVisible; // Own flag and every ancestor's
|
|
bool shadowCaster; // Drawn into shadow maps (nodeSetShadow)
|
|
bool used;
|
|
} NodeT;
|
|
|
|
typedef struct DrawS {
|
|
int32_t node;
|
|
float depth; // View-space distance, for sorting blended draws
|
|
Vec3T centre; // World bounding sphere, filled by _fitShadows
|
|
float radius;
|
|
} DrawT;
|
|
|
|
// Matches SkinUniforms in scene.hlsl.
|
|
typedef struct SkinUniformsS {
|
|
Mat4T joints[MAX_JOINTS];
|
|
} SkinUniformsT;
|
|
|
|
// One corner of a particle billboard; the vertex shader expands it from the camera's axes.
|
|
typedef struct ParticleVertexS {
|
|
float centre[3];
|
|
float corner[2];
|
|
float size;
|
|
float angle;
|
|
float colour[4];
|
|
float uv[2];
|
|
} ParticleVertexT;
|
|
|
|
typedef struct ParticleUniformsS {
|
|
Mat4T viewProjection;
|
|
float right[4];
|
|
float up[4];
|
|
} ParticleUniformsT;
|
|
|
|
// GPU textures made from an emitter's frames, kept until the frames change or the emitter goes.
|
|
typedef struct ParticleTexturesS {
|
|
int32_t id;
|
|
uint32_t version;
|
|
int32_t count;
|
|
SDL_GPUTexture **textures;
|
|
} ParticleTexturesT;
|
|
|
|
// A stretch of the frame's particle vertices drawn with one texture and blend.
|
|
typedef struct ParticleRunS {
|
|
int32_t first;
|
|
int32_t count;
|
|
SDL_GPUTexture *texture;
|
|
ParticleBlendE blend;
|
|
} ParticleRunT;
|
|
|
|
// Sort keys for particles and emitters, far to near.
|
|
typedef struct DepthOrderS {
|
|
float depth;
|
|
int32_t index;
|
|
} DepthOrderT;
|
|
|
|
// What a point light's six faces were last rendered from, so unchanged ones are kept.
|
|
typedef struct ShadowCacheS {
|
|
int32_t node;
|
|
int32_t layer;
|
|
uint32_t mapsVersion;
|
|
uint64_t hash;
|
|
} ShadowCacheT;
|
|
|
|
typedef struct SceneS {
|
|
SDL_GPUDevice *device;
|
|
SDL_Renderer *renderer;
|
|
SDL_GPUTexture *colour; // What the 2D renderer samples (resolved when multisampled)
|
|
SDL_GPUTexture *multisampled; // The colour target while antialiasing, resolved into colour
|
|
SDL_GPUTexture *depth;
|
|
SDL_GPUTextureFormat depthFormat;
|
|
SDL_GPUSampleCount sampleCount;
|
|
bool antialias; // Wanted; sampleCount says what the device gave
|
|
SDL_Texture *composite; // The colour target as the 2D renderer sees it
|
|
SDL_GPUShader *vertexStatic;
|
|
SDL_GPUShader *vertexSkinned;
|
|
SDL_GPUShader *fragment;
|
|
SDL_GPUShader *depthFragment; // Empty; the shadow pass writes depth only
|
|
SDL_GPUShader *particleVertex;
|
|
SDL_GPUShader *particleFragment;
|
|
SDL_GPUGraphicsPipeline *particlePipelines[PARTICLE_PIPELINES];
|
|
SDL_GPUBuffer *particleBuffer; // This frame's billboard vertices
|
|
SDL_GPUTransferBuffer *particleTransfer;
|
|
uint32_t particleCapacity; // Bytes in both
|
|
ParticleVertexT *particleVertices; // CPU side, grown as needed
|
|
int32_t particleVertexCapacity;
|
|
int32_t particleVertexCount;
|
|
ParticleRunT particleRuns[PARTICLE_DRAW_MAX * 16];
|
|
int32_t particleRunCount;
|
|
ParticleTexturesT *particleTextures;
|
|
int32_t particleTextureCount;
|
|
SDL_GPUGraphicsPipeline *pipelines[PIPELINE_COUNT];
|
|
SDL_GPUGraphicsPipeline *shadowPipelines[SHADOW_PIPELINES];
|
|
ShadowCacheT shadowCache[MAX_SHADOWS];
|
|
uint32_t shadowMapsVersion; // Bumped whenever the map array is (re)made
|
|
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_GPUBuffer *noMorphs; // Stand-in delta buffer for meshes without morph targets
|
|
SDL_FColor background;
|
|
Vec3T ambient;
|
|
NodeT *nodes;
|
|
int32_t nodeCount;
|
|
MeshT *meshes;
|
|
int32_t meshCount;
|
|
MaterialT *materials;
|
|
int32_t materialCount;
|
|
FeedT *feeds;
|
|
int32_t feedCount;
|
|
DrawT *draws;
|
|
int32_t drawCapacity;
|
|
int32_t cameraNode; // NO_HANDLE for the built-in default view
|
|
bool perspective;
|
|
float fov;
|
|
float orthoHeight;
|
|
float near;
|
|
float far;
|
|
Mat4T viewProjection; // Of the last rendered frame, for sceneProject
|
|
int32_t width;
|
|
int32_t height;
|
|
bool enabled;
|
|
} SceneT;
|
|
|
|
|
|
static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
|
|
static int32_t _allocFeed(int32_t player);
|
|
static int32_t _allocMaterial(void);
|
|
static int32_t _allocNode(void);
|
|
static void _attach(int32_t node, int32_t parent);
|
|
static int32_t _compareDepthOrder(const void *a, const void *b);
|
|
static int32_t _compareDraws(const void *a, const void *b);
|
|
static bool _createParticlePipeline(int32_t blend);
|
|
static bool _createPipeline(int32_t variant);
|
|
static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms, uint32_t storageBuffers);
|
|
static bool _createShaders(void);
|
|
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_GPUTextureFormat _depthFormat(void);
|
|
static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bool *skip);
|
|
static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount);
|
|
static void _destroyPipelines(void);
|
|
static void _destroyShadowMaps(void);
|
|
static void _destroyTargets(void);
|
|
static void _detach(int32_t node);
|
|
static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, FragmentUniformsT *fragmentUniforms);
|
|
static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const Mat4T *viewProjection, Vec3T right, Vec3T up);
|
|
static void _fillLights(FragmentUniformsT *uniforms);
|
|
static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms);
|
|
static void _fitShadows(int32_t drawCount);
|
|
static void _freeFeed(FeedT *feed);
|
|
static void _freeMaterialTexture(MaterialT *material);
|
|
static void _freeMorphs(MeshT *mesh);
|
|
static void _freeMorphWeights(NodeT *node);
|
|
static void _freeSkin(NodeT *node);
|
|
static void _gatherParticles(Vec3T eye, Vec3T forward);
|
|
static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **indices, int32_t *indexCount, float bottomRadius, float topRadius, float height, int32_t segments);
|
|
static int32_t _lookupPipeline(int32_t node);
|
|
static void _matchMorphWeights(NodeT *node);
|
|
static SDL_GPUTexture *_materialTexture(const MaterialT *material);
|
|
static ParticleTexturesT *_particleTextures(const EmitterViewT *view);
|
|
static Mat4T _projection(void);
|
|
static void _releaseParticleTextures(bool all);
|
|
static SDL_GPUTextureFormat _shadowFormat(void);
|
|
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible);
|
|
static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size);
|
|
static void _uploadParticles(SDL_GPUCommandBuffer *commands);
|
|
static SDL_GPUTexture *_uploadTexture(SDL_Surface *image);
|
|
static SceneVertexT _vertex(float x, float y, float z, float nx, float ny, float nz, float u, float v);
|
|
static Mat4T _view(void);
|
|
static Vec3T _viewEye(void);
|
|
|
|
|
|
static SceneT _scene;
|
|
|
|
|
|
// ===== Internal helpers =====
|
|
|
|
static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned) {
|
|
int32_t x;
|
|
MeshT *mesh;
|
|
|
|
if ((_scene.device == NULL) || (vertexCount <= 0) || (indexCount <= 0)) {
|
|
return NO_HANDLE;
|
|
}
|
|
for (x = 0; x < _scene.meshCount; x++) {
|
|
if (!_scene.meshes[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _scene.meshCount) {
|
|
_scene.meshes = SDL_realloc(_scene.meshes, sizeof(MeshT) * (size_t)(_scene.meshCount + 1));
|
|
if (_scene.meshes == NULL) {
|
|
utilDie("Out of memory allocating a mesh.");
|
|
}
|
|
_scene.meshCount++;
|
|
}
|
|
mesh = &_scene.meshes[x];
|
|
memset(mesh, 0, sizeof(*mesh));
|
|
mesh->vertexBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_VERTEX, vertices, (uint32_t)(sizeof(SceneVertexT) * (size_t)vertexCount));
|
|
mesh->indexBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_INDEX, indices, (uint32_t)(sizeof(uint32_t) * (size_t)indexCount));
|
|
if ((mesh->vertexBuffer == NULL) || (mesh->indexBuffer == NULL)) {
|
|
mesh->used = true;
|
|
meshDelete(x);
|
|
return NO_HANDLE;
|
|
}
|
|
mesh->indexCount = (uint32_t)indexCount;
|
|
mesh->vertexCount = vertexCount;
|
|
mesh->skinned = skinned;
|
|
mesh->used = true;
|
|
mesh->positions = SDL_malloc(sizeof(float) * 3 * (size_t)vertexCount);
|
|
mesh->indices = SDL_malloc(sizeof(uint32_t) * (size_t)indexCount);
|
|
if ((mesh->positions == NULL) || (mesh->indices == NULL)) {
|
|
utilDie("Out of memory keeping a mesh's geometry.");
|
|
}
|
|
mesh->vertices = SDL_malloc(sizeof(SceneVertexT) * (size_t)vertexCount);
|
|
if (mesh->vertices == NULL) {
|
|
utilDie("Out of memory keeping a mesh.");
|
|
}
|
|
memcpy(mesh->vertices, vertices, sizeof(SceneVertexT) * (size_t)vertexCount);
|
|
for (x = 0; x < vertexCount; x++) {
|
|
mesh->positions[x * 3] = vertices[x].position[0];
|
|
mesh->positions[x * 3 + 1] = vertices[x].position[1];
|
|
mesh->positions[x * 3 + 2] = vertices[x].position[2];
|
|
}
|
|
memcpy(mesh->indices, indices, sizeof(uint32_t) * (size_t)indexCount);
|
|
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);
|
|
}
|
|
|
|
|
|
// One feed per player, shared by every material showing it.
|
|
static int32_t _allocFeed(int32_t player) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _scene.feedCount; x++) {
|
|
if (_scene.feeds[x].used && (_scene.feeds[x].player == player)) {
|
|
return x;
|
|
}
|
|
}
|
|
for (x = 0; x < _scene.feedCount; x++) {
|
|
if (!_scene.feeds[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _scene.feedCount) {
|
|
_scene.feeds = SDL_realloc(_scene.feeds, sizeof(FeedT) * (size_t)(_scene.feedCount + 1));
|
|
if (_scene.feeds == NULL) {
|
|
utilDie("Out of memory allocating a video feed.");
|
|
}
|
|
_scene.feedCount++;
|
|
}
|
|
memset(&_scene.feeds[x], 0, sizeof(FeedT));
|
|
_scene.feeds[x].player = player;
|
|
_scene.feeds[x].used = true;
|
|
return x;
|
|
}
|
|
|
|
|
|
static int32_t _allocMaterial(void) {
|
|
int32_t x;
|
|
MaterialT *material;
|
|
|
|
for (x = 0; x < _scene.materialCount; x++) {
|
|
if (!_scene.materials[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _scene.materialCount) {
|
|
_scene.materials = SDL_realloc(_scene.materials, sizeof(MaterialT) * (size_t)(_scene.materialCount + 1));
|
|
if (_scene.materials == NULL) {
|
|
utilDie("Out of memory allocating a material.");
|
|
}
|
|
_scene.materialCount++;
|
|
}
|
|
material = &_scene.materials[x];
|
|
memset(material, 0, sizeof(*material));
|
|
material->baseColor.x = 1.0f;
|
|
material->baseColor.y = 1.0f;
|
|
material->baseColor.z = 1.0f;
|
|
material->baseColor.w = 1.0f;
|
|
material->roughness = 0.5f;
|
|
material->feed = NO_HANDLE;
|
|
material->used = true;
|
|
return x;
|
|
}
|
|
|
|
|
|
// A fresh node, detached, at the origin.
|
|
static int32_t _allocNode(void) {
|
|
int32_t x;
|
|
uint32_t generation;
|
|
NodeT *node;
|
|
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
if (!_scene.nodes[x].used) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == _scene.nodeCount) {
|
|
_scene.nodes = SDL_realloc(_scene.nodes, sizeof(NodeT) * (size_t)(_scene.nodeCount + 1));
|
|
if (_scene.nodes == NULL) {
|
|
utilDie("Out of memory allocating a scene node.");
|
|
}
|
|
_scene.nodeCount++;
|
|
}
|
|
node = &_scene.nodes[x];
|
|
generation = node->generation + 1;
|
|
memset(node, 0, sizeof(*node));
|
|
node->generation = generation;
|
|
node->parent = NO_HANDLE;
|
|
node->firstChild = NO_HANDLE;
|
|
node->nextSibling = NO_HANDLE;
|
|
node->rotation = quatIdentity();
|
|
node->scale = vec3(1.0f, 1.0f, 1.0f);
|
|
node->world = mat4Identity();
|
|
node->mesh = NO_HANDLE;
|
|
node->material = NO_HANDLE;
|
|
node->visible = true;
|
|
node->shadowCaster = true;
|
|
node->used = true;
|
|
return x;
|
|
}
|
|
|
|
|
|
// Links node under parent as its last child.
|
|
static void _attach(int32_t node, int32_t parent) {
|
|
int32_t last;
|
|
|
|
_scene.nodes[node].parent = parent;
|
|
_scene.nodes[node].nextSibling = NO_HANDLE;
|
|
if (_scene.nodes[parent].firstChild == NO_HANDLE) {
|
|
_scene.nodes[parent].firstChild = node;
|
|
return;
|
|
}
|
|
last = _scene.nodes[parent].firstChild;
|
|
while (_scene.nodes[last].nextSibling != NO_HANDLE) {
|
|
last = _scene.nodes[last].nextSibling;
|
|
}
|
|
_scene.nodes[last].nextSibling = node;
|
|
}
|
|
|
|
|
|
// Blended draws go back to front; opaque ones keep their order.
|
|
// Far to near.
|
|
static int32_t _compareDepthOrder(const void *a, const void *b) {
|
|
const DepthOrderT *x = a;
|
|
const DepthOrderT *y = b;
|
|
|
|
if (x->depth > y->depth) {
|
|
return -1;
|
|
}
|
|
if (x->depth < y->depth) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int32_t _compareDraws(const void *a, const void *b) {
|
|
const DrawT *da = a;
|
|
const DrawT *db = b;
|
|
|
|
if (da->depth > db->depth) {
|
|
return -1;
|
|
}
|
|
if (da->depth < db->depth) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Billboard pipeline for one blend: camera-facing quads, depth tested, never written, two-sided.
|
|
static bool _createParticlePipeline(int32_t blend) {
|
|
SDL_GPUGraphicsPipelineCreateInfo info;
|
|
SDL_GPUVertexBufferDescription buffers[1];
|
|
SDL_GPUVertexAttribute attributes[5];
|
|
SDL_GPUColorTargetDescription colour;
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
memset(buffers, 0, sizeof(buffers));
|
|
memset(attributes, 0, sizeof(attributes));
|
|
memset(&colour, 0, sizeof(colour));
|
|
buffers[0].slot = 0;
|
|
buffers[0].pitch = sizeof(ParticleVertexT);
|
|
buffers[0].input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
|
attributes[0].location = 0;
|
|
attributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3;
|
|
attributes[0].offset = offsetof(ParticleVertexT, centre);
|
|
attributes[1].location = 1;
|
|
attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
|
attributes[1].offset = offsetof(ParticleVertexT, corner);
|
|
attributes[2].location = 2;
|
|
attributes[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
|
attributes[2].offset = offsetof(ParticleVertexT, size);
|
|
attributes[3].location = 3;
|
|
attributes[3].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
|
attributes[3].offset = offsetof(ParticleVertexT, colour);
|
|
attributes[4].location = 4;
|
|
attributes[4].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
|
attributes[4].offset = offsetof(ParticleVertexT, uv);
|
|
colour.format = SDL_GetGPUTextureFormatFromPixelFormat(SDL_PIXELFORMAT_BGRA32);
|
|
colour.blend_state.enable_blend = true;
|
|
colour.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
|
colour.blend_state.dst_color_blendfactor = (blend == PARTICLE_ADD) ? SDL_GPU_BLENDFACTOR_ONE : SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
|
colour.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
|
|
colour.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
|
|
colour.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
|
colour.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
|
info.vertex_shader = _scene.particleVertex;
|
|
info.fragment_shader = _scene.particleFragment;
|
|
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 = SDL_GPU_CULLMODE_NONE;
|
|
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
|
|
info.multisample_state.sample_count = _scene.sampleCount;
|
|
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 = false;
|
|
info.target_info.color_target_descriptions = &colour;
|
|
info.target_info.num_color_targets = 1;
|
|
info.target_info.depth_stencil_format = _scene.depthFormat;
|
|
info.target_info.has_depth_stencil_target = true;
|
|
_scene.particlePipelines[blend] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info);
|
|
if (_scene.particlePipelines[blend] == NULL) {
|
|
utilTrace("Scene: particle pipeline %d: %s", blend, SDL_GetError());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
static bool _createPipeline(int32_t variant) {
|
|
SDL_GPUGraphicsPipelineCreateInfo info;
|
|
SDL_GPUVertexBufferDescription buffers[1];
|
|
SDL_GPUVertexAttribute attributes[5];
|
|
SDL_GPUColorTargetDescription colour;
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
memset(buffers, 0, sizeof(buffers));
|
|
memset(attributes, 0, sizeof(attributes));
|
|
memset(&colour, 0, sizeof(colour));
|
|
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);
|
|
colour.format = SDL_GetGPUTextureFormatFromPixelFormat(SDL_PIXELFORMAT_BGRA32);
|
|
if (variant & PIPELINE_BLEND) {
|
|
colour.blend_state.enable_blend = true;
|
|
colour.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
|
|
colour.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
|
colour.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
|
|
colour.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
|
|
colour.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
|
colour.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
|
}
|
|
info.vertex_shader = (variant & PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic;
|
|
info.fragment_shader = _scene.fragment;
|
|
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.multisample_state.sample_count = _scene.sampleCount;
|
|
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 = (variant & PIPELINE_BLEND) ? false : true;
|
|
info.target_info.color_target_descriptions = &colour;
|
|
info.target_info.num_color_targets = 1;
|
|
info.target_info.depth_stencil_format = _scene.depthFormat;
|
|
info.target_info.has_depth_stencil_target = true;
|
|
_scene.pipelines[variant] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info);
|
|
if (_scene.pipelines[variant] == NULL) {
|
|
utilTrace("Scene: pipeline %d: %s", variant, SDL_GetError());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Picks the blob for the format the device accepts.
|
|
static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms, uint32_t storageBuffers) {
|
|
SDL_GPUShaderCreateInfo info;
|
|
SDL_GPUShaderFormat formats = SDL_GetGPUShaderFormats(_scene.device);
|
|
SDL_GPUShader *result;
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
if (formats & SDL_GPU_SHADERFORMAT_SPIRV) {
|
|
info.code = shader->spirv;
|
|
info.code_size = shader->spirvSize;
|
|
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
|
} else if (formats & SDL_GPU_SHADERFORMAT_DXIL) {
|
|
info.code = shader->dxil;
|
|
info.code_size = shader->dxilSize;
|
|
info.format = SDL_GPU_SHADERFORMAT_DXIL;
|
|
} else if (formats & SDL_GPU_SHADERFORMAT_MSL) {
|
|
info.code = shader->msl;
|
|
info.code_size = shader->mslSize;
|
|
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
|
} else {
|
|
utilTrace("Scene: the GPU device accepts none of SPIR-V, DXIL or MSL.");
|
|
return NULL;
|
|
}
|
|
info.entrypoint = shader->entryPoint;
|
|
info.stage = stage;
|
|
info.num_samplers = samplers;
|
|
info.num_uniform_buffers = uniforms;
|
|
info.num_storage_buffers = storageBuffers;
|
|
result = SDL_CreateGPUShader(_scene.device, &info);
|
|
if (result == NULL) {
|
|
utilTrace("Scene: shader %s: %s", shader->entryPoint, SDL_GetError());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
static bool _createShaders(void) {
|
|
_scene.vertexStatic = _createShader(&sceneShaderVertexStatic, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 1);
|
|
_scene.vertexSkinned = _createShader(&sceneShaderVertexSkinned, SDL_GPU_SHADERSTAGE_VERTEX, 0, 2, 1);
|
|
_scene.fragment = _createShader(&sceneShaderFragmentMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 2, 1, 0);
|
|
_scene.depthFragment = _createShader(&sceneShaderDepthMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0, 0);
|
|
_scene.particleVertex = _createShader(&sceneShaderParticleVertex, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 0);
|
|
_scene.particleFragment = _createShader(&sceneShaderParticleFragment, SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0, 0);
|
|
return (_scene.vertexStatic != NULL) && (_scene.vertexSkinned != NULL) && (_scene.fragment != NULL) && (_scene.depthFragment != NULL) && (_scene.particleVertex != NULL) && (_scene.particleFragment != 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.shadowMapsVersion++;
|
|
_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;
|
|
}
|
|
|
|
|
|
// Picks the best depth format the device offers.
|
|
static SDL_GPUTextureFormat _depthFormat(void) {
|
|
SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D16_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)) {
|
|
return wanted[x];
|
|
}
|
|
}
|
|
return SDL_GPU_TEXTUREFORMAT_D16_UNORM;
|
|
}
|
|
|
|
|
|
// Pipelines bake in the sample count, so a change in antialiasing drops them; they come back on
|
|
// first use.
|
|
static void _destroyPipelines(void) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < PIPELINE_COUNT; x++) {
|
|
if (_scene.pipelines[x] != NULL) {
|
|
SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.pipelines[x]);
|
|
_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;
|
|
}
|
|
|
|
|
|
static void _destroyTargets(void) {
|
|
// The SDL_Texture only wraps the colour target (it was created from it), so it goes first.
|
|
if (_scene.composite != NULL) {
|
|
SDL_DestroyTexture(_scene.composite);
|
|
_scene.composite = NULL;
|
|
}
|
|
if (_scene.colour != NULL) {
|
|
SDL_ReleaseGPUTexture(_scene.device, _scene.colour);
|
|
_scene.colour = NULL;
|
|
}
|
|
if (_scene.multisampled != NULL) {
|
|
SDL_ReleaseGPUTexture(_scene.device, _scene.multisampled);
|
|
_scene.multisampled = NULL;
|
|
}
|
|
if (_scene.depth != NULL) {
|
|
SDL_ReleaseGPUTexture(_scene.device, _scene.depth);
|
|
_scene.depth = NULL;
|
|
}
|
|
_scene.width = 0;
|
|
_scene.height = 0;
|
|
}
|
|
|
|
|
|
// 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, bool twoSided, const bool *skip, 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 || !node->shadowCaster || ((skip != NULL) && skip[x])) {
|
|
continue;
|
|
}
|
|
if (twoSided) {
|
|
// A bulb inside a closed mesh sees only its back faces; they must still cast.
|
|
variant |= PIPELINE_TWO_SIDED;
|
|
}
|
|
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;
|
|
}
|
|
memset(&drawUniforms, 0, sizeof(drawUniforms));
|
|
drawUniforms.modelViewProjection = mat4Multiply(*viewProjection, node->world);
|
|
drawUniforms.model = node->world;
|
|
if (mat4Invert(node->world, &inverse)) {
|
|
drawUniforms.normalMatrix = mat4Transpose(inverse);
|
|
} else {
|
|
drawUniforms.normalMatrix = identity;
|
|
}
|
|
// The strongest active morph targets, up to the shader's limit.
|
|
if ((mesh->morphBuffer != NULL) && (node->morphCount == mesh->morphCount)) {
|
|
int32_t active = 0;
|
|
int32_t t;
|
|
|
|
for (t = 0; t < node->morphCount; t++) {
|
|
if (node->morphWeights[t] == 0.0f) {
|
|
continue;
|
|
}
|
|
if (active < MAX_MORPHS) {
|
|
drawUniforms.morphWeights[active] = node->morphWeights[t];
|
|
drawUniforms.morphTargets[active] = t;
|
|
active++;
|
|
} else {
|
|
// Replace the weakest chosen one if this is stronger.
|
|
int32_t weakest = 0;
|
|
int32_t k;
|
|
|
|
for (k = 1; k < MAX_MORPHS; k++) {
|
|
if (fabsf(drawUniforms.morphWeights[k]) < fabsf(drawUniforms.morphWeights[weakest])) {
|
|
weakest = k;
|
|
}
|
|
}
|
|
if (fabsf(node->morphWeights[t]) > fabsf(drawUniforms.morphWeights[weakest])) {
|
|
drawUniforms.morphWeights[weakest] = node->morphWeights[t];
|
|
drawUniforms.morphTargets[weakest] = t;
|
|
}
|
|
}
|
|
}
|
|
drawUniforms.morphInfo[0] = active;
|
|
drawUniforms.morphInfo[1] = mesh->vertexCount;
|
|
}
|
|
SDL_PushGPUVertexUniformData(commands, 0, &drawUniforms, sizeof(drawUniforms));
|
|
SDL_BindGPUVertexStorageBuffers(pass, 0, (mesh->morphBuffer != NULL) ? &mesh->morphBuffer : &_scene.noMorphs, 1);
|
|
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);
|
|
}
|
|
|
|
|
|
// Draws this frame's particle runs, after every mesh, with the camera's axes for the billboards.
|
|
static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const Mat4T *viewProjection, Vec3T right, Vec3T up) {
|
|
SDL_GPUBufferBinding binding;
|
|
SDL_GPUTextureSamplerBinding sampler;
|
|
ParticleUniformsT uniforms;
|
|
int32_t x;
|
|
int32_t lastBlend = NO_HANDLE;
|
|
|
|
if ((_scene.particleRunCount == 0) || (_scene.particleBuffer == NULL)) {
|
|
return;
|
|
}
|
|
memset(&uniforms, 0, sizeof(uniforms));
|
|
uniforms.viewProjection = *viewProjection;
|
|
uniforms.right[0] = right.x;
|
|
uniforms.right[1] = right.y;
|
|
uniforms.right[2] = right.z;
|
|
uniforms.up[0] = up.x;
|
|
uniforms.up[1] = up.y;
|
|
uniforms.up[2] = up.z;
|
|
memset(&binding, 0, sizeof(binding));
|
|
binding.buffer = _scene.particleBuffer;
|
|
SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
|
|
memset(&sampler, 0, sizeof(sampler));
|
|
sampler.sampler = _scene.sampler;
|
|
for (x = 0; x < _scene.particleRunCount; x++) {
|
|
ParticleRunT *run = &_scene.particleRuns[x];
|
|
|
|
if ((_scene.particlePipelines[run->blend] == NULL) && !_createParticlePipeline(run->blend)) {
|
|
continue;
|
|
}
|
|
if ((int32_t)run->blend != lastBlend) {
|
|
SDL_BindGPUGraphicsPipeline(pass, _scene.particlePipelines[run->blend]);
|
|
SDL_PushGPUVertexUniformData(commands, 0, &uniforms, sizeof(uniforms));
|
|
lastBlend = run->blend;
|
|
}
|
|
sampler.texture = run->texture;
|
|
SDL_BindGPUFragmentSamplers(pass, 0, &sampler, 1);
|
|
SDL_DrawGPUPrimitives(pass, (Uint32)run->count, 1, (Uint32)run->first, 0);
|
|
}
|
|
}
|
|
|
|
|
|
// A fingerprint of everything a point light's faces depend on: the light, its range, and every
|
|
// caster's transform; skinned and morphing casters count as always changed.
|
|
static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) {
|
|
uint64_t hash = 1469598103934665603ULL;
|
|
int32_t x;
|
|
int32_t b;
|
|
const uint8_t *bytes;
|
|
|
|
bytes = (const uint8_t *)&_scene.nodes[shadow->node].world;
|
|
for (b = 0; b < (int32_t)sizeof(Mat4T); b++) {
|
|
hash = (hash ^ bytes[b]) * 1099511628211ULL;
|
|
}
|
|
bytes = (const uint8_t *)&shadow->far;
|
|
for (b = 0; b < (int32_t)sizeof(float); b++) {
|
|
hash = (hash ^ bytes[b]) * 1099511628211ULL;
|
|
}
|
|
for (x = 0; x < drawCount; x++) {
|
|
NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
|
MeshT *mesh = &_scene.meshes[node->mesh];
|
|
|
|
if (!node->shadowCaster) {
|
|
continue;
|
|
}
|
|
if ((mesh->skinned && (node->skinCount > 0)) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) {
|
|
return 0;
|
|
}
|
|
hash = (hash ^ (uint64_t)(uint32_t)_scene.draws[x].node) * 1099511628211ULL;
|
|
bytes = (const uint8_t *)&node->world;
|
|
for (b = 0; b < (int32_t)sizeof(Mat4T); b++) {
|
|
hash = (hash ^ bytes[b]) * 1099511628211ULL;
|
|
}
|
|
}
|
|
return (hash == 0) ? 1 : hash;
|
|
}
|
|
|
|
|
|
// Marks the draws a point light's face cannot see: bounding sphere against the 90 degree frustum.
|
|
static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bool *skip) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < drawCount; x++) {
|
|
Vec3T local = mat4TransformPoint(shadow->faceViews[face], _scene.draws[x].centre);
|
|
float radius = _scene.draws[x].radius;
|
|
float ahead = -local.z;
|
|
|
|
skip[x] = ((ahead + radius < shadow->near) || (ahead - radius > shadow->far) || (fabsf(local.x) - radius * 1.4143f > ahead + radius) || (fabsf(local.y) - radius * 1.4143f > ahead + radius));
|
|
}
|
|
}
|
|
|
|
|
|
// Unlinks node from its parent's child list.
|
|
static void _detach(int32_t node) {
|
|
int32_t parent = _scene.nodes[node].parent;
|
|
int32_t child;
|
|
|
|
if (parent == NO_HANDLE) {
|
|
return;
|
|
}
|
|
if (_scene.nodes[parent].firstChild == node) {
|
|
_scene.nodes[parent].firstChild = _scene.nodes[node].nextSibling;
|
|
} else {
|
|
child = _scene.nodes[parent].firstChild;
|
|
while ((child != NO_HANDLE) && (_scene.nodes[child].nextSibling != node)) {
|
|
child = _scene.nodes[child].nextSibling;
|
|
}
|
|
if (child != NO_HANDLE) {
|
|
_scene.nodes[child].nextSibling = _scene.nodes[node].nextSibling;
|
|
}
|
|
}
|
|
_scene.nodes[node].parent = NO_HANDLE;
|
|
_scene.nodes[node].nextSibling = NO_HANDLE;
|
|
}
|
|
|
|
|
|
// 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) {
|
|
int32_t x;
|
|
int32_t count = 0;
|
|
int32_t layers = 0;
|
|
NodeT *node;
|
|
LightUniformT *light;
|
|
Vec3T position;
|
|
Vec3T direction;
|
|
|
|
for (x = 0; (x < _scene.nodeCount) && (count < MAX_LIGHTS); x++) {
|
|
node = &_scene.nodes[x];
|
|
if (!node->used || !node->hasLight || !node->worldVisible) {
|
|
continue;
|
|
}
|
|
light = &uniforms->lights[count];
|
|
position = mat4TransformPoint(node->world, vec3(0.0f, 0.0f, 0.0f));
|
|
direction = vec3Normalize(mat4TransformVector(node->world, vec3(0.0f, 0.0f, -1.0f)));
|
|
light->positionType[0] = position.x;
|
|
light->positionType[1] = position.y;
|
|
light->positionType[2] = position.z;
|
|
light->positionType[3] = (float)node->light.type;
|
|
light->directionRange[0] = direction.x;
|
|
light->directionRange[1] = direction.y;
|
|
light->directionRange[2] = direction.z;
|
|
light->directionRange[3] = node->light.range;
|
|
light->color[0] = node->light.color.x * node->light.intensity;
|
|
light->color[1] = node->light.color.y * node->light.intensity;
|
|
light->color[2] = node->light.color.z * node->light.intensity;
|
|
light->color[3] = 1.0f;
|
|
light->cone[0] = cosf(node->light.innerDegrees * PI / 180.0f);
|
|
light->cone[1] = cosf(node->light.outerDegrees * PI / 180.0f);
|
|
light->cone[2] = 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++;
|
|
}
|
|
uniforms->counts[0] = (float)count;
|
|
}
|
|
|
|
|
|
// Joint matrices for one skinned draw: the mesh node's own transform cancels out (glTF says a
|
|
// skinned mesh ignores it), so each joint is world * inverseBind brought into the mesh's space.
|
|
static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms) {
|
|
Mat4T meshInverse;
|
|
int32_t x;
|
|
|
|
if (!mat4Invert(node->world, &meshInverse)) {
|
|
meshInverse = mat4Identity();
|
|
}
|
|
for (x = 0; (x < node->skinCount) && (x < MAX_JOINTS); x++) {
|
|
int32_t joint = node->skinJoints[x];
|
|
|
|
if (nodeValid(joint)) {
|
|
uniforms->joints[x] = mat4Multiply(meshInverse, mat4Multiply(_scene.nodes[joint].world, node->skinInverseBind[x]));
|
|
} else {
|
|
uniforms->joints[x] = mat4Identity();
|
|
}
|
|
}
|
|
for (; x < MAX_JOINTS; x++) {
|
|
uniforms->joints[x] = mat4Identity();
|
|
}
|
|
}
|
|
|
|
|
|
// 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, and a bounding sphere per draw.
|
|
for (x = 0; x < drawCount; x++) {
|
|
NodeT *node = &_scene.nodes[_scene.draws[x].node];
|
|
MeshT *mesh = &_scene.meshes[node->mesh];
|
|
Vec3T drawMin = vec3(0.0f, 0.0f, 0.0f);
|
|
Vec3T drawMax = vec3(0.0f, 0.0f, 0.0f);
|
|
|
|
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 (c == 0) {
|
|
drawMin = corner;
|
|
drawMax = corner;
|
|
} else {
|
|
drawMin = vec3(SDL_min(drawMin.x, corner.x), SDL_min(drawMin.y, corner.y), SDL_min(drawMin.z, corner.z));
|
|
drawMax = vec3(SDL_max(drawMax.x, corner.x), SDL_max(drawMax.y, corner.y), SDL_max(drawMax.z, corner.z));
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
_scene.draws[x].centre = vec3Scale(vec3Add(drawMin, drawMax), 0.5f);
|
|
_scene.draws[x].radius = vec3Length(vec3Subtract(drawMax, _scene.draws[x].centre)) + 0.001f;
|
|
}
|
|
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 {
|
|
// Near and far from the casters themselves: the nearest caster surface (a shade round the
|
|
// bulb) sets near, the range or the farthest caster sets far.
|
|
float nearest = 1.0e30f;
|
|
float farthest = 0.0f;
|
|
|
|
for (x = 0; x < drawCount; x++) {
|
|
float distance = vec3Length(vec3Subtract(_scene.draws[x].centre, position));
|
|
|
|
if (!_scene.nodes[_scene.draws[x].node].shadowCaster) {
|
|
continue;
|
|
}
|
|
nearest = SDL_min(nearest, distance - _scene.draws[x].radius);
|
|
farthest = SDL_max(farthest, distance + _scene.draws[x].radius);
|
|
}
|
|
far = (light->light.range > 0.0f) ? light->light.range : farthest;
|
|
far = SDL_max(far, 0.02f);
|
|
shadow->near = SDL_clamp(nearest, SHADOW_NEAR_MIN, far * 0.5f);
|
|
shadow->far = far;
|
|
if (light->light.type == LIGHT_SPOT) {
|
|
view = mat4LookAt(position, vec3Add(position, direction), up);
|
|
shadow->matrix = mat4Multiply(mat4Perspective(SDL_min(light->light.outerDegrees * 2.0f * SHADOW_MARGIN, 170.0f), 1.0f, shadow->near, far), view);
|
|
continue;
|
|
}
|
|
for (c = 0; c < CUBE_FACES; c++) {
|
|
shadow->faceViews[c] = mat4LookAt(position, vec3Add(position, faceForward[c]), faceUp[c]);
|
|
shadow->faces[c] = mat4Multiply(mat4Perspective(90.0f, 1.0f, shadow->near, shadow->far), shadow->faceViews[c]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Collects every 3D emitter's live particles into this frame's vertex list: emitters far to near,
|
|
// alpha-blended particles far to near within each, one run per frame texture.
|
|
static void _gatherParticles(Vec3T eye, Vec3T forward) {
|
|
EmitterViewT views[PARTICLE_DRAW_MAX];
|
|
DepthOrderT order[PARTICLE_DRAW_MAX];
|
|
DepthOrderT *particleOrder = NULL;
|
|
ParticleTexturesT *textures;
|
|
ParticleViewT *particle;
|
|
ParticleVertexT *vertex;
|
|
Vec3T origin;
|
|
int32_t emitters = 0;
|
|
int32_t total = 0;
|
|
int32_t needed;
|
|
int32_t e;
|
|
int32_t i;
|
|
int32_t k;
|
|
int32_t frame;
|
|
int32_t first;
|
|
int32_t c;
|
|
// The two triangles of a quad as corner offsets.
|
|
static const float corners[PARTICLE_VERTICES][2] = { { -1.0f, -1.0f }, { 1.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, 1.0f } };
|
|
|
|
_scene.particleRunCount = 0;
|
|
_scene.particleVertexCount = 0;
|
|
_releaseParticleTextures(false);
|
|
for (i = 0; (i < particlesCount()) && (emitters < PARTICLE_DRAW_MAX); i++) {
|
|
if (!particlesView(i, &views[emitters]) || (views[emitters].node < 0) || (views[emitters].count == 0)) {
|
|
continue;
|
|
}
|
|
origin = nodeValid(views[emitters].node) ? nodeGetWorldPosition(views[emitters].node) : eye;
|
|
order[emitters].depth = vec3Dot(vec3Subtract(origin, eye), forward);
|
|
order[emitters].index = emitters;
|
|
total += views[emitters].count;
|
|
emitters++;
|
|
}
|
|
if (total == 0) {
|
|
return;
|
|
}
|
|
needed = total * PARTICLE_VERTICES;
|
|
if (_scene.particleVertexCapacity < needed) {
|
|
_scene.particleVertices = SDL_realloc(_scene.particleVertices, sizeof(ParticleVertexT) * (size_t)needed);
|
|
if (_scene.particleVertices == NULL) {
|
|
utilDie("Out of memory for %d particle vertices.", needed);
|
|
}
|
|
_scene.particleVertexCapacity = needed;
|
|
}
|
|
qsort(order, (size_t)emitters, sizeof(DepthOrderT), _compareDepthOrder);
|
|
for (e = 0; e < emitters; e++) {
|
|
EmitterViewT *view = &views[order[e].index];
|
|
|
|
textures = _particleTextures(view);
|
|
if (textures == NULL) {
|
|
continue;
|
|
}
|
|
particleOrder = SDL_malloc(sizeof(DepthOrderT) * (size_t)view->count);
|
|
if (particleOrder == NULL) {
|
|
utilDie("Out of memory sorting particles.");
|
|
}
|
|
for (i = 0; i < view->count; i++) {
|
|
particleOrder[i].depth = vec3Dot(vec3Subtract(view->particles[i].position, eye), forward);
|
|
particleOrder[i].index = i;
|
|
}
|
|
if (view->blend == PARTICLE_ALPHA) {
|
|
qsort(particleOrder, (size_t)view->count, sizeof(DepthOrderT), _compareDepthOrder);
|
|
}
|
|
for (frame = 0; frame < textures->count; frame++) {
|
|
first = _scene.particleVertexCount;
|
|
for (k = 0; k < view->count; k++) {
|
|
particle = &view->particles[particleOrder[k].index];
|
|
if ((textures->count > 1) && (particle->frame != frame)) {
|
|
continue;
|
|
}
|
|
for (c = 0; c < PARTICLE_VERTICES; c++) {
|
|
vertex = &_scene.particleVertices[_scene.particleVertexCount++];
|
|
vertex->centre[0] = particle->position.x;
|
|
vertex->centre[1] = particle->position.y;
|
|
vertex->centre[2] = particle->position.z;
|
|
vertex->corner[0] = corners[c][0];
|
|
vertex->corner[1] = corners[c][1];
|
|
vertex->size = particle->size;
|
|
vertex->angle = particle->angle;
|
|
vertex->colour[0] = particle->colour[0];
|
|
vertex->colour[1] = particle->colour[1];
|
|
vertex->colour[2] = particle->colour[2];
|
|
vertex->colour[3] = particle->colour[3];
|
|
vertex->uv[0] = corners[c][0] * 0.5f + 0.5f;
|
|
vertex->uv[1] = 0.5f - corners[c][1] * 0.5f;
|
|
}
|
|
}
|
|
if ((_scene.particleVertexCount > first) && (_scene.particleRunCount < (int32_t)(sizeof(_scene.particleRuns) / sizeof(_scene.particleRuns[0])))) {
|
|
_scene.particleRuns[_scene.particleRunCount].first = first;
|
|
_scene.particleRuns[_scene.particleRunCount].count = _scene.particleVertexCount - first;
|
|
_scene.particleRuns[_scene.particleRunCount].texture = textures->textures[frame];
|
|
_scene.particleRuns[_scene.particleRunCount].blend = view->blend;
|
|
_scene.particleRunCount++;
|
|
}
|
|
}
|
|
SDL_free(particleOrder);
|
|
}
|
|
}
|
|
|
|
|
|
static void _freeFeed(FeedT *feed) {
|
|
if (feed->target != NULL) {
|
|
SDL_DestroyTexture(feed->target);
|
|
}
|
|
memset(feed, 0, sizeof(*feed));
|
|
}
|
|
|
|
|
|
static void _freeMaterialTexture(MaterialT *material) {
|
|
if (material->texture != NULL) {
|
|
SDL_ReleaseGPUTexture(_scene.device, material->texture);
|
|
material->texture = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
static void _freeMorphs(MeshT *mesh) {
|
|
int32_t x;
|
|
|
|
if (mesh->morphBuffer != NULL) {
|
|
SDL_ReleaseGPUBuffer(_scene.device, mesh->morphBuffer);
|
|
mesh->morphBuffer = NULL;
|
|
}
|
|
for (x = 0; x < mesh->morphCount; x++) {
|
|
SDL_free(mesh->morphNames[x]);
|
|
}
|
|
SDL_free(mesh->morphNames);
|
|
mesh->morphNames = NULL;
|
|
mesh->morphCount = 0;
|
|
}
|
|
|
|
|
|
static void _freeMorphWeights(NodeT *node) {
|
|
SDL_free(node->morphWeights);
|
|
node->morphWeights = NULL;
|
|
node->morphCount = 0;
|
|
}
|
|
|
|
|
|
static void _freeSkin(NodeT *node) {
|
|
SDL_free(node->skinJoints);
|
|
SDL_free(node->skinInverseBind);
|
|
node->skinJoints = NULL;
|
|
node->skinInverseBind = NULL;
|
|
node->skinCount = 0;
|
|
}
|
|
|
|
|
|
// A surface of revolution around Y with flat caps: cylinders and cones. Sides get their own
|
|
// vertices so the caps can have flat normals.
|
|
static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **indices, int32_t *indexCount, float bottomRadius, float topRadius, float height, int32_t segments) {
|
|
int32_t x;
|
|
int32_t v = 0;
|
|
int32_t i = 0;
|
|
float angle;
|
|
float c;
|
|
float s;
|
|
float half = height / 2.0f;
|
|
float slope;
|
|
float slopeLength;
|
|
int32_t bottomCentre;
|
|
int32_t topCentre;
|
|
SceneVertexT *verts;
|
|
uint32_t *idx;
|
|
|
|
// Sides: two rings of segments + 1 vertices (the seam is doubled for the uv wrap), then a
|
|
// centre and ring per cap.
|
|
*vertexCount = (segments + 1) * 2 + (segments + 1) * 2;
|
|
*indexCount = segments * 6 + segments * 3 * 2;
|
|
verts = SDL_calloc((size_t)*vertexCount, sizeof(SceneVertexT));
|
|
idx = SDL_calloc((size_t)*indexCount, sizeof(uint32_t));
|
|
if ((verts == NULL) || (idx == NULL)) {
|
|
utilDie("Out of memory building a mesh.");
|
|
}
|
|
slope = bottomRadius - topRadius;
|
|
slopeLength = sqrtf(slope * slope + height * height);
|
|
for (x = 0; x <= segments; x++) {
|
|
angle = (float)x / (float)segments * 2.0f * PI;
|
|
c = cosf(angle);
|
|
s = sinf(angle);
|
|
verts[v++] = _vertex(c * bottomRadius, -half, s * bottomRadius, c * height / slopeLength, slope / slopeLength, s * height / slopeLength, (float)x / (float)segments, 1.0f);
|
|
verts[v++] = _vertex(c * topRadius, half, s * topRadius, c * height / slopeLength, slope / slopeLength, s * height / slopeLength, (float)x / (float)segments, 0.0f);
|
|
}
|
|
for (x = 0; x < segments; x++) {
|
|
// Counter-clockwise seen from outside: the ring runs with the angle, +X toward +Z.
|
|
idx[i++] = (uint32_t)(x * 2);
|
|
idx[i++] = (uint32_t)(x * 2 + 2);
|
|
idx[i++] = (uint32_t)(x * 2 + 1);
|
|
idx[i++] = (uint32_t)(x * 2 + 1);
|
|
idx[i++] = (uint32_t)(x * 2 + 2);
|
|
idx[i++] = (uint32_t)(x * 2 + 3);
|
|
}
|
|
bottomCentre = v;
|
|
verts[v++] = _vertex(0.0f, -half, 0.0f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f);
|
|
for (x = 0; x < segments; x++) {
|
|
angle = (float)x / (float)segments * 2.0f * PI;
|
|
verts[v++] = _vertex(cosf(angle) * bottomRadius, -half, sinf(angle) * bottomRadius, 0.0f, -1.0f, 0.0f, 0.5f + cosf(angle) / 2.0f, 0.5f + sinf(angle) / 2.0f);
|
|
}
|
|
for (x = 0; x < segments; x++) {
|
|
idx[i++] = (uint32_t)bottomCentre;
|
|
idx[i++] = (uint32_t)(bottomCentre + 1 + x);
|
|
idx[i++] = (uint32_t)(bottomCentre + 1 + (x + 1) % segments);
|
|
}
|
|
topCentre = v;
|
|
verts[v++] = _vertex(0.0f, half, 0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f);
|
|
for (x = 0; x < segments; x++) {
|
|
angle = (float)x / (float)segments * 2.0f * PI;
|
|
verts[v++] = _vertex(cosf(angle) * topRadius, half, sinf(angle) * topRadius, 0.0f, 1.0f, 0.0f, 0.5f + cosf(angle) / 2.0f, 0.5f - sinf(angle) / 2.0f);
|
|
}
|
|
for (x = 0; x < segments; x++) {
|
|
idx[i++] = (uint32_t)topCentre;
|
|
idx[i++] = (uint32_t)(topCentre + 1 + (x + 1) % segments);
|
|
idx[i++] = (uint32_t)(topCentre + 1 + x);
|
|
}
|
|
*vertices = verts;
|
|
*indices = idx;
|
|
}
|
|
|
|
|
|
// Sizes the node's weight list to its mesh's targets (weights start at 0).
|
|
static void _matchMorphWeights(NodeT *node) {
|
|
int32_t count = ((node->mesh != NO_HANDLE) && meshValid(node->mesh)) ? _scene.meshes[node->mesh].morphCount : 0;
|
|
|
|
if (count == node->morphCount) {
|
|
return;
|
|
}
|
|
_freeMorphWeights(node);
|
|
if (count > 0) {
|
|
node->morphWeights = SDL_calloc((size_t)count, sizeof(float));
|
|
if (node->morphWeights == NULL) {
|
|
utilDie("Out of memory allocating morph weights.");
|
|
}
|
|
node->morphCount = count;
|
|
}
|
|
}
|
|
|
|
|
|
// What the fragment shader samples for a material: its video feed, its image, or NULL.
|
|
static SDL_GPUTexture *_materialTexture(const MaterialT *material) {
|
|
if ((material->feed != NO_HANDLE) && (material->feed < _scene.feedCount) && _scene.feeds[material->feed].used) {
|
|
return _scene.feeds[material->feed].gpu;
|
|
}
|
|
return material->texture;
|
|
}
|
|
|
|
|
|
// The pipeline variant a node's mesh and material call for, created on first use.
|
|
static int32_t _lookupPipeline(int32_t node) {
|
|
int32_t variant = 0;
|
|
MaterialT *material;
|
|
|
|
if (_scene.meshes[_scene.nodes[node].mesh].skinned && (_scene.nodes[node].skinCount > 0)) {
|
|
variant |= PIPELINE_SKINNED;
|
|
}
|
|
if (_scene.nodes[node].material != NO_HANDLE) {
|
|
material = &_scene.materials[_scene.nodes[node].material];
|
|
if (material->blend) {
|
|
variant |= PIPELINE_BLEND;
|
|
}
|
|
if (material->doubleSided) {
|
|
variant |= PIPELINE_TWO_SIDED;
|
|
}
|
|
}
|
|
if ((_scene.pipelines[variant] == NULL) && !_createPipeline(variant)) {
|
|
return NO_HANDLE;
|
|
}
|
|
return variant;
|
|
}
|
|
|
|
|
|
// The GPU textures for an emitter's frames, made on first sight and remade when the frames change.
|
|
static ParticleTexturesT *_particleTextures(const EmitterViewT *view) {
|
|
ParticleTexturesT *entry = NULL;
|
|
int32_t x;
|
|
|
|
for (x = 0; x < _scene.particleTextureCount; x++) {
|
|
if (_scene.particleTextures[x].id == view->id) {
|
|
entry = &_scene.particleTextures[x];
|
|
break;
|
|
}
|
|
}
|
|
if ((entry != NULL) && (entry->version != view->textureVersion)) {
|
|
for (x = 0; x < entry->count; x++) {
|
|
SDL_ReleaseGPUTexture(_scene.device, entry->textures[x]);
|
|
}
|
|
SDL_free(entry->textures);
|
|
entry->textures = NULL;
|
|
entry->count = 0;
|
|
entry->version = 0;
|
|
}
|
|
if ((entry != NULL) && (entry->textures != NULL)) {
|
|
return entry;
|
|
}
|
|
if (entry == NULL) {
|
|
_scene.particleTextures = SDL_realloc(_scene.particleTextures, sizeof(ParticleTexturesT) * (size_t)(_scene.particleTextureCount + 1));
|
|
if (_scene.particleTextures == NULL) {
|
|
utilDie("Out of memory for particle textures.");
|
|
}
|
|
entry = &_scene.particleTextures[_scene.particleTextureCount++];
|
|
memset(entry, 0, sizeof(*entry));
|
|
entry->id = view->id;
|
|
}
|
|
entry->textures = SDL_calloc((size_t)view->frameCount, sizeof(SDL_GPUTexture *));
|
|
if (entry->textures == NULL) {
|
|
utilDie("Out of memory for particle textures.");
|
|
}
|
|
for (x = 0; x < view->frameCount; x++) {
|
|
entry->textures[x] = _uploadTexture(view->frames[x]);
|
|
if (entry->textures[x] == NULL) {
|
|
while (x > 0) {
|
|
x--;
|
|
SDL_ReleaseGPUTexture(_scene.device, entry->textures[x]);
|
|
}
|
|
SDL_free(entry->textures);
|
|
entry->textures = NULL;
|
|
return NULL;
|
|
}
|
|
}
|
|
entry->count = view->frameCount;
|
|
entry->version = view->textureVersion;
|
|
return entry;
|
|
}
|
|
|
|
|
|
static Mat4T _projection(void) {
|
|
float aspect = (_scene.height > 0) ? (float)_scene.width / (float)_scene.height : 1.0f;
|
|
|
|
if (_scene.perspective) {
|
|
return mat4Perspective(_scene.fov, aspect, _scene.near, _scene.far);
|
|
}
|
|
return mat4Orthographic(_scene.orthoHeight * aspect, _scene.orthoHeight, _scene.near, _scene.far);
|
|
}
|
|
|
|
|
|
// A depth format the shadow map can be both rendered into and sampled from.
|
|
// Drops the textures of emitters that no longer exist, or all of them.
|
|
static void _releaseParticleTextures(bool all) {
|
|
int32_t x = 0;
|
|
int32_t f;
|
|
|
|
while (x < _scene.particleTextureCount) {
|
|
ParticleTexturesT *entry = &_scene.particleTextures[x];
|
|
|
|
if (all || !emitterValid(entry->id)) {
|
|
for (f = 0; f < entry->count; f++) {
|
|
SDL_ReleaseGPUTexture(_scene.device, entry->textures[f]);
|
|
}
|
|
SDL_free(entry->textures);
|
|
_scene.particleTextureCount--;
|
|
*entry = _scene.particleTextures[_scene.particleTextureCount];
|
|
continue;
|
|
}
|
|
x++;
|
|
}
|
|
if (all) {
|
|
SDL_free(_scene.particleTextures);
|
|
_scene.particleTextures = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
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.
|
|
static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) {
|
|
NodeT *n = &_scene.nodes[node];
|
|
int32_t child;
|
|
|
|
n->world = mat4Multiply(*parentWorld, mat4Compose(n->translation, n->rotation, n->scale));
|
|
n->worldVisible = parentVisible && n->visible;
|
|
for (child = n->firstChild; child != NO_HANDLE; child = _scene.nodes[child].nextSibling) {
|
|
_updateWorld(child, &n->world, n->worldVisible);
|
|
}
|
|
}
|
|
|
|
|
|
// Copies data into a new GPU buffer through a transfer buffer.
|
|
static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size) {
|
|
SDL_GPUBufferCreateInfo info;
|
|
SDL_GPUTransferBufferCreateInfo transferInfo;
|
|
SDL_GPUTransferBufferLocation source;
|
|
SDL_GPUBufferRegion region;
|
|
SDL_GPUBuffer *buffer;
|
|
SDL_GPUTransferBuffer *transfer;
|
|
SDL_GPUCommandBuffer *commands;
|
|
SDL_GPUCopyPass *pass;
|
|
void *mapped;
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
info.usage = usage;
|
|
info.size = size;
|
|
buffer = SDL_CreateGPUBuffer(_scene.device, &info);
|
|
if (buffer == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
return NULL;
|
|
}
|
|
memset(&transferInfo, 0, sizeof(transferInfo));
|
|
transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
|
transferInfo.size = size;
|
|
transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo);
|
|
if (transfer == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
SDL_ReleaseGPUBuffer(_scene.device, buffer);
|
|
return NULL;
|
|
}
|
|
mapped = SDL_MapGPUTransferBuffer(_scene.device, transfer, false);
|
|
memcpy(mapped, data, size);
|
|
SDL_UnmapGPUTransferBuffer(_scene.device, transfer);
|
|
commands = SDL_AcquireGPUCommandBuffer(_scene.device);
|
|
pass = SDL_BeginGPUCopyPass(commands);
|
|
memset(&source, 0, sizeof(source));
|
|
memset(®ion, 0, sizeof(region));
|
|
source.transfer_buffer = transfer;
|
|
region.buffer = buffer;
|
|
region.size = size;
|
|
SDL_UploadToGPUBuffer(pass, &source, ®ion, false);
|
|
SDL_EndGPUCopyPass(pass);
|
|
SDL_SubmitGPUCommandBuffer(commands);
|
|
SDL_ReleaseGPUTransferBuffer(_scene.device, transfer);
|
|
return buffer;
|
|
}
|
|
|
|
|
|
// Copies this frame's particle vertices to the GPU, growing the buffers when a frame needs more.
|
|
static void _uploadParticles(SDL_GPUCommandBuffer *commands) {
|
|
SDL_GPUBufferCreateInfo info;
|
|
SDL_GPUTransferBufferCreateInfo transferInfo;
|
|
SDL_GPUTransferBufferLocation source;
|
|
SDL_GPUBufferRegion region;
|
|
SDL_GPUCopyPass *pass;
|
|
void *mapped;
|
|
uint32_t bytes = (uint32_t)_scene.particleVertexCount * (uint32_t)sizeof(ParticleVertexT);
|
|
|
|
if (bytes == 0) {
|
|
return;
|
|
}
|
|
if (bytes > _scene.particleCapacity) {
|
|
if (_scene.particleBuffer != NULL) {
|
|
SDL_ReleaseGPUBuffer(_scene.device, _scene.particleBuffer);
|
|
}
|
|
if (_scene.particleTransfer != NULL) {
|
|
SDL_ReleaseGPUTransferBuffer(_scene.device, _scene.particleTransfer);
|
|
}
|
|
_scene.particleCapacity = SDL_max(bytes * 2, 65536);
|
|
memset(&info, 0, sizeof(info));
|
|
info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
|
info.size = _scene.particleCapacity;
|
|
_scene.particleBuffer = SDL_CreateGPUBuffer(_scene.device, &info);
|
|
memset(&transferInfo, 0, sizeof(transferInfo));
|
|
transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
|
transferInfo.size = _scene.particleCapacity;
|
|
_scene.particleTransfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo);
|
|
if ((_scene.particleBuffer == NULL) || (_scene.particleTransfer == NULL)) {
|
|
utilTrace("Scene: particle buffer: %s", SDL_GetError());
|
|
_scene.particleRunCount = 0;
|
|
return;
|
|
}
|
|
}
|
|
mapped = SDL_MapGPUTransferBuffer(_scene.device, _scene.particleTransfer, true);
|
|
memcpy(mapped, _scene.particleVertices, bytes);
|
|
SDL_UnmapGPUTransferBuffer(_scene.device, _scene.particleTransfer);
|
|
pass = SDL_BeginGPUCopyPass(commands);
|
|
memset(&source, 0, sizeof(source));
|
|
memset(®ion, 0, sizeof(region));
|
|
source.transfer_buffer = _scene.particleTransfer;
|
|
region.buffer = _scene.particleBuffer;
|
|
region.size = bytes;
|
|
SDL_UploadToGPUBuffer(pass, &source, ®ion, true);
|
|
SDL_EndGPUCopyPass(pass);
|
|
}
|
|
|
|
|
|
// An RGBA sampler texture from any surface.
|
|
static SDL_GPUTexture *_uploadTexture(SDL_Surface *image) {
|
|
SDL_Surface *rgba;
|
|
SDL_GPUTextureCreateInfo info;
|
|
SDL_GPUTransferBufferCreateInfo transferInfo;
|
|
SDL_GPUTextureTransferInfo source;
|
|
SDL_GPUTextureRegion region;
|
|
SDL_GPUTexture *texture;
|
|
SDL_GPUTransferBuffer *transfer;
|
|
SDL_GPUCommandBuffer *commands;
|
|
SDL_GPUCopyPass *pass;
|
|
void *mapped;
|
|
uint32_t size;
|
|
|
|
rgba = SDL_ConvertSurface(image, SDL_PIXELFORMAT_RGBA32);
|
|
if (rgba == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
return NULL;
|
|
}
|
|
size = (uint32_t)(rgba->w * rgba->h * 4);
|
|
memset(&info, 0, sizeof(info));
|
|
info.type = SDL_GPU_TEXTURETYPE_2D;
|
|
info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
|
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
|
info.width = (Uint32)rgba->w;
|
|
info.height = (Uint32)rgba->h;
|
|
info.layer_count_or_depth = 1;
|
|
info.num_levels = 1;
|
|
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
|
texture = SDL_CreateGPUTexture(_scene.device, &info);
|
|
if (texture == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
SDL_DestroySurface(rgba);
|
|
return NULL;
|
|
}
|
|
memset(&transferInfo, 0, sizeof(transferInfo));
|
|
transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
|
transferInfo.size = size;
|
|
transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo);
|
|
if (transfer == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
SDL_ReleaseGPUTexture(_scene.device, texture);
|
|
SDL_DestroySurface(rgba);
|
|
return NULL;
|
|
}
|
|
mapped = SDL_MapGPUTransferBuffer(_scene.device, transfer, false);
|
|
if (rgba->pitch == rgba->w * 4) {
|
|
memcpy(mapped, rgba->pixels, size);
|
|
} else {
|
|
int32_t y;
|
|
|
|
for (y = 0; y < rgba->h; y++) {
|
|
memcpy((uint8_t *)mapped + y * rgba->w * 4, (uint8_t *)rgba->pixels + y * rgba->pitch, (size_t)rgba->w * 4);
|
|
}
|
|
}
|
|
SDL_UnmapGPUTransferBuffer(_scene.device, transfer);
|
|
commands = SDL_AcquireGPUCommandBuffer(_scene.device);
|
|
pass = SDL_BeginGPUCopyPass(commands);
|
|
memset(&source, 0, sizeof(source));
|
|
memset(®ion, 0, sizeof(region));
|
|
source.transfer_buffer = transfer;
|
|
region.texture = texture;
|
|
region.w = (Uint32)rgba->w;
|
|
region.h = (Uint32)rgba->h;
|
|
region.d = 1;
|
|
SDL_UploadToGPUTexture(pass, &source, ®ion, false);
|
|
SDL_EndGPUCopyPass(pass);
|
|
SDL_SubmitGPUCommandBuffer(commands);
|
|
SDL_ReleaseGPUTransferBuffer(_scene.device, transfer);
|
|
SDL_DestroySurface(rgba);
|
|
return texture;
|
|
}
|
|
|
|
|
|
static SceneVertexT _vertex(float x, float y, float z, float nx, float ny, float nz, float u, float v) {
|
|
SceneVertexT out;
|
|
|
|
memset(&out, 0, sizeof(out));
|
|
out.position[0] = x;
|
|
out.position[1] = y;
|
|
out.position[2] = z;
|
|
out.normal[0] = nx;
|
|
out.normal[1] = ny;
|
|
out.normal[2] = nz;
|
|
out.uv[0] = u;
|
|
out.uv[1] = v;
|
|
out.weights[0] = 1.0f;
|
|
return out;
|
|
}
|
|
|
|
|
|
// The view matrix: the inverse of the camera node's world matrix, or the default view.
|
|
static Mat4T _view(void) {
|
|
Mat4T view;
|
|
|
|
if ((_scene.cameraNode == NO_HANDLE) || !nodeValid(_scene.cameraNode)) {
|
|
return mat4LookAt(vec3(0.0f, 0.0f, DEFAULT_EYE_Z), vec3(0.0f, 0.0f, 0.0f), vec3(0.0f, 1.0f, 0.0f));
|
|
}
|
|
if (!mat4Invert(_scene.nodes[_scene.cameraNode].world, &view)) {
|
|
return mat4Identity();
|
|
}
|
|
return view;
|
|
}
|
|
|
|
|
|
static Vec3T _viewEye(void) {
|
|
if ((_scene.cameraNode == NO_HANDLE) || !nodeValid(_scene.cameraNode)) {
|
|
return vec3(0.0f, 0.0f, DEFAULT_EYE_Z);
|
|
}
|
|
return mat4TransformPoint(_scene.nodes[_scene.cameraNode].world, vec3(0.0f, 0.0f, 0.0f));
|
|
}
|
|
|
|
|
|
// ===== Camera =====
|
|
|
|
// Any node can be the camera: it looks down its own -Z. NO_HANDLE restores the default view.
|
|
bool cameraSet(int32_t node) {
|
|
if ((node != NO_HANDLE) && !nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.cameraNode = node;
|
|
return true;
|
|
}
|
|
|
|
|
|
void cameraSetOrthographic(float height, float near, float far) {
|
|
_scene.perspective = false;
|
|
_scene.orthoHeight = height;
|
|
_scene.near = near;
|
|
_scene.far = far;
|
|
}
|
|
|
|
|
|
void cameraSetPerspective(float fovDegrees, float near, float far) {
|
|
_scene.perspective = true;
|
|
_scene.fov = fovDegrees;
|
|
_scene.near = near;
|
|
_scene.far = far;
|
|
}
|
|
|
|
|
|
// ===== Lights =====
|
|
|
|
// Makes an existing node a light. Directional and spot lights shine down the node's -Z.
|
|
bool lightAttach(int32_t node, LightTypeE type) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].hasLight = true;
|
|
_scene.nodes[node].light.type = type;
|
|
_scene.nodes[node].light.color = vec3(1.0f, 1.0f, 1.0f);
|
|
_scene.nodes[node].light.intensity = 1.0f;
|
|
_scene.nodes[node].light.range = 0.0f;
|
|
_scene.nodes[node].light.innerDegrees = 20.0f;
|
|
_scene.nodes[node].light.outerDegrees = 30.0f;
|
|
return true;
|
|
}
|
|
|
|
|
|
// A new node carrying a light.
|
|
int32_t lightNew(LightTypeE type, int32_t parent) {
|
|
int32_t node = nodeNew(parent);
|
|
|
|
if (node != NO_HANDLE) {
|
|
lightAttach(node, type);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
|
|
bool lightSetColor(int32_t node, uint8_t r, uint8_t g, uint8_t b) {
|
|
if (!nodeValid(node) || !_scene.nodes[node].hasLight) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].light.color = vec3(r / COLOUR_MAX, g / COLOUR_MAX, b / COLOUR_MAX);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool lightSetCone(int32_t node, float innerDegrees, float outerDegrees) {
|
|
if (!nodeValid(node) || !_scene.nodes[node].hasLight) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].light.innerDegrees = innerDegrees;
|
|
_scene.nodes[node].light.outerDegrees = outerDegrees;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool lightSetIntensity(int32_t node, float intensity) {
|
|
if (!nodeValid(node) || !_scene.nodes[node].hasLight) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].light.intensity = intensity;
|
|
return true;
|
|
}
|
|
|
|
|
|
// 0 means no range limit.
|
|
bool lightSetRange(int32_t node, float range) {
|
|
if (!nodeValid(node) || !_scene.nodes[node].hasLight) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].light.range = range;
|
|
return true;
|
|
}
|
|
|
|
|
|
// 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 =====
|
|
|
|
bool materialDelete(int32_t material) {
|
|
int32_t x;
|
|
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_freeMaterialTexture(&_scene.materials[material]);
|
|
_scene.materials[material].used = false;
|
|
// Nodes that used it fall back to the default look.
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
if (_scene.nodes[x].used && (_scene.nodes[x].material == material)) {
|
|
_scene.nodes[x].material = NO_HANDLE;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// White, half rough, no texture.
|
|
int32_t materialNew(void) {
|
|
return _allocMaterial();
|
|
}
|
|
|
|
|
|
bool materialSetBlend(int32_t material, bool blend) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].blend = blend;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].baseColor.x = r / COLOUR_MAX;
|
|
_scene.materials[material].baseColor.y = g / COLOUR_MAX;
|
|
_scene.materials[material].baseColor.z = b / COLOUR_MAX;
|
|
_scene.materials[material].baseColor.w = a / COLOUR_MAX;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialSetDoubleSided(int32_t material, bool doubleSided) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].doubleSided = doubleSided;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialSetEmissive(int32_t material, uint8_t r, uint8_t g, uint8_t b) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].emissive = vec3(r / COLOUR_MAX, g / COLOUR_MAX, b / COLOUR_MAX);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialSetMetallic(int32_t material, float metallic) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].metallic = SDL_clamp(metallic, 0.0f, 1.0f);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialSetRoughness(int32_t material, float roughness) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].roughness = SDL_clamp(roughness, 0.0f, 1.0f);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Copies the image into a GPU texture; NULL removes the texture.
|
|
bool materialSetTexture(int32_t material, SDL_Surface *image) {
|
|
SDL_GPUTexture *texture = NULL;
|
|
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
if (image != NULL) {
|
|
texture = _uploadTexture(image);
|
|
if (texture == NULL) {
|
|
return false;
|
|
}
|
|
}
|
|
_freeMaterialTexture(&_scene.materials[material]);
|
|
_scene.materials[material].texture = texture;
|
|
_scene.materials[material].feed = NO_HANDLE;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialSetUnlit(int32_t material, bool unlit) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.materials[material].unlit = unlit;
|
|
return true;
|
|
}
|
|
|
|
|
|
// A video player's frames as the base colour texture; replaces any image. The frames arrive
|
|
// through sceneUpdateVideo each frame.
|
|
bool materialSetVideo(int32_t material, int32_t player) {
|
|
if (!materialValid(material)) {
|
|
return false;
|
|
}
|
|
_freeMaterialTexture(&_scene.materials[material]);
|
|
_scene.materials[material].feed = _allocFeed(player);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool materialValid(int32_t material) {
|
|
return (material >= 0) && (material < _scene.materialCount) && _scene.materials[material].used;
|
|
}
|
|
|
|
|
|
// ===== Meshes =====
|
|
|
|
// Six faces with their own vertices so each has a flat normal. Centred on the origin.
|
|
int32_t meshBox(float width, float height, float depth) {
|
|
SceneVertexT vertices[24];
|
|
uint32_t indices[36];
|
|
float w = width / 2.0f;
|
|
float h = height / 2.0f;
|
|
float d = depth / 2.0f;
|
|
int32_t face;
|
|
int32_t v = 0;
|
|
int32_t i = 0;
|
|
// Per face: normal, then the four corners counter-clockwise seen from outside.
|
|
float faces[6][5][3] = {
|
|
{ { 0.0f, 0.0f, 1.0f }, { -w, -h, d }, { w, -h, d }, { w, h, d }, { -w, h, d } }, // Front (+Z)
|
|
{ { 0.0f, 0.0f, -1.0f }, { w, -h, -d }, { -w, -h, -d }, { -w, h, -d }, { w, h, -d } }, // Back (-Z)
|
|
{ { 1.0f, 0.0f, 0.0f }, { w, -h, d }, { w, -h, -d }, { w, h, -d }, { w, h, d } }, // Right (+X)
|
|
{ { -1.0f, 0.0f, 0.0f }, { -w, -h, -d }, { -w, -h, d }, { -w, h, d }, { -w, h, -d } }, // Left (-X)
|
|
{ { 0.0f, 1.0f, 0.0f }, { -w, h, d }, { w, h, d }, { w, h, -d }, { -w, h, -d } }, // Top (+Y)
|
|
{ { 0.0f, -1.0f, 0.0f }, { -w, -h, -d }, { w, -h, -d }, { w, -h, d }, { -w, -h, d } }, // Bottom (-Y)
|
|
};
|
|
float uvs[4][2] = { { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f } };
|
|
|
|
for (face = 0; face < 6; face++) {
|
|
int32_t corner;
|
|
|
|
for (corner = 0; corner < 4; corner++) {
|
|
vertices[v++] = _vertex(faces[face][corner + 1][0], faces[face][corner + 1][1], faces[face][corner + 1][2], faces[face][0][0], faces[face][0][1], faces[face][0][2], uvs[corner][0], uvs[corner][1]);
|
|
}
|
|
indices[i++] = (uint32_t)(face * 4);
|
|
indices[i++] = (uint32_t)(face * 4 + 1);
|
|
indices[i++] = (uint32_t)(face * 4 + 2);
|
|
indices[i++] = (uint32_t)(face * 4);
|
|
indices[i++] = (uint32_t)(face * 4 + 2);
|
|
indices[i++] = (uint32_t)(face * 4 + 3);
|
|
}
|
|
return _addMesh(vertices, 24, indices, 36, false);
|
|
}
|
|
|
|
|
|
int32_t meshCone(float radius, float height, int32_t segments) {
|
|
SceneVertexT *vertices;
|
|
uint32_t *indices;
|
|
int32_t vertexCount;
|
|
int32_t indexCount;
|
|
int32_t mesh;
|
|
|
|
_lathe(&vertices, &vertexCount, &indices, &indexCount, radius, 0.0f, height, SDL_max(segments, MIN_SEGMENTS));
|
|
mesh = _addMesh(vertices, vertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
SDL_free(indices);
|
|
return mesh;
|
|
}
|
|
|
|
|
|
int32_t meshCylinder(float radius, float height, int32_t segments) {
|
|
SceneVertexT *vertices;
|
|
uint32_t *indices;
|
|
int32_t vertexCount;
|
|
int32_t indexCount;
|
|
int32_t mesh;
|
|
|
|
_lathe(&vertices, &vertexCount, &indices, &indexCount, radius, radius, height, SDL_max(segments, MIN_SEGMENTS));
|
|
mesh = _addMesh(vertices, vertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
SDL_free(indices);
|
|
return mesh;
|
|
}
|
|
|
|
|
|
bool meshDelete(int32_t mesh) {
|
|
int32_t x;
|
|
|
|
if (!meshValid(mesh)) {
|
|
return false;
|
|
}
|
|
if (_scene.meshes[mesh].vertexBuffer != NULL) {
|
|
SDL_ReleaseGPUBuffer(_scene.device, _scene.meshes[mesh].vertexBuffer);
|
|
}
|
|
if (_scene.meshes[mesh].indexBuffer != NULL) {
|
|
SDL_ReleaseGPUBuffer(_scene.device, _scene.meshes[mesh].indexBuffer);
|
|
}
|
|
SDL_free(_scene.meshes[mesh].positions);
|
|
SDL_free(_scene.meshes[mesh].indices);
|
|
SDL_free(_scene.meshes[mesh].vertices);
|
|
if (_scene.meshes[mesh].transfer != NULL) {
|
|
SDL_ReleaseGPUTransferBuffer(_scene.device, _scene.meshes[mesh].transfer);
|
|
}
|
|
_freeMorphs(&_scene.meshes[mesh]);
|
|
memset(&_scene.meshes[mesh], 0, sizeof(MeshT));
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) {
|
|
_scene.nodes[x].mesh = NO_HANDLE;
|
|
_freeMorphWeights(&_scene.nodes[x]);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// A morph target by name, or -1.
|
|
int32_t meshFindMorph(int32_t mesh, const char *name) {
|
|
int32_t x;
|
|
|
|
if (!meshValid(mesh) || (name == NULL)) {
|
|
return NO_HANDLE;
|
|
}
|
|
for (x = 0; x < _scene.meshes[mesh].morphCount; x++) {
|
|
if ((_scene.meshes[mesh].morphNames[x] != NULL) && (strcmp(_scene.meshes[mesh].morphNames[x], name) == 0)) {
|
|
return x;
|
|
}
|
|
}
|
|
return NO_HANDLE;
|
|
}
|
|
|
|
|
|
// The mesh's geometry as kept on the CPU: x, y, z per vertex and triangle indices.
|
|
// Rewrites a mesh's vertex positions (x, y, z per vertex, in the mesh's space), recomputing normals
|
|
// and bounds and uploading in place, for meshes a soft body drives.
|
|
bool meshSetPositions(int32_t mesh, const float *positions) {
|
|
MeshT *m;
|
|
SDL_GPUTransferBufferCreateInfo transferInfo;
|
|
SDL_GPUTransferBufferLocation source;
|
|
SDL_GPUBufferRegion region;
|
|
SDL_GPUCommandBuffer *commands;
|
|
SDL_GPUCopyPass *pass;
|
|
void *mapped;
|
|
uint32_t size;
|
|
int32_t x;
|
|
|
|
if (!meshValid(mesh) || (positions == NULL)) {
|
|
return false;
|
|
}
|
|
m = &_scene.meshes[mesh];
|
|
size = (uint32_t)(sizeof(SceneVertexT) * (size_t)m->vertexCount);
|
|
memcpy(m->positions, positions, sizeof(float) * 3 * (size_t)m->vertexCount);
|
|
for (x = 0; x < m->vertexCount; x++) {
|
|
m->vertices[x].position[0] = positions[x * 3];
|
|
m->vertices[x].position[1] = positions[x * 3 + 1];
|
|
m->vertices[x].position[2] = positions[x * 3 + 2];
|
|
if (x == 0) {
|
|
m->boundsMin = vec3(positions[0], positions[1], positions[2]);
|
|
m->boundsMax = m->boundsMin;
|
|
} else {
|
|
m->boundsMin = vec3(SDL_min(m->boundsMin.x, positions[x * 3]), SDL_min(m->boundsMin.y, positions[x * 3 + 1]), SDL_min(m->boundsMin.z, positions[x * 3 + 2]));
|
|
m->boundsMax = vec3(SDL_max(m->boundsMax.x, positions[x * 3]), SDL_max(m->boundsMax.y, positions[x * 3 + 1]), SDL_max(m->boundsMax.z, positions[x * 3 + 2]));
|
|
}
|
|
}
|
|
sceneComputeNormals(m->vertices, m->vertexCount, m->indices, (int32_t)m->indexCount);
|
|
if (_scene.device == NULL) {
|
|
return true;
|
|
}
|
|
if (m->transfer == NULL) {
|
|
memset(&transferInfo, 0, sizeof(transferInfo));
|
|
transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
|
transferInfo.size = size;
|
|
m->transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo);
|
|
if (m->transfer == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
return false;
|
|
}
|
|
}
|
|
mapped = SDL_MapGPUTransferBuffer(_scene.device, m->transfer, true);
|
|
memcpy(mapped, m->vertices, size);
|
|
SDL_UnmapGPUTransferBuffer(_scene.device, m->transfer);
|
|
commands = SDL_AcquireGPUCommandBuffer(_scene.device);
|
|
if (commands == NULL) {
|
|
return false;
|
|
}
|
|
pass = SDL_BeginGPUCopyPass(commands);
|
|
memset(&source, 0, sizeof(source));
|
|
memset(®ion, 0, sizeof(region));
|
|
source.transfer_buffer = m->transfer;
|
|
region.buffer = m->vertexBuffer;
|
|
region.size = size;
|
|
SDL_UploadToGPUBuffer(pass, &source, ®ion, true);
|
|
SDL_EndGPUCopyPass(pass);
|
|
SDL_SubmitGPUCommandBuffer(commands);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount) {
|
|
if (!meshValid(mesh)) {
|
|
return false;
|
|
}
|
|
*positions = _scene.meshes[mesh].positions;
|
|
*vertexCount = _scene.meshes[mesh].vertexCount;
|
|
*indices = _scene.meshes[mesh].indices;
|
|
*indexCount = (int32_t)_scene.meshes[mesh].indexCount;
|
|
return true;
|
|
}
|
|
|
|
|
|
int32_t meshGetMorphCount(int32_t mesh) {
|
|
if (!meshValid(mesh)) {
|
|
return 0;
|
|
}
|
|
return _scene.meshes[mesh].morphCount;
|
|
}
|
|
|
|
|
|
const char *meshGetMorphName(int32_t mesh, int32_t target) {
|
|
if (!meshValid(mesh) || (target < 0) || (target >= _scene.meshes[mesh].morphCount) || (_scene.meshes[mesh].morphNames[target] == NULL)) {
|
|
return "";
|
|
}
|
|
return _scene.meshes[mesh].morphNames[target];
|
|
}
|
|
|
|
|
|
// Raw geometry from a script: positions (3 per vertex), normals (3, may be NULL for flat
|
|
// shading computed here), uvs (2, may be NULL), and triangle indices.
|
|
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) {
|
|
SceneVertexT *vertices;
|
|
int32_t x;
|
|
int32_t mesh;
|
|
|
|
if ((positions == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) {
|
|
return NO_HANDLE;
|
|
}
|
|
for (x = 0; x < indexCount; x++) {
|
|
if (indices[x] >= (uint32_t)vertexCount) {
|
|
return NO_HANDLE;
|
|
}
|
|
}
|
|
vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT));
|
|
if (vertices == NULL) {
|
|
utilDie("Out of memory building a mesh.");
|
|
}
|
|
for (x = 0; x < vertexCount; x++) {
|
|
vertices[x] = _vertex(positions[x * 3], positions[x * 3 + 1], positions[x * 3 + 2], 0.0f, 0.0f, 0.0f, uvs ? uvs[x * 2] : 0.0f, uvs ? uvs[x * 2 + 1] : 0.0f);
|
|
if (normals != NULL) {
|
|
vertices[x].normal[0] = normals[x * 3];
|
|
vertices[x].normal[1] = normals[x * 3 + 1];
|
|
vertices[x].normal[2] = normals[x * 3 + 2];
|
|
}
|
|
}
|
|
if (normals == NULL) {
|
|
sceneComputeNormals(vertices, vertexCount, indices, indexCount);
|
|
}
|
|
mesh = _addMesh(vertices, vertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
return mesh;
|
|
}
|
|
|
|
|
|
// Geometry a loader has already laid out in GPU form.
|
|
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned) {
|
|
int32_t x;
|
|
|
|
if ((vertices == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) {
|
|
return NO_HANDLE;
|
|
}
|
|
for (x = 0; x < indexCount; x++) {
|
|
if (indices[x] >= (uint32_t)vertexCount) {
|
|
return NO_HANDLE;
|
|
}
|
|
}
|
|
return _addMesh(vertices, vertexCount, indices, indexCount, skinned);
|
|
}
|
|
|
|
|
|
// A quad in the XZ plane facing +Y.
|
|
// A plane of columns x rows quads, for cloth and terrain that bends.
|
|
int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows) {
|
|
SceneVertexT *vertices;
|
|
uint32_t *indices;
|
|
int32_t vertexCount = (columns + 1) * (rows + 1);
|
|
int32_t indexCount = columns * rows * 6;
|
|
int32_t x;
|
|
int32_t y;
|
|
int32_t mesh;
|
|
|
|
if ((columns < 1) || (rows < 1)) {
|
|
return NO_HANDLE;
|
|
}
|
|
vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT));
|
|
indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t));
|
|
if ((vertices == NULL) || (indices == NULL)) {
|
|
utilDie("Out of memory making a grid.");
|
|
}
|
|
for (y = 0; y <= rows; y++) {
|
|
for (x = 0; x <= columns; x++) {
|
|
float u = (float)x / (float)columns;
|
|
float v = (float)y / (float)rows;
|
|
|
|
vertices[y * (columns + 1) + x] = _vertex(-width / 2.0f + width * u, 0.0f, depth / 2.0f - depth * v, 0.0f, 1.0f, 0.0f, u, 1.0f - v);
|
|
}
|
|
}
|
|
for (y = 0; y < rows; y++) {
|
|
for (x = 0; x < columns; x++) {
|
|
uint32_t a = (uint32_t)(y * (columns + 1) + x);
|
|
uint32_t b = a + 1;
|
|
uint32_t c = a + (uint32_t)(columns + 1);
|
|
uint32_t d = c + 1;
|
|
uint32_t *tri = &indices[(y * columns + x) * 6];
|
|
|
|
tri[0] = a;
|
|
tri[1] = b;
|
|
tri[2] = d;
|
|
tri[3] = a;
|
|
tri[4] = d;
|
|
tri[5] = c;
|
|
}
|
|
}
|
|
mesh = _addMesh(vertices, vertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
SDL_free(indices);
|
|
return mesh;
|
|
}
|
|
|
|
|
|
int32_t meshPlane(float width, float depth) {
|
|
SceneVertexT vertices[4];
|
|
uint32_t indices[6] = { 0, 1, 2, 0, 2, 3 };
|
|
float w = width / 2.0f;
|
|
float d = depth / 2.0f;
|
|
|
|
vertices[0] = _vertex(-w, 0.0f, d, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f);
|
|
vertices[1] = _vertex( w, 0.0f, d, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f);
|
|
vertices[2] = _vertex( w, 0.0f, -d, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f);
|
|
vertices[3] = _vertex(-w, 0.0f, -d, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f);
|
|
return _addMesh(vertices, 4, indices, 6, false);
|
|
}
|
|
|
|
|
|
// Gives the mesh morph targets: deltas holds, per target, per vertex, a position delta (x, y, z)
|
|
// and a normal delta (x, y, z), six floats; names may be NULL or hold NULL entries. Nodes using
|
|
// the mesh get a weight per target, all 0.
|
|
bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names) {
|
|
MeshT *m;
|
|
float *packed;
|
|
int32_t count;
|
|
int32_t x;
|
|
|
|
if (!meshValid(mesh) || (deltas == NULL) || (targetCount <= 0)) {
|
|
return false;
|
|
}
|
|
m = &_scene.meshes[mesh];
|
|
count = targetCount * m->vertexCount;
|
|
packed = SDL_calloc((size_t)count * MORPH_FLOATS, sizeof(float));
|
|
if (packed == NULL) {
|
|
utilDie("Out of memory packing morph targets.");
|
|
}
|
|
// float4 pairs for the shader: xyz0 position delta, xyz0 normal delta.
|
|
for (x = 0; x < count; x++) {
|
|
packed[x * MORPH_FLOATS] = deltas[x * 6];
|
|
packed[x * MORPH_FLOATS + 1] = deltas[x * 6 + 1];
|
|
packed[x * MORPH_FLOATS + 2] = deltas[x * 6 + 2];
|
|
packed[x * MORPH_FLOATS + 4] = deltas[x * 6 + 3];
|
|
packed[x * MORPH_FLOATS + 5] = deltas[x * 6 + 4];
|
|
packed[x * MORPH_FLOATS + 6] = deltas[x * 6 + 5];
|
|
}
|
|
_freeMorphs(m);
|
|
m->morphBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, packed, (uint32_t)((size_t)count * MORPH_FLOATS * sizeof(float)));
|
|
SDL_free(packed);
|
|
if (m->morphBuffer == NULL) {
|
|
return false;
|
|
}
|
|
m->morphNames = SDL_calloc((size_t)targetCount, sizeof(char *));
|
|
if (m->morphNames == NULL) {
|
|
utilDie("Out of memory naming morph targets.");
|
|
}
|
|
for (x = 0; x < targetCount; x++) {
|
|
m->morphNames[x] = ((names != NULL) && (names[x] != NULL)) ? SDL_strdup(names[x]) : NULL;
|
|
}
|
|
m->morphCount = targetCount;
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) {
|
|
_matchMorphWeights(&_scene.nodes[x]);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Latitude/longitude sphere; segments around, half as many from pole to pole.
|
|
int32_t meshSphere(float radius, int32_t segments) {
|
|
SceneVertexT *vertices;
|
|
uint32_t *indices;
|
|
int32_t rings;
|
|
int32_t ring;
|
|
int32_t seg;
|
|
int32_t v = 0;
|
|
int32_t i = 0;
|
|
int32_t vertexCount;
|
|
int32_t indexCount;
|
|
int32_t mesh;
|
|
|
|
segments = SDL_max(segments, MIN_SEGMENTS);
|
|
rings = SDL_max(segments / 2, 2);
|
|
vertexCount = (rings + 1) * (segments + 1);
|
|
indexCount = rings * segments * 6;
|
|
vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT));
|
|
indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t));
|
|
if ((vertices == NULL) || (indices == NULL)) {
|
|
utilDie("Out of memory building a mesh.");
|
|
}
|
|
for (ring = 0; ring <= rings; ring++) {
|
|
float phi = (float)ring / (float)rings * PI;
|
|
|
|
for (seg = 0; seg <= segments; seg++) {
|
|
float theta = (float)seg / (float)segments * 2.0f * PI;
|
|
float nx = sinf(phi) * cosf(theta);
|
|
float ny = cosf(phi);
|
|
float nz = sinf(phi) * sinf(theta);
|
|
|
|
// u runs the other way so an image reads correctly from outside.
|
|
vertices[v++] = _vertex(nx * radius, ny * radius, nz * radius, nx, ny, nz, 1.0f - (float)seg / (float)segments, (float)ring / (float)rings);
|
|
}
|
|
}
|
|
for (ring = 0; ring < rings; ring++) {
|
|
for (seg = 0; seg < segments; seg++) {
|
|
uint32_t a = (uint32_t)(ring * (segments + 1) + seg);
|
|
uint32_t b = a + (uint32_t)segments + 1;
|
|
|
|
indices[i++] = a;
|
|
indices[i++] = a + 1;
|
|
indices[i++] = b;
|
|
indices[i++] = a + 1;
|
|
indices[i++] = b + 1;
|
|
indices[i++] = b;
|
|
}
|
|
}
|
|
mesh = _addMesh(vertices, vertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
SDL_free(indices);
|
|
return mesh;
|
|
}
|
|
|
|
|
|
// A ring around Y.
|
|
int32_t meshTorus(float radius, float tubeRadius, int32_t segments) {
|
|
SceneVertexT *vertices;
|
|
uint32_t *indices;
|
|
int32_t tubeSegments;
|
|
int32_t x;
|
|
int32_t y;
|
|
int32_t v = 0;
|
|
int32_t i = 0;
|
|
int32_t vertexCount;
|
|
int32_t indexCount;
|
|
int32_t mesh;
|
|
|
|
segments = SDL_max(segments, MIN_SEGMENTS);
|
|
tubeSegments = SDL_max(segments / 2, MIN_SEGMENTS);
|
|
vertexCount = (segments + 1) * (tubeSegments + 1);
|
|
indexCount = segments * tubeSegments * 6;
|
|
vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT));
|
|
indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t));
|
|
if ((vertices == NULL) || (indices == NULL)) {
|
|
utilDie("Out of memory building a mesh.");
|
|
}
|
|
for (x = 0; x <= segments; x++) {
|
|
float theta = (float)x / (float)segments * 2.0f * PI;
|
|
float cx = cosf(theta);
|
|
float sx = sinf(theta);
|
|
|
|
for (y = 0; y <= tubeSegments; y++) {
|
|
float phi = (float)y / (float)tubeSegments * 2.0f * PI;
|
|
float cy = cosf(phi);
|
|
float sy = sinf(phi);
|
|
|
|
vertices[v++] = _vertex(cx * (radius + cy * tubeRadius), sy * tubeRadius, sx * (radius + cy * tubeRadius), cx * cy, sy, sx * cy, (float)x / (float)segments, (float)y / (float)tubeSegments);
|
|
}
|
|
}
|
|
for (x = 0; x < segments; x++) {
|
|
for (y = 0; y < tubeSegments; y++) {
|
|
uint32_t a = (uint32_t)(x * (tubeSegments + 1) + y);
|
|
uint32_t b = a + (uint32_t)tubeSegments + 1;
|
|
|
|
indices[i++] = a;
|
|
indices[i++] = b;
|
|
indices[i++] = a + 1;
|
|
indices[i++] = a + 1;
|
|
indices[i++] = b;
|
|
indices[i++] = b + 1;
|
|
}
|
|
}
|
|
mesh = _addMesh(vertices, vertexCount, indices, indexCount, false);
|
|
SDL_free(vertices);
|
|
SDL_free(indices);
|
|
return mesh;
|
|
}
|
|
|
|
|
|
bool meshValid(int32_t mesh) {
|
|
return (mesh >= 0) && (mesh < _scene.meshCount) && _scene.meshes[mesh].used;
|
|
}
|
|
|
|
|
|
// ===== Nodes =====
|
|
|
|
// Frees the node and everything under it. The root cannot be deleted.
|
|
bool nodeDelete(int32_t node) {
|
|
int32_t child;
|
|
int32_t next;
|
|
|
|
if (!nodeValid(node) || (node == SCENE_ROOT_NODE)) {
|
|
return false;
|
|
}
|
|
for (child = _scene.nodes[node].firstChild; child != NO_HANDLE; child = next) {
|
|
next = _scene.nodes[child].nextSibling;
|
|
nodeDelete(child);
|
|
}
|
|
_detach(node);
|
|
if (_scene.cameraNode == node) {
|
|
_scene.cameraNode = NO_HANDLE;
|
|
}
|
|
SDL_free(_scene.nodes[node].name);
|
|
_freeSkin(&_scene.nodes[node]);
|
|
_freeMorphWeights(&_scene.nodes[node]);
|
|
_scene.nodes[node].name = NULL;
|
|
_scene.nodes[node].used = false;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Depth-first search below root (root itself included) for a node by name.
|
|
int32_t nodeFind(int32_t root, const char *name) {
|
|
int32_t child;
|
|
int32_t found;
|
|
|
|
if (!nodeValid(root) || (name == NULL)) {
|
|
return NO_HANDLE;
|
|
}
|
|
if ((_scene.nodes[root].name != NULL) && (strcmp(_scene.nodes[root].name, name) == 0)) {
|
|
return root;
|
|
}
|
|
for (child = _scene.nodes[root].firstChild; child != NO_HANDLE; child = _scene.nodes[child].nextSibling) {
|
|
found = nodeFind(child, name);
|
|
if (found != NO_HANDLE) {
|
|
return found;
|
|
}
|
|
}
|
|
return NO_HANDLE;
|
|
}
|
|
|
|
|
|
int32_t nodeGetChild(int32_t node, int32_t index) {
|
|
int32_t child;
|
|
|
|
if (!nodeValid(node) || (index < 0)) {
|
|
return NO_HANDLE;
|
|
}
|
|
child = _scene.nodes[node].firstChild;
|
|
while ((child != NO_HANDLE) && (index > 0)) {
|
|
child = _scene.nodes[child].nextSibling;
|
|
index--;
|
|
}
|
|
return child;
|
|
}
|
|
|
|
|
|
int32_t nodeGetChildCount(int32_t node) {
|
|
int32_t child;
|
|
int32_t count = 0;
|
|
|
|
if (!nodeValid(node)) {
|
|
return 0;
|
|
}
|
|
for (child = _scene.nodes[node].firstChild; child != NO_HANDLE; child = _scene.nodes[child].nextSibling) {
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
|
|
// Changes every time the slot is reused, so a handle kept across a delete can be detected.
|
|
uint32_t nodeGetGeneration(int32_t node) {
|
|
if ((node < 0) || (node >= _scene.nodeCount)) {
|
|
return 0;
|
|
}
|
|
return _scene.nodes[node].generation;
|
|
}
|
|
|
|
|
|
// The node's mesh handle, or -1.
|
|
int32_t nodeGetMaterial(int32_t node) {
|
|
return nodeValid(node) ? _scene.nodes[node].material : NO_HANDLE;
|
|
}
|
|
|
|
|
|
int32_t nodeGetMesh(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return NO_HANDLE;
|
|
}
|
|
return _scene.nodes[node].mesh;
|
|
}
|
|
|
|
|
|
int32_t nodeGetMorphCount(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return 0;
|
|
}
|
|
return _scene.nodes[node].morphCount;
|
|
}
|
|
|
|
|
|
float nodeGetMorphWeight(int32_t node, int32_t target) {
|
|
if (!nodeValid(node) || (target < 0) || (target >= _scene.nodes[node].morphCount)) {
|
|
return 0.0f;
|
|
}
|
|
return _scene.nodes[node].morphWeights[target];
|
|
}
|
|
|
|
|
|
const char *nodeGetName(int32_t node) {
|
|
if (!nodeValid(node) || (_scene.nodes[node].name == NULL)) {
|
|
return "";
|
|
}
|
|
return _scene.nodes[node].name;
|
|
}
|
|
|
|
|
|
int32_t nodeGetParent(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return NO_HANDLE;
|
|
}
|
|
return _scene.nodes[node].parent;
|
|
}
|
|
|
|
|
|
Vec3T nodeGetPosition(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
return _scene.nodes[node].translation;
|
|
}
|
|
|
|
|
|
QuatT nodeGetRotation(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return quatIdentity();
|
|
}
|
|
return _scene.nodes[node].rotation;
|
|
}
|
|
|
|
|
|
Vec3T nodeGetScale(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return vec3(1.0f, 1.0f, 1.0f);
|
|
}
|
|
return _scene.nodes[node].scale;
|
|
}
|
|
|
|
|
|
// From the last rendered frame's matrices (this frame's edits show after the next render).
|
|
Vec3T nodeGetWorldPosition(int32_t node) {
|
|
if (!nodeValid(node)) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
return mat4TransformPoint(_scene.nodes[node].world, vec3(0.0f, 0.0f, 0.0f));
|
|
}
|
|
|
|
|
|
// The node's world-space position, rotation and scale as of the last transform update.
|
|
bool nodeGetWorldTransform(int32_t node, Vec3T *position, QuatT *rotation, Vec3T *scale) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
mat4Decompose(_scene.nodes[node].world, position, rotation, scale);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Points the node's -Z at a world-space target (the camera and lights look down -Z). Only the
|
|
// node's own rotation changes, in its parent's space.
|
|
bool nodeLookAt(int32_t node, Vec3T target) {
|
|
NodeT *n;
|
|
Mat4T parentInverse;
|
|
Vec3T localTarget;
|
|
Vec3T forward;
|
|
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
n = &_scene.nodes[node];
|
|
if ((n->parent != NO_HANDLE) && mat4Invert(_scene.nodes[n->parent].world, &parentInverse)) {
|
|
localTarget = mat4TransformPoint(parentInverse, target);
|
|
} else {
|
|
localTarget = target;
|
|
}
|
|
forward = vec3Subtract(localTarget, n->translation);
|
|
if (vec3Length(forward) < 1e-6f) {
|
|
return true;
|
|
}
|
|
n->rotation = quatLookRotation(forward, vec3(0.0f, 1.0f, 0.0f));
|
|
return true;
|
|
}
|
|
|
|
|
|
// Moves along the node's own axes.
|
|
bool nodeMove(int32_t node, Vec3T delta) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].translation = vec3Add(_scene.nodes[node].translation, quatRotate(_scene.nodes[node].rotation, delta));
|
|
return true;
|
|
}
|
|
|
|
|
|
int32_t nodeNew(int32_t parent) {
|
|
int32_t node;
|
|
|
|
if (parent == NO_HANDLE) {
|
|
parent = SCENE_ROOT_NODE;
|
|
}
|
|
if (!nodeValid(parent)) {
|
|
return NO_HANDLE;
|
|
}
|
|
node = _allocNode();
|
|
_attach(node, parent);
|
|
return node;
|
|
}
|
|
|
|
|
|
// Rotates about the node's own axes.
|
|
bool nodeRotate(int32_t node, QuatT delta) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].rotation = quatNormalize(quatMultiply(_scene.nodes[node].rotation, delta));
|
|
return true;
|
|
}
|
|
|
|
|
|
// mesh NO_HANDLE clears; material NO_HANDLE means the default look.
|
|
// Changes the material and keeps the mesh.
|
|
bool nodeSetMaterial(int32_t node, int32_t material) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].material = material;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeSetMesh(int32_t node, int32_t mesh, int32_t material) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
if ((mesh != NO_HANDLE) && !meshValid(mesh)) {
|
|
return false;
|
|
}
|
|
if ((material != NO_HANDLE) && !materialValid(material)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].mesh = mesh;
|
|
_scene.nodes[node].material = material;
|
|
_matchMorphWeights(&_scene.nodes[node]);
|
|
return true;
|
|
}
|
|
|
|
|
|
// How much of a morph target the node's mesh shows (usually 0 to 1).
|
|
bool nodeSetMorphWeight(int32_t node, int32_t target, float weight) {
|
|
if (!nodeValid(node) || (target < 0) || (target >= _scene.nodes[node].morphCount)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].morphWeights[target] = weight;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeSetName(int32_t node, const char *name) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
SDL_free(_scene.nodes[node].name);
|
|
_scene.nodes[node].name = (name != NULL) ? SDL_strdup(name) : NULL;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Re-parents, keeping the node's local transform (so it moves with the new parent). A node
|
|
// cannot be put under itself or its own descendants.
|
|
bool nodeSetParent(int32_t node, int32_t parent) {
|
|
int32_t ancestor;
|
|
|
|
if (!nodeValid(node) || (node == SCENE_ROOT_NODE)) {
|
|
return false;
|
|
}
|
|
if (parent == NO_HANDLE) {
|
|
parent = SCENE_ROOT_NODE;
|
|
}
|
|
if (!nodeValid(parent)) {
|
|
return false;
|
|
}
|
|
for (ancestor = parent; ancestor != NO_HANDLE; ancestor = _scene.nodes[ancestor].parent) {
|
|
if (ancestor == node) {
|
|
return false;
|
|
}
|
|
}
|
|
_detach(node);
|
|
_attach(node, parent);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeSetPosition(int32_t node, Vec3T position) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].translation = position;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeSetRotation(int32_t node, QuatT rotation) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].rotation = quatNormalize(rotation);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeSetScale(int32_t node, Vec3T scale) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].scale = scale;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Drives the node's skinned mesh from other nodes: joints (up to 128) and their inverse bind
|
|
// matrices, copied. count 0 removes the skin and the mesh draws unskinned.
|
|
// The joint nodes of a skinned node's skin, or 0 with none.
|
|
int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints) {
|
|
if (!nodeValid(node) || (_scene.nodes[node].skinCount == 0)) {
|
|
return 0;
|
|
}
|
|
if (joints != NULL) {
|
|
*joints = _scene.nodes[node].skinJoints;
|
|
}
|
|
return _scene.nodes[node].skinCount;
|
|
}
|
|
|
|
|
|
bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count) {
|
|
NodeT *n;
|
|
|
|
if (!nodeValid(node) || (count < 0) || (count > MAX_JOINTS)) {
|
|
return false;
|
|
}
|
|
n = &_scene.nodes[node];
|
|
_freeSkin(n);
|
|
if (count == 0) {
|
|
return true;
|
|
}
|
|
n->skinJoints = SDL_malloc(sizeof(int32_t) * (size_t)count);
|
|
n->skinInverseBind = SDL_malloc(sizeof(Mat4T) * (size_t)count);
|
|
if ((n->skinJoints == NULL) || (n->skinInverseBind == NULL)) {
|
|
utilDie("Out of memory attaching a skin.");
|
|
}
|
|
memcpy(n->skinJoints, joints, sizeof(int32_t) * (size_t)count);
|
|
memcpy(n->skinInverseBind, inverseBind, sizeof(Mat4T) * (size_t)count);
|
|
n->skinCount = count;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Hides the node and everything under it.
|
|
// Whether the node's mesh is drawn into shadow maps; a bulb's own mesh or a glowing sign is not.
|
|
bool nodeSetShadow(int32_t node, bool casts) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].shadowCaster = casts;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeSetVisible(int32_t node, bool visible) {
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
_scene.nodes[node].visible = visible;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Places the node at a world-space position and rotation by converting through its parent's
|
|
// world transform (what a physics body needs to drive a node under any parent). Scale is kept.
|
|
bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation) {
|
|
NodeT *n;
|
|
Mat4T parentInverse;
|
|
Vec3T parentPosition;
|
|
QuatT parentRotation;
|
|
Vec3T parentScale;
|
|
QuatT parentInverseRotation;
|
|
|
|
if (!nodeValid(node)) {
|
|
return false;
|
|
}
|
|
n = &_scene.nodes[node];
|
|
if ((n->parent == NO_HANDLE) || (n->parent == SCENE_ROOT_NODE) || !mat4Invert(_scene.nodes[n->parent].world, &parentInverse)) {
|
|
n->translation = position;
|
|
n->rotation = quatNormalize(rotation);
|
|
return true;
|
|
}
|
|
mat4Decompose(_scene.nodes[n->parent].world, &parentPosition, &parentRotation, &parentScale);
|
|
parentInverseRotation = parentRotation;
|
|
parentInverseRotation.x = -parentInverseRotation.x;
|
|
parentInverseRotation.y = -parentInverseRotation.y;
|
|
parentInverseRotation.z = -parentInverseRotation.z;
|
|
n->translation = mat4TransformPoint(parentInverse, position);
|
|
n->rotation = quatNormalize(quatMultiply(parentInverseRotation, rotation));
|
|
return true;
|
|
}
|
|
|
|
|
|
bool nodeValid(int32_t node) {
|
|
return (node >= 0) && (node < _scene.nodeCount) && _scene.nodes[node].used;
|
|
}
|
|
|
|
|
|
// ===== Scene =====
|
|
|
|
bool sceneAvailable(void) {
|
|
return _scene.device != NULL;
|
|
}
|
|
|
|
|
|
// Turns the layer on or off. Fails only when the machine has no GPU device.
|
|
bool sceneEnable(bool enabled) {
|
|
if (enabled && (_scene.device == NULL)) {
|
|
return false;
|
|
}
|
|
_scene.enabled = enabled;
|
|
return true;
|
|
}
|
|
|
|
|
|
void sceneGetSize(int32_t *width, int32_t *height) {
|
|
*width = _scene.width;
|
|
*height = _scene.height;
|
|
}
|
|
|
|
|
|
// device may be NULL (no GPU backend on this machine); the scene then refuses to be enabled.
|
|
// Creates the root node, the shaders, the sampler and the white stand-in texture.
|
|
bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer) {
|
|
SDL_GPUSamplerCreateInfo samplerInfo;
|
|
SDL_Surface *pixel;
|
|
|
|
memset(&_scene, 0, sizeof(_scene));
|
|
_scene.device = device;
|
|
_scene.renderer = renderer;
|
|
_scene.cameraNode = NO_HANDLE;
|
|
_scene.perspective = true;
|
|
_scene.fov = DEFAULT_FOV;
|
|
_scene.near = DEFAULT_NEAR;
|
|
_scene.far = DEFAULT_FAR;
|
|
_scene.orthoHeight = DEFAULT_EYE_Z;
|
|
_scene.ambient = vec3(0.1f, 0.1f, 0.1f);
|
|
_scene.antialias = true;
|
|
_scene.sampleCount = SDL_GPU_SAMPLECOUNT_1;
|
|
_scene.shadowSize = SHADOW_SIZE;
|
|
// The node tree is plain data and exists on every machine (physics bodies live on nodes);
|
|
// everything from here on needs the GPU, and without one the layer cannot be enabled.
|
|
_allocNode();
|
|
nodeSetName(SCENE_ROOT_NODE, "root");
|
|
if (device == NULL) {
|
|
return false;
|
|
}
|
|
_scene.depthFormat = _depthFormat();
|
|
_scene.shadowFormat = _shadowFormat();
|
|
if (!_createShaders()) {
|
|
sceneQuit();
|
|
return false;
|
|
}
|
|
memset(&samplerInfo, 0, sizeof(samplerInfo));
|
|
samplerInfo.min_filter = SDL_GPU_FILTER_LINEAR;
|
|
samplerInfo.mag_filter = SDL_GPU_FILTER_LINEAR;
|
|
samplerInfo.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR;
|
|
samplerInfo.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
|
|
samplerInfo.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
|
|
samplerInfo.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
|
|
_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);
|
|
if (pixel != NULL) {
|
|
SDL_FillSurfaceRect(pixel, NULL, 0xFFFFFFFFu);
|
|
_scene.white = _uploadTexture(pixel);
|
|
SDL_DestroySurface(pixel);
|
|
}
|
|
{
|
|
float zero[MORPH_FLOATS] = { 0.0f };
|
|
|
|
_scene.noMorphs = _uploadBuffer(SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, zero, sizeof(zero));
|
|
}
|
|
_scene.shadowMapsNone = _createShadowArray(SDL_GPU_TEXTURETYPE_2D_ARRAY, 1, 1);
|
|
if ((_scene.sampler == NULL) || (_scene.shadowSampler == NULL) || (_scene.white == NULL) || (_scene.shadowMapsNone == NULL) || (_scene.noMorphs == NULL)) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
sceneQuit();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool sceneIsEnabled(void) {
|
|
return _scene.enabled;
|
|
}
|
|
|
|
|
|
// World point to overlay coordinates using the last rendered frame's camera. Returns false when
|
|
// the point is behind the camera (x and y are still filled in).
|
|
bool sceneProject(Vec3T world, float *x, float *y, float *depth) {
|
|
Vec3T clip;
|
|
float w = _scene.viewProjection.m[3] * world.x + _scene.viewProjection.m[7] * world.y + _scene.viewProjection.m[11] * world.z + _scene.viewProjection.m[15];
|
|
|
|
clip = mat4TransformPoint(_scene.viewProjection, world);
|
|
*x = (clip.x + 1.0f) / 2.0f * (float)_scene.width;
|
|
*y = (1.0f - clip.y) / 2.0f * (float)_scene.height;
|
|
*depth = clip.z;
|
|
return w > 0.0f;
|
|
}
|
|
|
|
|
|
void sceneQuit(void) {
|
|
int32_t x;
|
|
|
|
if (_scene.device != NULL) {
|
|
for (x = 0; x < _scene.materialCount; x++) {
|
|
_freeMaterialTexture(&_scene.materials[x]);
|
|
}
|
|
for (x = 0; x < _scene.feedCount; x++) {
|
|
if (_scene.feeds[x].used) {
|
|
_freeFeed(&_scene.feeds[x]);
|
|
}
|
|
}
|
|
for (x = 0; x < _scene.meshCount; x++) {
|
|
if (_scene.meshes[x].used) {
|
|
meshDelete(x);
|
|
}
|
|
}
|
|
_destroyPipelines();
|
|
for (x = 0; x < PARTICLE_PIPELINES; x++) {
|
|
if (_scene.particlePipelines[x] != NULL) {
|
|
SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.particlePipelines[x]);
|
|
}
|
|
}
|
|
if (_scene.particleVertex != NULL) {
|
|
SDL_ReleaseGPUShader(_scene.device, _scene.particleVertex);
|
|
}
|
|
if (_scene.particleFragment != NULL) {
|
|
SDL_ReleaseGPUShader(_scene.device, _scene.particleFragment);
|
|
}
|
|
if (_scene.particleBuffer != NULL) {
|
|
SDL_ReleaseGPUBuffer(_scene.device, _scene.particleBuffer);
|
|
}
|
|
if (_scene.particleTransfer != NULL) {
|
|
SDL_ReleaseGPUTransferBuffer(_scene.device, _scene.particleTransfer);
|
|
}
|
|
_releaseParticleTextures(true);
|
|
if (_scene.vertexStatic != NULL) {
|
|
SDL_ReleaseGPUShader(_scene.device, _scene.vertexStatic);
|
|
}
|
|
if (_scene.vertexSkinned != NULL) {
|
|
SDL_ReleaseGPUShader(_scene.device, _scene.vertexSkinned);
|
|
}
|
|
if (_scene.fragment != NULL) {
|
|
SDL_ReleaseGPUShader(_scene.device, _scene.fragment);
|
|
}
|
|
if (_scene.depthFragment != NULL) {
|
|
SDL_ReleaseGPUShader(_scene.device, _scene.depthFragment);
|
|
}
|
|
if (_scene.sampler != NULL) {
|
|
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) {
|
|
SDL_ReleaseGPUTexture(_scene.device, _scene.white);
|
|
}
|
|
if (_scene.noMorphs != NULL) {
|
|
SDL_ReleaseGPUBuffer(_scene.device, _scene.noMorphs);
|
|
}
|
|
_destroyTargets();
|
|
}
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
SDL_free(_scene.nodes[x].name);
|
|
_freeSkin(&_scene.nodes[x]);
|
|
_freeMorphWeights(&_scene.nodes[x]);
|
|
}
|
|
SDL_free(_scene.nodes);
|
|
SDL_free(_scene.meshes);
|
|
SDL_free(_scene.materials);
|
|
SDL_free(_scene.feeds);
|
|
SDL_free(_scene.draws);
|
|
SDL_free(_scene.particleVertices);
|
|
memset(&_scene, 0, sizeof(_scene));
|
|
}
|
|
|
|
|
|
// Draws the frame into the colour target and returns it as a texture for compositing, or NULL when
|
|
// the layer is off or has no target yet. The command buffer is submitted before the 2D renderer
|
|
// flushes its own, so the composite always samples this frame's result.
|
|
SDL_Texture *sceneRender(void) {
|
|
SDL_GPUColorTargetInfo colour;
|
|
SDL_GPUDepthStencilTargetInfo depth;
|
|
SDL_GPUCommandBuffer *commands;
|
|
SDL_GPURenderPass *pass;
|
|
FragmentUniformsT fragmentUniforms;
|
|
Mat4T identity = mat4Identity();
|
|
Mat4T view;
|
|
Mat4T inverseView;
|
|
Vec3T eye;
|
|
Vec3T cameraRight = vec3(1.0f, 0.0f, 0.0f);
|
|
Vec3T cameraUp = vec3(0.0f, 1.0f, 0.0f);
|
|
Vec3T cameraForward = vec3(0.0f, 0.0f, -1.0f);
|
|
bool *skip = NULL;
|
|
int32_t x;
|
|
int32_t drawCount = 0;
|
|
int32_t opaqueCount = 0;
|
|
int32_t slot;
|
|
int32_t face;
|
|
int32_t layers = 0;
|
|
NodeT *node;
|
|
|
|
if (!_scene.enabled || (_scene.colour == NULL)) {
|
|
return NULL;
|
|
}
|
|
// Transforms, camera and lights for this frame.
|
|
_updateWorld(SCENE_ROOT_NODE, &identity, true);
|
|
view = _view();
|
|
eye = _viewEye();
|
|
_scene.viewProjection = mat4Multiply(_projection(), view);
|
|
memset(&fragmentUniforms, 0, sizeof(fragmentUniforms));
|
|
fragmentUniforms.cameraPosition[0] = eye.x;
|
|
fragmentUniforms.cameraPosition[1] = eye.y;
|
|
fragmentUniforms.cameraPosition[2] = eye.z;
|
|
fragmentUniforms.cameraPosition[3] = 1.0f;
|
|
fragmentUniforms.ambient[0] = _scene.ambient.x;
|
|
fragmentUniforms.ambient[1] = _scene.ambient.y;
|
|
fragmentUniforms.ambient[2] = _scene.ambient.z;
|
|
fragmentUniforms.ambient[3] = 1.0f;
|
|
_scene.shadowCount = 0;
|
|
_fillLights(&fragmentUniforms);
|
|
// The camera's axes for particle billboards, and the frame's particles.
|
|
if (mat4Invert(view, &inverseView)) {
|
|
cameraRight = vec3Normalize(mat4TransformVector(inverseView, vec3(1.0f, 0.0f, 0.0f)));
|
|
cameraUp = vec3Normalize(mat4TransformVector(inverseView, vec3(0.0f, 1.0f, 0.0f)));
|
|
cameraForward = vec3Normalize(mat4TransformVector(inverseView, vec3(0.0f, 0.0f, -1.0f)));
|
|
}
|
|
_gatherParticles(eye, cameraForward);
|
|
// Collect what to draw: opaque first in node order, then blended back to front.
|
|
if (_scene.drawCapacity < _scene.nodeCount) {
|
|
_scene.draws = SDL_realloc(_scene.draws, sizeof(DrawT) * (size_t)_scene.nodeCount);
|
|
if (_scene.draws == NULL) {
|
|
utilDie("Out of memory collecting scene draws.");
|
|
}
|
|
_scene.drawCapacity = _scene.nodeCount;
|
|
}
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
node = &_scene.nodes[x];
|
|
if (!node->used || !node->worldVisible || (node->mesh == NO_HANDLE) || !meshValid(node->mesh)) {
|
|
continue;
|
|
}
|
|
if ((node->material == NO_HANDLE) || !_scene.materials[node->material].blend) {
|
|
_scene.draws[opaqueCount].node = x;
|
|
_scene.draws[opaqueCount].depth = 0.0f;
|
|
opaqueCount++;
|
|
}
|
|
}
|
|
drawCount = opaqueCount;
|
|
for (x = 0; x < _scene.nodeCount; x++) {
|
|
node = &_scene.nodes[x];
|
|
if (!node->used || !node->worldVisible || (node->mesh == NO_HANDLE) || !meshValid(node->mesh)) {
|
|
continue;
|
|
}
|
|
if ((node->material != NO_HANDLE) && _scene.materials[node->material].blend) {
|
|
_scene.draws[drawCount].node = x;
|
|
_scene.draws[drawCount].depth = vec3Length(vec3Subtract(mat4TransformPoint(node->world, vec3(0.0f, 0.0f, 0.0f)), eye));
|
|
drawCount++;
|
|
}
|
|
}
|
|
if (drawCount > opaqueCount) {
|
|
qsort(&_scene.draws[opaqueCount], (size_t)(drawCount - opaqueCount), sizeof(DrawT), _compareDraws);
|
|
}
|
|
commands = SDL_AcquireGPUCommandBuffer(_scene.device);
|
|
if (commands == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
return NULL;
|
|
}
|
|
_uploadParticles(commands);
|
|
// 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)) {
|
|
skip = SDL_calloc((size_t)drawCount, sizeof(bool));
|
|
if (skip == NULL) {
|
|
utilDie("Out of memory culling shadow casters.");
|
|
}
|
|
_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) {
|
|
// Faces rendered last frame from the same light and casters are still good.
|
|
ShadowCacheT *cache = &_scene.shadowCache[slot];
|
|
uint64_t hash = _cubeHash(shadow, drawCount);
|
|
|
|
if ((hash != 0) && (cache->node == shadow->node) && (cache->layer == shadow->layer) && (cache->mapsVersion == _scene.shadowMapsVersion) && (cache->hash == hash)) {
|
|
continue;
|
|
}
|
|
cache->node = shadow->node;
|
|
cache->layer = shadow->layer;
|
|
cache->mapsVersion = _scene.shadowMapsVersion;
|
|
cache->hash = hash;
|
|
for (face = 0; face < CUBE_FACES; face++) {
|
|
_cullFace(shadow, face, drawCount, skip);
|
|
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, true, skip, 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, false, NULL, 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));
|
|
colour.clear_color = _scene.background;
|
|
colour.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
if (_scene.multisampled != NULL) {
|
|
colour.texture = _scene.multisampled;
|
|
colour.resolve_texture = _scene.colour;
|
|
colour.store_op = SDL_GPU_STOREOP_RESOLVE;
|
|
} else {
|
|
colour.texture = _scene.colour;
|
|
colour.store_op = SDL_GPU_STOREOP_STORE;
|
|
}
|
|
memset(&depth, 0, sizeof(depth));
|
|
depth.texture = _scene.depth;
|
|
depth.clear_depth = 1.0f;
|
|
depth.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
depth.store_op = SDL_GPU_STOREOP_DONT_CARE;
|
|
depth.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
|
|
depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
|
|
pass = SDL_BeginGPURenderPass(commands, &colour, 1, &depth);
|
|
_drawList(commands, pass, drawCount, &_scene.viewProjection, false, false, NULL, &fragmentUniforms);
|
|
_drawParticles(commands, pass, &_scene.viewProjection, cameraRight, cameraUp);
|
|
SDL_free(skip);
|
|
SDL_EndGPURenderPass(pass);
|
|
SDL_SubmitGPUCommandBuffer(commands);
|
|
return _scene.composite;
|
|
}
|
|
|
|
|
|
// (Re)creates the render targets at the overlay's size.
|
|
bool sceneResize(int32_t width, int32_t height) {
|
|
SDL_GPUTextureCreateInfo info;
|
|
SDL_PropertiesID props;
|
|
|
|
if (_scene.device == NULL) {
|
|
return false;
|
|
}
|
|
if ((width == _scene.width) && (height == _scene.height)) {
|
|
return true;
|
|
}
|
|
_destroyTargets();
|
|
memset(&info, 0, sizeof(info));
|
|
info.type = SDL_GPU_TEXTURETYPE_2D;
|
|
info.format = SDL_GetGPUTextureFormatFromPixelFormat(SDL_PIXELFORMAT_BGRA32);
|
|
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
|
info.width = (Uint32)width;
|
|
info.height = (Uint32)height;
|
|
info.layer_count_or_depth = 1;
|
|
info.num_levels = 1;
|
|
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
|
_scene.colour = SDL_CreateGPUTexture(_scene.device, &info);
|
|
if (_scene.colour == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
return false;
|
|
}
|
|
// 4x multisampling when wanted and offered: a multisampled colour target resolved into colour,
|
|
// and a multisampled depth target to match.
|
|
_scene.sampleCount = SDL_GPU_SAMPLECOUNT_1;
|
|
if (_scene.antialias && SDL_GPUTextureSupportsSampleCount(_scene.device, info.format, SDL_GPU_SAMPLECOUNT_4) && SDL_GPUTextureSupportsSampleCount(_scene.device, _scene.depthFormat, SDL_GPU_SAMPLECOUNT_4)) {
|
|
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
|
info.sample_count = SDL_GPU_SAMPLECOUNT_4;
|
|
_scene.multisampled = SDL_CreateGPUTexture(_scene.device, &info);
|
|
if (_scene.multisampled != NULL) {
|
|
_scene.sampleCount = SDL_GPU_SAMPLECOUNT_4;
|
|
} else {
|
|
utilTrace("Scene: no multisampling: %s", SDL_GetError());
|
|
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
|
}
|
|
}
|
|
_destroyPipelines();
|
|
info.format = _scene.depthFormat;
|
|
info.usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET;
|
|
_scene.depth = SDL_CreateGPUTexture(_scene.device, &info);
|
|
if (_scene.depth == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
_destroyTargets();
|
|
return false;
|
|
}
|
|
props = SDL_CreateProperties();
|
|
SDL_SetPointerProperty(props, SDL_PROP_TEXTURE_CREATE_GPU_TEXTURE_POINTER, _scene.colour);
|
|
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, SDL_PIXELFORMAT_BGRA32);
|
|
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STATIC);
|
|
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, width);
|
|
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, height);
|
|
_scene.composite = SDL_CreateTextureWithProperties(_scene.renderer, props);
|
|
SDL_DestroyProperties(props);
|
|
if (_scene.composite == NULL) {
|
|
utilTrace("Scene: %s", SDL_GetError());
|
|
_destroyTargets();
|
|
return false;
|
|
}
|
|
SDL_SetTextureBlendMode(_scene.composite, SDL_BLENDMODE_BLEND);
|
|
_scene.width = width;
|
|
_scene.height = height;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Smooth normals from the triangles: face normals accumulated per vertex, then normalised.
|
|
void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) {
|
|
int32_t x;
|
|
|
|
for (x = 0; x < vertexCount; x++) {
|
|
vertices[x].normal[0] = 0.0f;
|
|
vertices[x].normal[1] = 0.0f;
|
|
vertices[x].normal[2] = 0.0f;
|
|
}
|
|
for (x = 0; x + 2 < indexCount; x += 3) {
|
|
SceneVertexT *a = &vertices[indices[x]];
|
|
SceneVertexT *b = &vertices[indices[x + 1]];
|
|
SceneVertexT *c = &vertices[indices[x + 2]];
|
|
SceneVertexT *corners[3] = { a, b, c };
|
|
Vec3T pa = vec3(a->position[0], a->position[1], a->position[2]);
|
|
Vec3T pb = vec3(b->position[0], b->position[1], b->position[2]);
|
|
Vec3T pc = vec3(c->position[0], c->position[1], c->position[2]);
|
|
Vec3T n = vec3Cross(vec3Subtract(pb, pa), vec3Subtract(pc, pa));
|
|
int32_t k;
|
|
|
|
for (k = 0; k < 3; k++) {
|
|
corners[k]->normal[0] += n.x;
|
|
corners[k]->normal[1] += n.y;
|
|
corners[k]->normal[2] += n.z;
|
|
}
|
|
}
|
|
for (x = 0; x < vertexCount; x++) {
|
|
Vec3T n = vec3Normalize(vec3(vertices[x].normal[0], vertices[x].normal[1], vertices[x].normal[2]));
|
|
|
|
vertices[x].normal[0] = n.x;
|
|
vertices[x].normal[1] = n.y;
|
|
vertices[x].normal[2] = n.z;
|
|
}
|
|
}
|
|
|
|
|
|
void sceneSetAmbient(uint8_t r, uint8_t g, uint8_t b) {
|
|
_scene.ambient = vec3(r / COLOUR_MAX, g / COLOUR_MAX, b / COLOUR_MAX);
|
|
}
|
|
|
|
|
|
// 4x multisampling on or off (on by default where the device offers it); takes effect on the
|
|
// next resize, so the targets are rebuilt here.
|
|
void sceneSetAntialias(bool antialias) {
|
|
int32_t width = _scene.width;
|
|
int32_t height = _scene.height;
|
|
|
|
_scene.antialias = antialias;
|
|
if (width > 0) {
|
|
_destroyTargets();
|
|
sceneResize(width, height);
|
|
}
|
|
}
|
|
|
|
|
|
void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
|
|
_scene.background.r = r / COLOUR_MAX;
|
|
_scene.background.g = g / COLOUR_MAX;
|
|
_scene.background.b = b / COLOUR_MAX;
|
|
_scene.background.a = a / COLOUR_MAX;
|
|
}
|
|
|
|
|
|
// 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
|
|
// distance from the camera's near plane, using the last rendered frame's camera. Two distances
|
|
// give a ray for picking.
|
|
Vec3T sceneUnproject(float x, float y, float distance) {
|
|
Mat4T inverse;
|
|
Vec3T ndc;
|
|
Vec3T near;
|
|
Vec3T far;
|
|
|
|
if ((_scene.width <= 0) || !mat4Invert(_scene.viewProjection, &inverse)) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
ndc = vec3(x / (float)_scene.width * 2.0f - 1.0f, 1.0f - y / (float)_scene.height * 2.0f, 0.0f);
|
|
near = mat4TransformPoint(inverse, ndc);
|
|
ndc.z = 1.0f;
|
|
far = mat4TransformPoint(inverse, ndc);
|
|
return vec3Add(near, vec3Scale(vec3Normalize(vec3Subtract(far, near)), distance));
|
|
}
|
|
|
|
|
|
// Rebuilds every node's world matrix now (sceneRender does it too); physics needs them before the
|
|
// step, after animation has moved the nodes.
|
|
void sceneUpdateTransforms(void) {
|
|
Mat4T identity = mat4Identity();
|
|
|
|
if (_scene.nodeCount > 0) {
|
|
_updateWorld(SCENE_ROOT_NODE, &identity, true);
|
|
}
|
|
}
|
|
|
|
|
|
// Once per frame before sceneRender: copies every player a material shows into that feed's RGBA
|
|
// target with the 2D renderer (which converts YUV on the way), then flushes the renderer so the
|
|
// copies are queued ahead of the scene's own command buffer.
|
|
void sceneUpdateVideo(SceneVideoSourceFn source) {
|
|
int32_t x;
|
|
bool any = false;
|
|
SDL_Texture *frame;
|
|
float width;
|
|
float height;
|
|
|
|
for (x = 0; x < _scene.feedCount; x++) {
|
|
FeedT *feed = &_scene.feeds[x];
|
|
|
|
if (!feed->used) {
|
|
continue;
|
|
}
|
|
frame = source(feed->player);
|
|
if (frame == NULL) {
|
|
continue;
|
|
}
|
|
if (feed->target == NULL) {
|
|
SDL_GetTextureSize(frame, &width, &height);
|
|
feed->target = SDL_CreateTexture(_scene.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_TARGET, (int32_t)width, (int32_t)height);
|
|
if (feed->target == NULL) {
|
|
utilTrace("Scene: video feed: %s", SDL_GetError());
|
|
continue;
|
|
}
|
|
feed->gpu = SDL_GetPointerProperty(SDL_GetTextureProperties(feed->target), SDL_PROP_TEXTURE_GPU_TEXTURE_POINTER, NULL);
|
|
}
|
|
SDL_SetRenderTarget(_scene.renderer, feed->target);
|
|
SDL_RenderTexture(_scene.renderer, frame, NULL, NULL);
|
|
any = true;
|
|
}
|
|
if (any) {
|
|
SDL_SetRenderTarget(_scene.renderer, NULL);
|
|
SDL_FlushRenderer(_scene.renderer);
|
|
}
|
|
}
|