1329 lines
34 KiB
JavaScript
1329 lines
34 KiB
JavaScript
var unit_transform = os.make_transform();
|
|
|
|
render.doc = {
|
|
doc: "Functions for rendering modes.",
|
|
normal: "Final render with all lighting.",
|
|
wireframe: "Show only wireframes of models.",
|
|
};
|
|
|
|
var cur = {};
|
|
cur.images = [];
|
|
cur.samplers = [];
|
|
|
|
function bind_pipeline(pass, pipeline)
|
|
{
|
|
make_pipeline(pipeline)
|
|
pass.bind_pipeline(pipeline.gpu)
|
|
pass.pipeline = pipeline;
|
|
}
|
|
|
|
var main_pass;
|
|
|
|
var base_pipeline = {
|
|
vertex: "sprite.vert",
|
|
fragment: "sprite.frag",
|
|
primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
|
|
fill: true, // false for lines
|
|
depth: {
|
|
compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
|
|
test: false,
|
|
write: false,
|
|
bias: 0,
|
|
bias_slope_scale: 0,
|
|
bias_clamp: 0
|
|
},
|
|
stencil: {
|
|
enabled: true,
|
|
front: {
|
|
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
|
|
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
|
|
depth_fail: "keep",
|
|
pass: "keep"
|
|
},
|
|
back: {
|
|
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
|
|
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
|
|
depth_fail: "keep",
|
|
pass: "keep"
|
|
},
|
|
test: true,
|
|
compare_mask: 0,
|
|
write_mask: 0
|
|
},
|
|
blend: {
|
|
enabled: false,
|
|
src_rgb: "one", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
|
|
dst_rgb: "zero",
|
|
op_rgb: "add", // add/sub/rev_sub/min/max
|
|
src_alpha: "one",
|
|
dst_alpha: "zero",
|
|
op_alpha: "add"
|
|
},
|
|
cull: "none", // none/front/back
|
|
face: "cw", // cw/ccw
|
|
alpha_to_coverage: false,
|
|
multisample: {
|
|
count: 1, // number of multisamples
|
|
mask: 0xFFFFFFFF,
|
|
domask: false
|
|
},
|
|
label: "scripted pipeline",
|
|
target: {}
|
|
}
|
|
|
|
var cornflower = [62/255,96/255,113/255,1];
|
|
|
|
var sprite_pipeline = Object.create(base_pipeline);
|
|
sprite_pipeline.target = {
|
|
color_targets: [{
|
|
format:"rgba8",
|
|
blend:base_pipeline.blend
|
|
}],
|
|
depth: "d32 float s8"
|
|
};
|
|
|
|
var dbgline_pipeline = Object.create(base_pipeline);
|
|
dbgline_pipeline.vertex = "dbgline.vert.hlsl"
|
|
dbgline_pipeline.fragment = "dbgline.frag.hlsl"
|
|
dbgline_pipeline.primitive = "line"
|
|
|
|
var post_camera = {};
|
|
post_camera.transform = os.make_transform();
|
|
post_camera.transform.unit();
|
|
post_camera.zoom = 1;
|
|
post_camera.size = [640,360];
|
|
post_camera.mode = 'keep';
|
|
post_camera.viewport = {x:0,y:0,width:1,height:1}
|
|
post_camera.fov = 45;
|
|
post_camera.type = 'ortho';
|
|
post_camera.aspect = 16/9;
|
|
|
|
function get_pipeline_ubo_slot(pipeline, name)
|
|
{
|
|
if (!pipeline.vertex.reflection.ubos) return;
|
|
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
|
|
var ubo = pipeline.vertex.reflection.ubos[i];
|
|
if (ubo.name.endsWith(name))
|
|
return i;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function transpose4x4(val) {
|
|
var out = [];
|
|
out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12];
|
|
out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13];
|
|
out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14];
|
|
out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15];
|
|
return out;
|
|
}
|
|
|
|
function ubo_obj_to_array(pipeline, name, obj)
|
|
{
|
|
var ubo;
|
|
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
|
|
ubo = pipeline.vertex.reflection.ubos[i];
|
|
if (ubo.name.endsWith(name)) break;
|
|
}
|
|
var type = pipeline.vertex.reflection.types[ubo.type];
|
|
var len = 0;
|
|
for (var mem of type.members)
|
|
len += type_to_byte_count(mem.type);
|
|
|
|
var buf = new ArrayBuffer(len);
|
|
var view = new DataView(buf);
|
|
|
|
for (var mem of type.members) {
|
|
var val = obj[mem.name];
|
|
if (!val) throw new Error (`Could not find ${mem.name} on supplied object`);
|
|
|
|
if (mem.name === 'model')
|
|
val = transpose4x4(val.array());
|
|
|
|
for (var i = 0; i < val.length; i++)
|
|
view.setFloat32(mem.offset + i*4, val[i],true);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
function type_to_byte_count(type)
|
|
{
|
|
switch (type) {
|
|
case 'float':
|
|
return 4;
|
|
case 'vec2':
|
|
return 8;
|
|
case 'vec3':
|
|
return 12;
|
|
case 'vec4':
|
|
return 16;
|
|
case 'mat4':
|
|
return 64;
|
|
// Add cases as needed
|
|
default:
|
|
throw new Error("Unknown or unsupported float-based type: " + type);
|
|
}
|
|
}
|
|
|
|
var sprite_model_ubo = {
|
|
model: unit_transform,
|
|
color: [1,1,1,1]
|
|
};
|
|
|
|
render.poly_prim = function poly_prim(verts) {
|
|
var index = [];
|
|
if (verts.length < 1) return undefined;
|
|
|
|
for (var i = 0; i < verts.length; i++) verts[i][2] = 0;
|
|
|
|
for (var i = 2; i < verts.length; i++) {
|
|
index.push(0);
|
|
index.push(i - 1);
|
|
index.push(i);
|
|
}
|
|
|
|
return {
|
|
pos: os.make_buffer(verts.flat()),
|
|
verts: verts.length,
|
|
index: os.make_buffer(index, 1),
|
|
count: index.length,
|
|
};
|
|
};
|
|
|
|
var shader_cache = {};
|
|
var shader_times = {};
|
|
|
|
render.hotreload = function shader_hotreload(file) {
|
|
console.warn('reimplement shader hot reloading for ' + file)
|
|
};
|
|
|
|
function make_pipeline(pipeline) {
|
|
if (pipeline.gpu) return; // this pipeline has already been made
|
|
|
|
if (typeof pipeline.vertex === 'string')
|
|
pipeline.vertex = make_shader(pipeline.vertex);
|
|
if (typeof pipeline.fragment === 'string')
|
|
pipeline.fragment = make_shader(pipeline.fragment)
|
|
|
|
// 1) Reflection data for vertex shader
|
|
var refl = pipeline.vertex.reflection
|
|
if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
|
|
pipeline.gpu = render._main.make_pipeline(pipeline);
|
|
return;
|
|
}
|
|
|
|
var inputs = refl.inputs
|
|
var buffer_descriptions = []
|
|
var attributes = []
|
|
|
|
// 2) For each input in the reflection, build one buffer + attribute
|
|
// (Simplest approach: each input is stored in its own slot, offset=0)
|
|
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"
|
|
}
|
|
}
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
// 3) Attach these arrays onto the pipeline object
|
|
pipeline.vertex_buffer_descriptions = buffer_descriptions
|
|
pipeline.vertex_attributes = attributes
|
|
|
|
// 4) Hand off the pipeline to native code
|
|
pipeline.gpu = render._main.make_pipeline(pipeline);
|
|
}
|
|
|
|
var shader_type;
|
|
|
|
function make_shader(sh_file) {
|
|
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
|
|
|
|
var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
|
|
if (shader_cache[file]) return shader_cache[file]
|
|
|
|
var shader = {
|
|
code: io.slurpbytes(file),
|
|
format: shader_type,
|
|
stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
|
|
num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
|
|
num_textures: 0,
|
|
num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
|
|
num_uniform_buffers: refl.ubos ? refl.ubos.length : 0,
|
|
entrypoint: shader_type === "msl" ? "main0" : "main"
|
|
}
|
|
|
|
shader.gpu = render._main.make_shader(shader)
|
|
shader.reflection = refl;
|
|
shader_cache[file] = shader
|
|
return shader
|
|
}
|
|
|
|
render.device = {
|
|
pc: [1920, 1080],
|
|
macbook_m2: [2560, 1664, 13.6],
|
|
ds_top: [400, 240, 3.53],
|
|
ds_bottom: [320, 240, 3.02],
|
|
playdate: [400, 240, 2.7],
|
|
switch: [1280, 720, 6.2],
|
|
switch_lite: [1280, 720, 5.5],
|
|
switch_oled: [1280, 720, 7],
|
|
dsi: [256, 192, 3.268],
|
|
ds: [256, 192, 3],
|
|
dsixl: [256, 192, 4.2],
|
|
ipad_air_m2: [2360, 1640, 11.97],
|
|
iphone_se: [1334, 750, 4.7],
|
|
iphone_12_pro: [2532, 1170, 6.06],
|
|
iphone_15: [2556, 1179, 6.1],
|
|
gba: [240, 160, 2.9],
|
|
gameboy: [160, 144, 2.48],
|
|
gbc: [160, 144, 2.28],
|
|
steamdeck: [1280, 800, 7],
|
|
vita: [960, 544, 5],
|
|
psp: [480, 272, 4.3],
|
|
imac_m3: [4480, 2520, 23.5],
|
|
macbook_pro_m3: [3024, 1964, 14.2],
|
|
ps1: [320, 240, 5],
|
|
ps2: [640, 480],
|
|
snes: [256, 224],
|
|
gamecube: [640, 480],
|
|
n64: [320, 240],
|
|
c64: [320, 200],
|
|
macintosh: [512, 342, 9],
|
|
gamegear: [160, 144, 3.2],
|
|
};
|
|
|
|
var render_queue = [];
|
|
|
|
render.device.doc = `Device resolutions given as [x,y,inches diagonal].`;
|
|
var std_sampler = {
|
|
min_filter: "nearest",
|
|
mag_filter: "nearest",
|
|
mipmap: "linear",
|
|
u: "repeat",
|
|
v: "repeat",
|
|
w: "repeat",
|
|
mip_bias: 0,
|
|
max_anisotropy: 0,
|
|
compare_op: "none",
|
|
min_lod: 0,
|
|
max_lod: 0,
|
|
anisotropy: false,
|
|
compare: false
|
|
};
|
|
|
|
var tbuffer;
|
|
|
|
function upload_model(model)
|
|
{
|
|
var bufs = [];
|
|
for (var i in model) {
|
|
if (typeof model[i] !== 'object') continue;
|
|
if (i === 'indices') model[i].index = true;
|
|
bufs.push(model[i]);
|
|
}
|
|
tbuffer = render._main.upload(this, bufs, tbuffer);
|
|
}
|
|
|
|
function bind_model(pass,pipeline,model)
|
|
{
|
|
var buffers = pipeline.vertex_buffer_descriptions;
|
|
var bufs = [];
|
|
if (buffers)
|
|
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, pipeline, mat)
|
|
{
|
|
var imgs = [];
|
|
var refl = pipeline.fragment.reflection;
|
|
if (refl.separate_images) {
|
|
for (var i of refl.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 group_sprites_by_texture(sprites)
|
|
{
|
|
if (sprites.length === 0) return;
|
|
var groups = [];
|
|
var lasttex = sprites[0].image;
|
|
var group = [];
|
|
var first = 0;
|
|
for (var i = 0; i < sprites.length; i++) {
|
|
if (lasttex !== sprites[i].image) {
|
|
groups.push({image:lasttex, num_indices:(i-first)*6, first_index:first*6});
|
|
lasttex = sprites[i].image;
|
|
first = i;
|
|
group = [];
|
|
}
|
|
|
|
group.push(sprites[i])
|
|
}
|
|
|
|
groups.push({
|
|
image:lasttex,num_indices:(sprites.length-first)*6,first_index:first*6
|
|
})
|
|
|
|
return groups;
|
|
}
|
|
|
|
var main_color = {
|
|
type:"2d",
|
|
format: "rgba8",
|
|
layers: 1,
|
|
mip_levels: 1,
|
|
samples: 0,
|
|
sampler:true,
|
|
color_target:true
|
|
};
|
|
|
|
var main_depth = {
|
|
type: "2d",
|
|
format: "d32 float s8",
|
|
layers:1,
|
|
mip_levels:1,
|
|
samples:0,
|
|
sampler:true,
|
|
depth_target:true
|
|
};
|
|
|
|
function render_camera(cmds, camera)
|
|
{
|
|
if (!camera.target) {
|
|
main_color.width = main_depth.width = prosperon.camera.size.x;
|
|
main_color.height = main_depth.height = prosperon.camera.size.y;
|
|
camera.target = {
|
|
color_targets: [{
|
|
texture: render._main.texture(main_color),
|
|
mip_level:0,
|
|
layer: 0,
|
|
load:"clear",
|
|
store:"store",
|
|
clear: cornflower
|
|
}
|
|
],
|
|
depth_stencil: {
|
|
texture: render._main.texture(main_depth),
|
|
clear:1,
|
|
load:"clear",
|
|
store:"dont_care",
|
|
stencil_load:"clear",
|
|
stencil_store:"dont_care",
|
|
stencil_clear:0
|
|
}
|
|
};
|
|
}
|
|
|
|
if (render_queue.length == 0) return;
|
|
var spritemesh = render._main.make_sprite_mesh(render_queue);
|
|
|
|
cmds.upload_model(spritemesh);
|
|
var pass = cmds.render_pass(camera.target);
|
|
var camera = prosperon.camera;
|
|
|
|
var draw_cmds = group_sprites_by_texture(render_queue);
|
|
for (var group of draw_cmds) {
|
|
var pipeline = sprite_pipeline;
|
|
var mesh = spritemesh;
|
|
var img = group.image;
|
|
img.sampler = std_sampler;
|
|
bind_pipeline(pass, pipeline);
|
|
bind_mat(pass, pipeline, {diffuse:img});
|
|
bind_model(pass,pipeline,spritemesh);
|
|
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
|
if (typeof camslot !== 'undefined')
|
|
cmds.camera(camera, pass, undefined, camslot);
|
|
var modelslot = get_pipeline_ubo_slot(pipeline, "model");
|
|
if (typeof modelslot !== 'undefined') {
|
|
var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo);
|
|
cmds.push_vertex_uniform_data(modelslot, ubo);
|
|
}
|
|
pass.draw_indexed(group.num_indices, 1, mesh.first_index, 0, 0);
|
|
}
|
|
|
|
pass.end();
|
|
|
|
render_queue = [];
|
|
spritemesh = undefined;
|
|
}
|
|
|
|
function mode_rect(src,dst,mode = "stretch")
|
|
{
|
|
var aspect_src = src.width/src.height;
|
|
var aspect_dst = dst.width/dst.height;
|
|
var out = {
|
|
x:dst.x,
|
|
y:dst.y,
|
|
width:dst.width,
|
|
height:dst.height
|
|
};
|
|
if (mode == "stretch") return out;
|
|
|
|
if (mode == "letterbox") {
|
|
if (aspect_src > aspect_dst) {
|
|
var scaled_h = out.width/aspect_src;
|
|
var off = (out.height - scaled_h) * 0.5;
|
|
out.y += off;
|
|
out.height = scaled_h;
|
|
} else {
|
|
var scaled_w =out.height * aspect_src;
|
|
var off = (out.width - scaled_w) * 0.5;
|
|
out.x += off;
|
|
out.width = scaled_w;
|
|
}
|
|
} else if (mode == "overscan"){
|
|
if (aspect_src > aspect_dst) {
|
|
var scaled_w = out.height * aspect_src;
|
|
var off = (out.width - scaled_w) * 0.5;
|
|
out.x += off;
|
|
out.width = scaled_w;
|
|
} else {
|
|
var scaled_h = out.width / aspect_src;
|
|
var off = (out.height - scaled_h) * 0.5;
|
|
out.y += off;
|
|
out.height = scaled_h;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
var swaps = [];
|
|
function gpupresent()
|
|
{
|
|
try{
|
|
var cmds = render._main.acquire_cmd_buffer();
|
|
render_camera(cmds, prosperon.camera);
|
|
} catch(e) { console.error(e); } finally {
|
|
var swapchain_tex = cmds.acquire_swapchain();
|
|
if (!swapchain_tex)
|
|
cmds.cancel();
|
|
else {
|
|
var mode = prosperon.camera.presentation || "letterbox"
|
|
var src_rect = {x:0,y:0,width:640,height:360}
|
|
var dst_rect = {x:0,y:0,width:swapchain_tex.width,height:swapchain_tex.height};
|
|
var torect = mode_rect(src_rect,dst_rect,mode);
|
|
torect.texture = swapchain_tex;
|
|
if (swapchain_tex) cmds.blit({
|
|
src: prosperon.camera.target.color_targets[0].texture,
|
|
dst: torect,
|
|
filter:"nearest",
|
|
load: "clear"
|
|
});
|
|
cmds.submit()
|
|
}
|
|
}
|
|
}
|
|
|
|
var pipeline_model;
|
|
|
|
pipeline_model = Object.create(base_pipeline);
|
|
pipeline_model.vertex = "model.vert"
|
|
pipeline_model.fragment = "model.frag"
|
|
|
|
var quad_model;
|
|
|
|
render.init = function () {
|
|
shader_type = render._main.shader_format()[0];
|
|
|
|
std_sampler = render._main.make_sampler({
|
|
min_filter: "nearest",
|
|
mag_filter: "nearest",
|
|
mipmap_mode: "nearest",
|
|
address_mode_u: "repeat",
|
|
address_mode_v: "repeat",
|
|
address_mode_w: "repeat"
|
|
});
|
|
quad_model = render._main.make_quad();
|
|
io.mount("core");
|
|
render._main.present = gpupresent;
|
|
var cmds = render._main.acquire_cmd_buffer();
|
|
cmds.__proto__.upload_model = upload_model;
|
|
cmds.upload_model(quad_model);
|
|
cmds.submit();
|
|
};
|
|
|
|
render.draw_sprites = true;
|
|
render.draw_particles = true;
|
|
render.draw_hud = true;
|
|
render.draw_gui = true;
|
|
render.draw_gizmos = true;
|
|
|
|
render.sprites = function render_sprites() {
|
|
var buckets = component.sprite_buckets();
|
|
if (buckets.length === 0) return;
|
|
render.use_shader(spritessboshader);
|
|
for (var l in buckets) {
|
|
var layer = buckets[l];
|
|
for (var img in layer) {
|
|
var sparray = layer[img];
|
|
if (sparray.length === 0) continue;
|
|
var ss = sparray[0];
|
|
ss.baseinstance = render.make_sprite_ssbo(sparray, sprite_ssbo);
|
|
render.use_mat(ss);
|
|
render.draw(shape.quad, sprite_ssbo, sparray.length);
|
|
}
|
|
}
|
|
};
|
|
|
|
function draw_sprites()
|
|
{
|
|
var buckets = component.sprite_buckets();
|
|
if (buckets.length === 0) return;
|
|
for (var l in buckets) {
|
|
var layer = buckets[l];
|
|
for (var img in layer) {
|
|
var sparray = layer[img];
|
|
if (sparray.length === 0) continue;
|
|
var geometry = render._main.make_sprite_mesh(sparray);
|
|
render.geometry(sparray[0], geometry);
|
|
}
|
|
}
|
|
}
|
|
|
|
render.circle = function render_circle(pos, radius, color, inner_radius = 1) {
|
|
check_flush();
|
|
|
|
if (inner_radius >= 1) inner_radius = inner_radius / radius;
|
|
else if (inner_radius < 0) inner_radius = 1.0;
|
|
|
|
var mat = {
|
|
radius: radius,
|
|
inner_r: inner_radius,
|
|
coord: pos,
|
|
shade: color,
|
|
};
|
|
render.use_shader(circleshader);
|
|
render.use_mat(mat);
|
|
render.draw(shape.quad);
|
|
};
|
|
render.circle.doc = "Draw a circle at pos, with a given radius and color. If inner_radius is between 0 and 1, it acts as a percentage of radius. If it is above 1, is acts as a unit (usually a pixel).";
|
|
|
|
render.poly = function render_poly(points, color, transform) {
|
|
var buffer = render.poly_prim(points);
|
|
var mat = { shade: color };
|
|
render.use_shader(polyshader);
|
|
set_model(transform);
|
|
render.use_mat(mat);
|
|
render.draw(buffer);
|
|
};
|
|
|
|
var nextflush = undefined;
|
|
function flush() {
|
|
nextflush?.();
|
|
nextflush = undefined;
|
|
}
|
|
|
|
// If flush_fn was already on deck, it does not flush. Otherwise, flushes and then sets the flush fn
|
|
function check_flush(flush_fn) {
|
|
if (!nextflush) nextflush = flush_fn;
|
|
else if (nextflush !== flush_fn) {
|
|
nextflush();
|
|
nextflush = flush_fn;
|
|
}
|
|
}
|
|
|
|
render.flush = check_flush;
|
|
render.forceflush = function forceflush()
|
|
{
|
|
if (nextflush) nextflush();
|
|
nextflush = undefined;
|
|
cur.shader = undefined;
|
|
}
|
|
|
|
var poly_cache = [];
|
|
var poly_idx = 0;
|
|
var poly_ssbo;
|
|
|
|
function poly_e() {
|
|
var e;
|
|
poly_idx++;
|
|
if (poly_idx > poly_cache.length) {
|
|
e = {
|
|
transform: os.make_transform(),
|
|
color: Color.white,
|
|
};
|
|
poly_cache.push(e);
|
|
return e;
|
|
}
|
|
var e = poly_cache[poly_idx - 1];
|
|
e.transform.unit();
|
|
return e;
|
|
}
|
|
|
|
function flush_poly() {
|
|
if (poly_idx === 0) return;
|
|
render.use_shader(queued_shader, queued_pipe);
|
|
var base = render.make_particle_ssbo(poly_cache.slice(0, poly_idx), poly_ssbo);
|
|
render.use_mat({baseinstance:base});
|
|
render.draw(shape.quad, poly_ssbo, poly_idx);
|
|
poly_idx = 0;
|
|
}
|
|
|
|
// render.line has uv and can be texture mapped; dbg_line is hardware standard lines
|
|
render.line = function render_line(points, color = Color.white, thickness = 1, pipe = base_pipeline) {
|
|
// render._main.line(points, color);
|
|
};
|
|
|
|
render.dbg_line = function(points, color = Color.white)
|
|
{
|
|
|
|
}
|
|
|
|
render.dbg_point = function(points, color = Color.white)
|
|
{
|
|
|
|
}
|
|
|
|
/* All draw in screen space */
|
|
render.point = function (pos, size, color = Color.blue) {
|
|
render._main.point(pos,color);
|
|
};
|
|
|
|
render.cross = function render_cross(pos, size, color = Color.red, thickness = 1, pipe = base_pipeline) {
|
|
var a = [pos.add([0, size]), pos.add([0, -size])];
|
|
var b = [pos.add([size, 0]), pos.add([-size, 0])];
|
|
render.line(a, color, thickness);
|
|
render.line(b, color, thickness);
|
|
};
|
|
|
|
render.arrow = function render_arrow(start, end, color = Color.red, wingspan = 4, wingangle = 10, pipe = base_pipeline) {
|
|
var dir = end.sub(start).normalized();
|
|
var wing1 = [Vector.rotate(dir, wingangle).scale(wingspan).add(end), end];
|
|
var wing2 = [Vector.rotate(dir, -wingangle).scale(wingspan).add(end), end];
|
|
render.line([start, end], color);
|
|
render.line(wing1, color);
|
|
render.line(wing2, color);
|
|
};
|
|
|
|
render.coordinate = function render_coordinate(pos, size, color) {
|
|
render.text(JSON.stringify(pos.map(p => Math.round(p))), pos, size, color);
|
|
render.point(pos, 2, color);
|
|
};
|
|
|
|
var queued_shader;
|
|
var queued_pipe;
|
|
render.rectangle = function render_rectangle(rect, color = Color.white, pipe = base_pipeline) {
|
|
render._main.fillrect(rect,color);
|
|
};
|
|
|
|
render.text = function text(text, rect, font = cur_font, size = 0, color = Color.white, wrap = 0, pipe = base_pipeline) {
|
|
// if (typeof font === 'string')
|
|
// font = render.get_font(font)
|
|
// var mesh = os.make_text_buffer(text, rect, 0, color, wrap, font);
|
|
// render._main.geometry(font.texture, mesh);
|
|
render_queue.push({
|
|
type: 'text',
|
|
text,
|
|
rect,
|
|
font,
|
|
size,
|
|
color,
|
|
wrap,
|
|
pipe
|
|
});
|
|
return;
|
|
|
|
if (typeof font === 'string')
|
|
font = render.get_font(font);
|
|
|
|
if (!font) return;
|
|
var pos = [rect.x,rect.y];
|
|
pos.y -= font.descent;
|
|
if (rect.anchor_y)
|
|
pos.y -= rect.anchor_y*(font.ascent-font.descent);
|
|
os.make_text_buffer(str, pos, size, color, wrap, font); // this puts text into buffer
|
|
cur_font = font;
|
|
check_flush(render.flush_text);
|
|
};
|
|
|
|
var tttsize = render.text_size;
|
|
render.text_size = function(str, font, ...args)
|
|
{
|
|
if (typeof font === 'string')
|
|
font = render.get_font(font);
|
|
return tttsize(str,font, ...args);
|
|
}
|
|
|
|
var lasttex = undefined;
|
|
var img_cache = [];
|
|
var img_idx = 0;
|
|
|
|
function flush_img() {
|
|
if (img_idx === 0) return;
|
|
render.use_shader(spritessboshader);
|
|
var startidx = render.make_sprite_ssbo(img_cache.slice(0, img_idx), sprite_ssbo);
|
|
render.use_mat({baseinstance:startidx});
|
|
cur.images = [lasttex];
|
|
render.draw(shape.quad, sprite_ssbo, img_idx);
|
|
lasttex = undefined;
|
|
img_idx = 0;
|
|
}
|
|
|
|
function img_e() {
|
|
img_idx++;
|
|
if (img_idx > img_cache.length) {
|
|
var e = {
|
|
transform: os.make_transform(),
|
|
shade: Color.white,
|
|
};
|
|
img_cache.push(e);
|
|
return e;
|
|
}
|
|
return img_cache[img_idx - 1];
|
|
}
|
|
|
|
var stencil_write = {
|
|
compare: "always",
|
|
fail_op: "replace",
|
|
depth_fail_op: "replace",
|
|
pass_op: "replace"
|
|
};
|
|
|
|
var stencil_writer = function stencil_writer(ref)
|
|
{
|
|
var pipe = Object.create(base_pipeline);
|
|
Object.assign(pipe, {
|
|
stencil: {
|
|
enabled: true,
|
|
front: stencil_write,
|
|
back: stencil_write,
|
|
write:true,
|
|
read:true,
|
|
ref:ref
|
|
},
|
|
write_mask: colormask.none
|
|
});
|
|
return pipe;
|
|
}.hashify();
|
|
|
|
render.stencil_writer = stencil_writer;
|
|
|
|
// objects by default draw where the stencil buffer is 0
|
|
render.fillmask = function fillmask(ref)
|
|
{
|
|
render.forceflush();
|
|
var pipe = stencil_writer(ref);
|
|
render.use_shader('screenfill.cg', pipe);
|
|
render.draw(shape.quad);
|
|
}
|
|
|
|
var stencil_invert = {
|
|
compare: "always",
|
|
fail_op: "invert",
|
|
depth_fail_op: "invert",
|
|
pass_op: "invert"
|
|
};
|
|
|
|
render.mask = function mask(image, pos, scale, rotation = 0, ref = 1)
|
|
{
|
|
if (typeof image === 'string')
|
|
image = game.texture(image);
|
|
|
|
var tex = image.texture;
|
|
|
|
if (scale) scale = sacle.div([tex.width,tex.height]);
|
|
else scale = vector.v3one;
|
|
|
|
var pipe = stencil_writer(ref);
|
|
render.use_shader('sprite.cg', pipe);
|
|
var t = os.make_transform();
|
|
t.trs(pos, undefined,scale);
|
|
set_model(t);
|
|
render.use_mat({
|
|
diffuse:image.texture,
|
|
rect: image.rect,
|
|
shade: Color.white
|
|
});
|
|
render.draw(shape.quad);
|
|
}
|
|
|
|
function calc_image_size(img)
|
|
{
|
|
return [img.texture.width*img.rect.width, img.texture.height*img.rect.height];
|
|
}
|
|
|
|
render.tile = function tile(image, rect = [0,0], color = Color.white)
|
|
{
|
|
if (!image) throw Error ('Need an image to render.')
|
|
if (typeof image === "string")
|
|
image = game.texture(image);
|
|
|
|
render._main.tile(image, rect, undefined, 1);
|
|
return;
|
|
|
|
var tex = image.texture;
|
|
if (!tex) return;
|
|
|
|
var image_size = calc_image_size(image); //image.size;
|
|
|
|
var size = [rect.width ? rect.width : image_size.x, rect.height ? rect.height : image_size.y];
|
|
|
|
if (!lasttex) {
|
|
check_flush(flush_img);
|
|
lasttex = tex;
|
|
}
|
|
|
|
if (lasttex !== tex) {
|
|
flush_img();
|
|
lasttex = tex;
|
|
}
|
|
|
|
render._main.tile(image.texture, rect, image.rect, 1);
|
|
return;
|
|
}
|
|
|
|
render.geometry = function geometry(material, geometry)
|
|
{
|
|
render._main.geometry(material.diffuse.texture, geometry);
|
|
}
|
|
|
|
// queues to be flushed later
|
|
render.image = function image(image, rect = [0,0], rotation = 0, color = Color.white, pipeline = base_pipeline) {
|
|
if (!image) throw Error ('Need an image to render.')
|
|
if (typeof image === "string")
|
|
image = game.texture(image);
|
|
|
|
rect.width ??= image.texture.width;
|
|
rect.height ??= image.texture.height;
|
|
var T = os.make_transform();
|
|
T.rect(rect);
|
|
render_queue.push({
|
|
transform: T,
|
|
color: color,
|
|
image:image,
|
|
pipeline: pipeline
|
|
});
|
|
};
|
|
|
|
render.images = function images(image, rects)
|
|
{
|
|
if (!image) throw Error ('Need an image to render.');
|
|
if (typeof image === "string") image = game.texture(image);
|
|
for (var rect of rects) render.image(image,rect);
|
|
return;
|
|
var tex = image.texture;
|
|
if (!tex) return;
|
|
|
|
var image_size = calc_image_size(image);
|
|
|
|
if (!lasttex) {
|
|
check_flush(flush_img);
|
|
lasttex = tex;
|
|
}
|
|
|
|
if (lasttex !== tex) {
|
|
flush_img();
|
|
lasttex = tex;
|
|
}
|
|
|
|
// rects = rects.flat();
|
|
var rect = rects[0];
|
|
var size = [rect.width ? rect.width : image_size.x, rect.height ? rect.height : image_size.y];
|
|
var offset = size.scale([rect.anchor_x, rect.anchor_y]);
|
|
for (var rect of rects) {
|
|
var e = img_e();
|
|
var pos = [rect.x,rect.y].sub(offset);
|
|
e.transform.trs(pos, undefined, size);
|
|
e.image = image;
|
|
e.shade = Color.white;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// slice is given in pixels
|
|
render.slice9 = function slice9(image, rect = [0,0], slice = 0, color = Color.white) {
|
|
render.image(image,rect,undefined,color); return;
|
|
if (typeof image === 'string')
|
|
image = game.texture(image);
|
|
|
|
rect.width ??= image.texture.width;
|
|
rect.height ??= image.texture.height;
|
|
slice = clay.normalizeSpacing(slice);
|
|
|
|
render._main.slice9(image.texture, rect, slice);
|
|
};
|
|
|
|
var textssbos = [];
|
|
var tdraw = 0;
|
|
var cur_font = undefined;
|
|
|
|
render.flush_text = function flush_text() {
|
|
if (!render.textshader) return;
|
|
tdraw++;
|
|
if (textssbos.length < tdraw) textssbos.push(render.make_textssbo());
|
|
|
|
var textssbo = textssbos[tdraw - 1];
|
|
var amt = render.flushtext(textssbo); // load from buffer into ssbo
|
|
|
|
if (amt === 0) {
|
|
tdraw--;
|
|
return;
|
|
}
|
|
|
|
render.use_shader(render.textshader);
|
|
render.use_mat({ text: cur_font.texture });
|
|
render.draw(shape.quad, textssbo, amt);
|
|
};
|
|
|
|
var fontcache = {};
|
|
var datas= [];
|
|
render.get_font = function get_font(path,size)
|
|
{
|
|
var parts = path.split('.');
|
|
if (!isNaN(parts[1])) {
|
|
path = parts[0];
|
|
size = Number(parts[1]);
|
|
}
|
|
path = Resources.find_font(path);
|
|
var fontstr = `${path}.${size}`;
|
|
if (fontcache[fontstr]) return fontcache[fontstr];
|
|
|
|
var data = io.slurpbytes(path);
|
|
fontcache[fontstr] = os.make_font(data,size);
|
|
fontcache[fontstr].texture = render._main.load_texture(fontcache[fontstr].surface);
|
|
return fontcache[fontstr];
|
|
}
|
|
|
|
render.doc = "Draw shapes in screen space.";
|
|
render.cross.doc = "Draw a cross centered at pos, with arm length size.";
|
|
render.arrow.doc = "Draw an arrow from start to end, with wings of length wingspan at angle wingangle.";
|
|
render.rectangle.doc = "Draw a rectangle, with its corners at lowerleft and upperright.";
|
|
|
|
render.draw = function render_draw(mesh, material, pipeline) {
|
|
|
|
};
|
|
|
|
render.viewport = function(rect)
|
|
{
|
|
render._main.viewport(rect);
|
|
}
|
|
|
|
render.scissor = function(rect)
|
|
{
|
|
render.viewport(rect)
|
|
}
|
|
|
|
function camscreen2world(pos) {
|
|
var view = this.screen2cam(pos);
|
|
var viewport = render._main.get_viewport();
|
|
view.x *= viewport.width;
|
|
view.y *= viewport.height;
|
|
view = view.add(this.pos.xy);
|
|
view = view.sub([viewport.width,viewport.height].scale(0.5))
|
|
// view = view.scale(this.transform.scale);
|
|
return view;
|
|
}
|
|
|
|
// world coordinates, the "actual" view relative to the game's universe
|
|
// camera coordinates, normalized from 0 to 1 inside of a camera's viewport, bottom left is 0,0, top right is 1,1
|
|
// screen coordinates, pixels, 0,0 at the top left of the window and [w,h] at the bottom right of the window
|
|
// hud coordinates, same as screen coordinates but the top left is 0,0
|
|
|
|
camscreen2world.doc = "Convert a view position for a camera to world.";
|
|
|
|
// return camera coordinates given a screen position
|
|
function screen2cam(pos) {
|
|
var tpos = render._main.coords(pos);
|
|
var viewport = render._main.get_viewport();
|
|
var viewpos = tpos.div([viewport.width,viewport.height]);
|
|
viewpos.y *= -1;
|
|
viewpos.y += 1;
|
|
return viewpos;
|
|
}
|
|
|
|
screen2cam.doc = "Convert a screen space position in pixels to a normalized viewport position in a camera.";
|
|
|
|
prosperon.gizmos = function gizmos() {
|
|
game.all_objects(o => {
|
|
if (o.gizmo) render.image(game.texture(o.gizmo), o.pos);
|
|
});
|
|
};
|
|
|
|
function screen2hud(pos)
|
|
{
|
|
var campos = this.screen2cam(pos);
|
|
var viewport = render._main.get_viewport();
|
|
campos = campos.scale([viewport.width,viewport.height]);
|
|
return campos;
|
|
}
|
|
|
|
/* cameras
|
|
* Cameras have a position and rotation. They are not affected by scale.
|
|
*/
|
|
|
|
prosperon.make_camera = function make_camera() {
|
|
var cam = world.spawn();
|
|
cam.near = 1;
|
|
cam.far = -1000;
|
|
cam.ortho = true; // True if this is a 2d camera
|
|
cam.size = prosperon.size.slice() // The render size of this camera in pixels
|
|
// In ortho mode, this determines how many pixels it will see
|
|
cam.mode = "stretch";
|
|
cam.screen2world = camscreen2world;
|
|
cam.screen2cam = screen2cam;
|
|
cam.screen2hud = screen2hud;
|
|
cam.zoom = 1; // the "scale factor" this camera demonstrates
|
|
// camera renders draw calls, and then hud
|
|
cam.render = function() {
|
|
prosperon.draw();
|
|
draw_sprites();
|
|
render._main.camera(this.transform,true);
|
|
render._main.scale([this.zoom, this.zoom]);
|
|
prosperon.hud();
|
|
render._main.scale([1,1]);
|
|
render._main.camera(unit_transform,false);
|
|
|
|
}
|
|
return cam;
|
|
};
|
|
|
|
var screencolor;
|
|
|
|
globalThis.imtoggle = function (name, obj, field) {
|
|
var changed = false;
|
|
var old = obj[field];
|
|
obj[field] = imgui.checkbox(name, obj[field]);
|
|
if (old !== obj[field]) return true;
|
|
return false;
|
|
};
|
|
var replstr = "";
|
|
|
|
var imdebug = function imdebug() {
|
|
imtoggle("Physics", debug, "draw_phys");
|
|
imtoggle("Bouning boxes", debug, "draw_bb");
|
|
imtoggle("Gizmos", debug, "draw_gizmos");
|
|
imtoggle("Names", debug, "draw_names");
|
|
imtoggle("Sprite nums", debug, "sprite_nums");
|
|
imtoggle("Debug overlay", debug, "show");
|
|
imtoggle("Show ur names", debug, "urnames");
|
|
};
|
|
|
|
var observed_tex = undefined;
|
|
|
|
var imgui_fn = function imgui_fn() {
|
|
return;
|
|
imgui.newframe(prosperon.size.x, prosperon.size.y, 0.01);
|
|
if (debug.console)
|
|
debug.console = imgui.window("console", _ => {
|
|
imgui.text(console.transcript);
|
|
replstr = imgui.textinput(undefined, replstr);
|
|
imgui.button("submit", _ => {
|
|
eval(replstr);
|
|
replstr = "";
|
|
});
|
|
});
|
|
|
|
imgui.mainmenubar(_ => {
|
|
imgui.menu("File", _ => {
|
|
imgui.menu("Game settings", _ => {
|
|
prosperon.title = imgui.textinput("Title", prosperon.title);
|
|
prosperon.icon = imgui.textinput("Icon", prosperon.icon);
|
|
imgui.button("Refresh window", _ => {
|
|
prosperon.set_icon(game.texture(prosperon.icon));
|
|
});
|
|
});
|
|
imgui.button("quit", os.exit);
|
|
});
|
|
imgui.menu("Debug", imdebug);
|
|
imgui.menu("View", _ => {
|
|
imtoggle("Profiler", debug, "showprofiler");
|
|
imtoggle("Terminal out", debug, "termout");
|
|
imtoggle("Meta [f7]", debug, "meta");
|
|
imtoggle("Cheats [f8]", debug, "cheat");
|
|
imtoggle("Console [f9]", debug, "console");
|
|
if (profile.tracing)
|
|
imgui.button("stop trace", profile.trace_stop);
|
|
else
|
|
imgui.button('start trace', profile.trace_start);
|
|
});
|
|
|
|
imgui.sokol_gfx();
|
|
|
|
imgui.menu("Graphics", _ => {
|
|
imtoggle("Draw sprites", render, "draw_sprites");
|
|
imtoggle("Draw particles", render, "draw_particles");
|
|
imtoggle("Draw HUD", render, "draw_hud");
|
|
imtoggle("Draw GUI", render, "draw_gui");
|
|
imtoggle("Draw gizmos", render, "draw_gizmos");
|
|
|
|
imgui.menu("Window", _ => {
|
|
prosperon.fullscreen = imgui.checkbox("fullscreen", prosperon.fullscreen);
|
|
// prosperon.vsync = imgui.checkbox("vsync", prosperon.vsync);
|
|
imgui.menu("MSAA", _ => {
|
|
for (var msaa of gamestate.msaa) imgui.button(msaa + "x", _ => (prosperon.sample_count = msaa));
|
|
});
|
|
});
|
|
});
|
|
|
|
prosperon.menu_hook?.();
|
|
});
|
|
|
|
/*
|
|
if (observed_tex) {
|
|
imgui.window("texture", _ => {
|
|
imgui.image(observed_tex);
|
|
});
|
|
}
|
|
|
|
var texs = {};
|
|
for (var path in game.texture.cache) {
|
|
var image = game.texture.cache[path];
|
|
if (image.texture && !texs[image.texture])
|
|
texs[image.texture] = image.texture;
|
|
}
|
|
imgui.window("textures", _ => {
|
|
for (var img in texs) {
|
|
imgui.button(img, _ => {
|
|
observed_tex = texs[img];
|
|
});
|
|
}
|
|
});
|
|
*/
|
|
prosperon.imgui();
|
|
imgui.endframe(render._main);
|
|
};
|
|
|
|
// figure out the highest resolution we can render at that's an integer
|
|
/* var basesize = prosperon.camera.size.slice();
|
|
var baseview = prosperon.camera.view();
|
|
var wh = [baseview[2]-baseview[0], baseview[3]-baseview[1]];
|
|
var mult = 1;
|
|
var trysize = basesize.scale(mult);
|
|
while (trysize.x <= wh.x && trysize.y <= wh.y) {
|
|
mult++;
|
|
trysize = basesize.scale(mult);
|
|
}
|
|
if (Math.abs(wh.x - basesize.scale(mult-1).x) < Math.abs(wh.x - trysize.x))
|
|
mult--;
|
|
|
|
\prosperon.window_render(basesize.scale(mult));
|
|
*/
|
|
|
|
prosperon.render = function prosperon_render() {
|
|
try{
|
|
//if (debug.show) try { imgui_fn(); } catch(e) { console.error(e) }
|
|
} catch(e) {
|
|
console.error(e)
|
|
} finally {
|
|
render._main.present();
|
|
|
|
}
|
|
};
|
|
|
|
//if (dmon) dmon.watch('.');
|
|
|
|
function dmon_cb(e)
|
|
{
|
|
try {
|
|
io.invalidate();
|
|
if (e.file.startsWith('.')) return;
|
|
if (e.file.endsWith('.js'))
|
|
actor.hotreload(e.file);
|
|
else if (e.file.endsWith('.hlsl'))
|
|
shader_hotreload(e.file);
|
|
else if (Resources.is_image(e.file))
|
|
game.tex_hotreload(e.file);
|
|
} catch(e) { console.error(e); }
|
|
}
|
|
|
|
var waittime = 1/60;
|
|
var last_frame_time = 0;
|
|
// Ran once per frame
|
|
prosperon.process = function process() {
|
|
var now = profile.now();
|
|
var dt = now - last_frame_time;
|
|
if (dt < waittime) os.sleep(waittime-dt);
|
|
last_frame_time = profile.now();
|
|
try {
|
|
layout.newframe();
|
|
// check for hot reloading
|
|
if (dmon) dmon.poll(dmon_cb);
|
|
var dt = last_frame_time - frame_t;
|
|
frame_t = last_frame_time;
|
|
|
|
game.engine_input(e => {
|
|
prosperon[e.type]?.(e);
|
|
});
|
|
|
|
try { prosperon.appupdate(dt); } catch(e) { console.error(e) }
|
|
|
|
input.procdown();
|
|
try {
|
|
update_emitters(dt * game.timescale);
|
|
os.update_timers(dt * game.timescale);
|
|
prosperon.update(dt*game.timescale);
|
|
} catch(e) { console.error(e) }
|
|
|
|
if (sim.mode === "step") sim.pause();
|
|
if (sim.mode === "play" || sim.mode === "step") {
|
|
/*
|
|
physlag += dt;
|
|
|
|
while (physlag > physics.delta) {
|
|
physlag -= physics.delta;
|
|
prosperon.phys2d_step(physics.delta * game.timescale);
|
|
prosperon.physupdate(physics.delta * game.timescale);
|
|
}
|
|
*/
|
|
}
|
|
|
|
try { prosperon.draw(); } catch(e) {console.error(e)}
|
|
prosperon.render();
|
|
} catch(e) {
|
|
console.error(e)
|
|
}
|
|
tracy.end_frame();
|
|
};
|
|
|
|
return { render };
|