This commit is contained in:
2024-12-24 09:48:52 -06:00
parent f3f0777de6
commit a0b610d93f
13 changed files with 1137 additions and 725 deletions

View File

@@ -61,9 +61,9 @@ var base_pipeline = {
primitive: "triangle", // point, line, linestrip, triangle, trianglestrip primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
fill: true, // false for lines fill: true, // false for lines
depth: { depth: {
compare: "always", // never/less/equal/less_equal/greater/not_equal/greater_equal/always compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
test: true, test: true,
write: false, write: true,
bias: 0, bias: 0,
bias_slope_scale: 0, bias_slope_scale: 0,
bias_clamp: 0 bias_clamp: 0
@@ -106,13 +106,68 @@ var base_pipeline = {
label: "scripted pipeline" label: "scripted pipeline"
} }
var pipeline_3d = Object.create(base_pipeline); /* rendering modes
ps1
gouraud
diffuse // 16 bit color, 5-5-5
7 dynamic lights, 1 ambient
textures are affine
no vertex skinning
256x256 texture max (generally 128x128)
320x240, variable up to 640x480
function use_pipeline(pipeline) { n64
if (!pipeline.gpu) gouraud
pipeline.gpu = gpu.make_pipeline(pipeline); diffuse
render._main.set_pipeline(pipeline); combiner // a secondary texture sometimes used to combine
} 7 dynamic lights, 1 ambient
320x240, or 640x480
sega saturn
gouraud
diffuse
320x240 or 640x480
ps2
phong
diffuse
combiner // second texture for modulation of diffuse
combine_mode // int for how to combine
dreamcast
phong
diffuse
combiner // second texture; could be an environment map, or emboss bump mapping
fog
640x480
640x448, special mode to 1280x1024
gamecube
phong
diffuse
+7 textures // wow!
8 dynamic lights
640x480
*/
/* meshes
position (float3)
color (rgba)
uv
*/
/* materials, modern pbr
any object can act as a "material". The engine expects some standardized things:
diffuse - base color texture
bump - a normal map for dot3 bump maping used in phong shading
height - a grayscale heightmap
occlusion - ambient occlusion texture
emission - texture for where model emits light
bump2 - a second normal map for detail
metallic - a metal/smoothness map
specular - specular map, alternative for the metallic workflow
*/
render.poly_prim = function poly_prim(verts) { render.poly_prim = function poly_prim(verts) {
var index = []; var index = [];
@@ -155,100 +210,92 @@ render.hotreload = function shader_hotreload() {
} }
}; };
function make_shader(shader, ...args) { function make_pipeline(pipeline) {
var file = `shaders/spirv/${shader}.spv`; // 1) Reflection data for vertex shader
if (shader_cache[file]) return shader_cache[file]; var refl = pipeline.vertex.reflection
shader = io.slurpbytes(file); if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
shader = render._main.make_shader(shader, ...args); // If there's no reflection data, just pass pipeline along
shader_cache[file] = shader; // or throw an error if reflection is mandatory
return shader; render._main.make_pipeline(pipeline)
} return
}
function shader_apply_material(shader, material = {}, old = {}) { var inputs = refl.inputs
render.setpipeline(cur.pipeline); var buffer_descriptions = []
for (var p in shader.vs.unimap) { var attributes = []
if (!(p in material)) continue;
if (material[p] === old[p]) continue;
assert(p in material, `shader ${shader.name} has no uniform for ${p}`);
var s = shader.vs.unimap[p];
if (p === "bones") { // 2) For each input in the reflection, build one buffer + attribute
render.setunibones(0, s.slot, material[p]); // (Simplest approach: each input is stored in its own slot, offset=0)
continue; for (var i = 0; i < inputs.length; i++) {
var inp = inputs[i]
var typeStr = inp.type
var nameStr = (inp.name || "").toUpperCase()
var pitch = 4 // fallback if unknown
var fmt = "float1"
// Decide pitch & format based on 'type'
if (typeStr == "vec2") {
pitch = 8
fmt = "float2"
} else if (typeStr == "vec3") {
pitch = 12
fmt = "float3"
} else if (typeStr == "vec4") {
// Special case: if "COLOR" is in the name, treat it as packed bytes
if (nameStr.indexOf("COLOR") >= 0) {
pitch = 4
fmt = "color" // signals engine to use SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4NORM
} else {
pitch = 16
fmt = "float4"
}
} }
shader_unisize[s.size](0, s.slot, material[p]); // Create a buffer description for this input
buffer_descriptions.push({
slot: i,
pitch: pitch,
input_rate: "vertex",
instance_step_rate: 0,
name:inp.name.split(".").pop()
})
// Create a matching vertex attribute
attributes.push({
location: inp.location,
buffer_slot: i,
format: fmt,
offset: 0
})
} }
for (var p in shader.fs.unimap) { // 3) Attach these arrays onto the pipeline object
if (!(p in material)) continue; pipeline.vertex_buffer_descriptions = buffer_descriptions
if (material[p] === old[p]) continue; pipeline.vertex_attributes = attributes
assert(p in material, `shader ${shader.name} has no uniform for ${p}`);
var s = shader.fs.unimap[p];
shader_unisize[s.size](1, s.slot, material[p]);
}
if (!material.diffuse) return; // 4) Hand off the pipeline to native code
if (material.diffuse === old.diffuse) return; console.log(`depth: ${json.encode(pipeline.depth)}`);
return render._main.make_pipeline(pipeline)
if ("diffuse_texel" in shader.fs.unimap) render.setuniv2(1, shader.fs.unimap.diffuse_texel.slot, [1,1].div([material.diffuse.width, material.diffuse.height]));
if ("diffuse_size" in shader.fs.unimap) render.setuniv2(1, shader.fs.unimap.diffuse_size.slot, [material.diffuse.width, material.diffuse.height]);
if ("diffuse_size" in shader.vs.unimap) render.setuniv2(0, shader.vs.unimap.diffuse_size.slot, [material.diffuse.width, material.diffuse.height]);
} }
// Creates a binding object for a given mesh and shader function make_shader(sh_file) {
var bcache = new WeakMap(); var file = `shaders/spirv/${sh_file}.spv`
function sg_bind(mesh, ssbo) { var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
if (cur.bind && cur.mesh === mesh && cur.ssbo === ssbo) { if (shader_cache[file]) return shader_cache[file]
if (ssbo)
cur.bind.ssbo = [ssbo]; var shader = {
else code: io.slurpbytes(file),
cur.bind.ssbo = undefined; stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
cur.bind.images = cur.images; num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
cur.bind.samplers = cur.samplers; num_textures: 0,
render.setbind(cur.bind); num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
return; num_uniform_buffers: refl.ubos ? refl.ubos.length : 0
} }
/* if (bcache.has(cur.shader) && bcache.get(cur.shader).has(mesh)) { shader.gpu = render._main.make_shader(shader)
cur.bind = bcache.get(cur.shader).get(mesh); shader.reflection = refl;
cur.bind.images = cur.images; shader_cache[file] = shader
cur.bind.samplers = cur.samplers; return shader
if (ssbo)
cur.bind.ssbo = [ssbo];
render.setbind(cur.bind);
return;
}*/
var bind = {};/* if (!bcache.has(cur.shader)) bcache.set(cur.shader, new WeakMap());
if (!bcache.get(cur.shader).has(mesh)) bcache.get(cur.shader).set(mesh, bind);*/
cur.mesh = mesh;
cur.ssbo = ssbo;
cur.bind = bind;
bind.attrib = [];
if (cur.shader.vs.inputs)
for (var a of cur.shader.vs.inputs) {
if (!(a.name in mesh)) {
console.error(`cannot draw shader ${cur.shader.name}; there is no attrib ${a.name} in the given mesh. ${json.encode(mesh)}`);
return undefined;
} else bind.attrib.push(mesh[a.name]);
}
if (cur.shader.indexed) {
bind.index = mesh.index;
bind.count = mesh.count;
} else bind.count = mesh.verts;
bind.ssbo = [];
if (cur.shader.vs.storage_buffers) for (var b of cur.shader.vs.storage_buffers) bind.ssbo.push(ssbo);
bind.images = cur.images;
bind.samplers = cur.samplers;
render.setbind(cur.bind);
return bind;
} }
render.device = { render.device = {
@@ -290,7 +337,9 @@ var sprite_stack = [];
render.device.doc = `Device resolutions given as [x,y,inches diagonal].`; render.device.doc = `Device resolutions given as [x,y,inches diagonal].`;
var std_sampler; var std_sampler;
function upload_model(model) var tbuffer;
var spritemesh;
function upload_model(cmds, model)
{ {
var bufs = []; var bufs = [];
for (var i in model) { for (var i in model) {
@@ -298,26 +347,58 @@ function upload_model(model)
if (i === 'indices') model[i].index = true; if (i === 'indices') model[i].index = true;
bufs.push(model[i]); bufs.push(model[i]);
} }
render._main.upload(bufs); tbuffer = render._main.upload(cmds, bufs, tbuffer);
} }
var campos = [0,0,500]; var campos = [0,0,500];
function bind_model(pass, model, pipeline)
{
var buffers = pipeline.vertex_buffer_descriptions;
var bufs = [];
for (var b of buffers) {
if (b.name in model)
bufs.push(model[b.name])
else
throw Error (`could not find buffer ${b.name} on model`);
}
pass.bind_buffers(0,bufs);
pass.bind_index_buffer(model.indices);
}
function bind_mat(pass, mat, pipeline)
{
var imgs = [];
var refl = pipeline.fragment.reflection;
if (refl.separate_images) {
for (var i of pipeline.fragment.reflection.separate_images) {
if (i.name in mat) {
var tex = mat[i.name];
imgs.push({texture:tex.texture, sampler:tex.sampler});
} else
throw Error (`could not find all necessary images: ${i.name}`)
}
pass.bind_samplers(false, 0,imgs);
}
}
function gpupresent() function gpupresent()
{ {
var cmds = render._main.acquire_cmd_buffer();
var myimg = game.texture("pockle"); var myimg = game.texture("pockle");
myimg.sampler = std_sampler; myimg.sampler = std_sampler;
var spritemesh = render._main.make_sprite_mesh(sprite_stack); spritemesh = render._main.make_sprite_mesh(sprite_stack, spritemesh);
upload_model(spritemesh); upload_model(cmds, spritemesh);
cmds.submit();
sprite_stack.length = 0; sprite_stack.length = 0;
var cmds = render._main.acquire_cmd_buffer(); cmds = render._main.acquire_cmd_buffer();
var pass = cmds.render_pass(); var pass = cmds.render_pass();
try{ try{
pass.bind_pipeline(base_pipeline.gpu); pass.bind_pipeline(base_pipeline.gpu);
pass.bind_model(spritemesh); bind_model(pass,spritemesh,base_pipeline);
pass.bind_mat({diffuse:myimg}); bind_mat(pass,{diffuse:myimg}, base_pipeline);
cmds.camera(prosperon.camera.transform); cmds.camera(prosperon.camera.transform);
pass.draw(spritemesh.count,1,0,0,0); pass.draw(spritemesh.count,1,0,0,0);
prosperon.camera.transform.pos = campos; prosperon.camera.transform.pos = campos;
@@ -328,12 +409,14 @@ try{
prosperon.camera.far = 100000; prosperon.camera.far = 100000;
cmds.camera_perspective(prosperon.camera); cmds.camera_perspective(prosperon.camera);
pass.bind_pipeline(pipeline_model.gpu); pass.bind_pipeline(pipeline_model.gpu);
pass.bind_model(ducky); bind_model(pass,ducky.mesh,pipeline_model);
pass.draw(ducky.count,1,0,0,0); bind_mat(pass,ducky.material,pipeline_model);
cmds.camera(prosperon.camera.transform, true); pass.draw(ducky.mesh.count,1,0,0,0);
pass.bind_pipeline(base_pipeline.gpu);
pass.draw(spritemesh.count,1,0,0,0); // cmds.camera(prosperon.camera.transform, true);
} catch(e) { console.log(e); } finally { // pass.bind_pipeline(base_pipeline.gpu);
// pass.draw(spritemesh.count,1,0,0,0);
} catch(e) { console.error(e); } finally {
pass.end(); pass.end();
cmds.submit(); cmds.submit();
} }
@@ -344,24 +427,6 @@ var ducky;
var pipeline_model; var pipeline_model;
render.init = function () { render.init = function () {
io.mount("core");
render._main.present = gpupresent;
ducky = os.model_buffer("Duck.glb");
upload_model(ducky);
for (var i in ducky) console.log(`ducky has ${i}`)
console.log(ducky.count)
var sprite_vert = make_shader("sprite.vert", true,0,0,0,1);
var sprite_frag = make_shader("sprite.frag", false,1,0,0,0);
base_pipeline.vertex = sprite_vert;
base_pipeline.fragment = sprite_frag;
base_pipeline.gpu = render._main.make_pipeline(base_pipeline);
var model_vert = make_shader("model.vert", true, 0,0,0,1);
var model_frag = make_shader("model.frag", false, 0,0,0,0);
pipeline_model = Object.create(base_pipeline);
pipeline_model.vertex = model_vert;
pipeline_model.fragment = model_frag;
pipeline_model.gpu = render._main.make_pipeline(pipeline_model);
std_sampler = render._main.make_sampler({ std_sampler = render._main.make_sampler({
min_filter: "nearest", min_filter: "nearest",
mag_filter: "nearest", mag_filter: "nearest",
@@ -371,6 +436,39 @@ render.init = function () {
address_mode_w: "clamp_edge" address_mode_w: "clamp_edge"
}); });
io.mount("core");
render._main.present = gpupresent;
ducky = os.model_buffer("Duck.glb");
for (var mesh of ducky) {
var mat = mesh.material;
for (var i in mat) {
var img = {};
img.surface = os.make_texture(mat[i]);
img.texture = render._main.load_texture(img.surface);
img.texture.mode(0);
img.sampler = std_sampler;
mat[i] = img;
}
}
ducky = ducky[0];
var cmds = render._main.acquire_cmd_buffer();
upload_model(cmds,ducky.mesh);
cmds.submit();
var sprite_vert = make_shader("sprite.vert");
var sprite_frag = make_shader("sprite.frag");
base_pipeline.vertex = sprite_vert;
base_pipeline.fragment = sprite_frag;
base_pipeline.gpu = make_pipeline(base_pipeline);
var model_vert = make_shader("model.vert");
var model_frag = make_shader("model.frag");
pipeline_model = Object.create(base_pipeline);
pipeline_model.vertex = model_vert;
pipeline_model.fragment = model_frag;
pipeline_model.gpu = make_pipeline(pipeline_model);
/* os.make_circle2d().draw = function () { /* os.make_circle2d().draw = function () {
render.circle(this.body().transform().pos, this.radius, [1, 1, 0, 1]); render.circle(this.body().transform().pos, this.radius, [1, 1, 0, 1]);
}; };

View File

@@ -1,15 +0,0 @@
// Constant Buffer for Transformation Matrices
cbuffer TransformBuffer : register(b0, space1)
{
float4x4 vp;
float4x4 view;
float time; // time in seconds since app start
}
struct VSInput
{
float3 pos : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
float3 normal : NORMAL;
};

View File

@@ -0,0 +1,16 @@
// Constant Buffer for Transformation Matrices
cbuffer TransformBuffer : register(b0, space1)
{
float4x4 world_to_projection;
float4x4 projection_to_world;
float4x4 world_to_view;
float4x4 view_to_projection;
float3 camera_pos_world;
float viewport_min_z;
float3 camera_dir_world;
float viewport_max_z;
float2 viewport_size;
float2 viewport_offset;
float2 render_size;
float time; // time in seconds since app start
}

View File

@@ -0,0 +1,10 @@
struct PSInput
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
float3 normal : NORMAL;
float3 gouraud : COLOR1;
};

View File

@@ -0,0 +1,42 @@
#include "common.hlsl"
struct input
{
float3 pos : pos;
float2 uv : uv;
float4 color : color;
float3 normal : norm;
};
struct output
{
float4 pos : SV_POSITION;
float2 uv: TEXCOORD0;
float4 color : COLOR0;
float3 normal : NORMAL;
float3 gouraud : COLOR1;
};
float3 ps1_directional_light(float3 normal, float3 direction, float3 ambient, float3 lightcolor)
{
float NdotL = dot(normal, normalize(-direction));
NdotL = max(NdotL, 0.0f);
float3 color = ambient + lightcolor*NdotL;
return color;
}
output main(input i)
{
output o;
o.pos = mul(world_to_projection, float4(i.pos,1.0));
o.uv = i.uv;
o.color = i.color;
o.normal = i.normal;
float3 ambient = float3(0.2,0.2,0.2);
float3 lightdir = normalize(float3(1,1,1));
o.gouraud = ps1_directional_light(i.normal, lightdir, ambient, float3(1,1,1));
return o;
}

View File

@@ -0,0 +1,8 @@
#include "common.hlsl"
// Structure for pixel shader input (from vertex shader output).
struct PSInput
{
float2 uv : TEXCOORD0;
float4 color : COLOR0;
};

View File

@@ -0,0 +1,25 @@
#include "common.hlsl"
struct input
{
float2 pos : pos; // given as world
float2 uv : uv; // always a quad
float4 color : color;
};
// Output structure from the vertex shader to the pixel shader
struct output
{
float4 pos : SV_Position; // Clip-space position
float2 uv : TEXCOORD0; // Texture coordinates
float4 color : COLOR0; // Interpolated vertex color
};
output main(input i)
{
output o;
o.pos = mul(world_to_projection, float4(i.pos,0,1));
o.uv = i.uv;
o.color = i.color;
return o;
}

View File

@@ -1,27 +1,46 @@
# Requires shadercross CLI installed from SDL_shadercross #!/usr/bin/env bash
# Ensure directories exist
mkdir -p spirv
mkdir -p msl
mkdir -p dxil
mkdir -p reflection
# Vertex shaders
for filename in *.vert.hlsl; do for filename in *.vert.hlsl; do
if [ -f "$filename" ]; then if [ -f "$filename" ]; then
echo "compiling ${filename}" echo "compiling ${filename}"
shadercross "$filename" -o "spirv/${filename/.hlsl/.spv}" shadercross "$filename" -o "spirv/${filename/.hlsl/.spv}"
shadercross "$filename" -o "msl/${filename/.hlsl/.msl}" shadercross "$filename" -o "msl/${filename/.hlsl/.msl}"
shadercross "$filename" -o "dxil/${filename/.hlsl/.dxil}" shadercross "$filename" -o "dxil/${filename/.hlsl/.dxil}"
# Generate reflection JSON
spirv-cross "spirv/${filename/.hlsl/.spv}" --reflect > "reflection/${filename/.hlsl/.json}"
fi fi
done done
# Fragment shaders
for filename in *.frag.hlsl; do for filename in *.frag.hlsl; do
if [ -f "$filename" ]; then if [ -f "$filename" ]; then
echo "compiling ${filename}" echo "compiling ${filename}"
shadercross "$filename" -o "spirv/${filename/.hlsl/.spv}" shadercross "$filename" -o "spirv/${filename/.hlsl/.spv}"
shadercross "$filename" -o "msl/${filename/.hlsl/.msl}" shadercross "$filename" -o "msl/${filename/.hlsl/.msl}"
shadercross "$filename" -o "dxil/${filename/.hlsl/.dxil}" shadercross "$filename" -o "dxil/${filename/.hlsl/.dxil}"
# Generate reflection JSON
spirv-cross "spirv/${filename/.hlsl/.spv}" --reflect > "reflection/${filename/.hlsl/.json}"
fi fi
done done
# Compute shaders
for filename in *.comp.hlsl; do for filename in *.comp.hlsl; do
if [ -f "$filename" ]; then if [ -f "$filename" ]; then
echo "compiling ${filename}" echo "compiling ${filename}"
shadercross "$filename" -o "spirv/${filename/.hlsl/.spv}" shadercross "$filename" -o "spirv/${filename/.hlsl/.spv}"
shadercross "$filename" -o "msl/${filename/.hlsl/.msl}" shadercross "$filename" -o "msl/${filename/.hlsl/.msl}"
shadercross "$filename" -o "dxil/${filename/.hlsl/.dxil}" shadercross "$filename" -o "dxil/${filename/.hlsl/.dxil}"
# Generate reflection JSON
spirv-cross "spirv/${filename/.hlsl/.spv}" --reflect > "reflection/${filename/.hlsl/.json}"
fi fi
done done

View File

@@ -1,15 +1,12 @@
// If using a texture instead, define: // If using a texture instead, define:
#include "common/model_pixel.hlsl"
Texture2D diffuse : register(t0, space2); Texture2D diffuse : register(t0, space2);
SamplerState smp : register(s0, space2); SamplerState smp : register(s0, space2);
struct PSInput
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
float4 main(PSInput input) : SV_TARGET float4 main(PSInput input) : SV_TARGET
{ {
return float4(input.uv,1,1); float4 color = diffuse.Sample(smp, input.uv);
return float4(color.rgb*input.gouraud, 1.0);
} }

View File

@@ -1,17 +1,6 @@
#include "common.hlsl" #include "common/model_vertex.hlsl"
struct VSOutput output vertex(output o)
{ {
float4 pos : SV_POSITION; return o;
float2 uv : TEXCOORD0;
float3 normal : NORMAL; // Might be unused in no-light case, but we'll pass it anyway.
};
VSOutput main(VSInput input)
{
VSOutput output;
output.pos = mul(vp, float4(input.pos, 1.0));
output.uv = input.uv;
output.normal = input.normal; // World space normal if needed
return output;
} }

View File

@@ -1,21 +1,12 @@
#include "common/pixel.hlsl"
Texture2D diffuse : register(t0, space2); Texture2D diffuse : register(t0, space2);
SamplerState smp : register(s0, space2); SamplerState smp : register(s0, space2);
// Structure for pixel shader input (from vertex shader output).
struct PSInput
{
float2 uv : TEXCOORD0;
float4 color : COLOR0;
};
// Pixel shader main function // Pixel shader main function
float4 main(PSInput input) : SV_TARGET float4 main(PSInput input) : SV_TARGET
{ {
float4 color = diffuse.Sample(smp, input.uv); float4 color = diffuse.Sample(smp, input.uv);
color *= input.color; color *= input.color;
if (color.a == 0.0f)
clip(-1);
return color; return color;
} }

View File

@@ -1,21 +1,8 @@
#include "common.hlsl" #include "common/vertex.hlsl"
// Output structure from the vertex shader to the pixel shader
struct VertexOutput
{
float4 pos : SV_Position; // Clip-space position
float2 uv : TEXCOORD0; // Texture coordinates
float4 color : COLOR0; // Interpolated vertex color
};
// Vertex shader // Vertex shader
VertexOutput main(VSInput input) output vertex(output i)
{ {
VertexOutput output; return i;
output.pos = mul(vp, float4(input.pos,1.0));
output.uv = input.uv;
output.color = input.color;
return output;
} }

File diff suppressed because it is too large Load Diff