Files
cell/scripts/render.js

1409 lines
37 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 = [];
var tbuffer;
function full_upload(buffers)
{
var cmds = render._main.acquire_cmd_buffer();
tbuffer = render._main.upload(cmds, buffers, tbuffer);
cmds.submit();
}
function queue_sprite_mesh(queue)
{
var sprites = queue.filter(x => x.type === 'sprite');
if (sprites.length === 0) return [];
var mesh = render._main.make_sprite_mesh(sprites);
for (var i = 0; i < sprites.length; i++) {
sprites[i].mesh = mesh;
sprites[i].first_index = i*6;
sprites[i].num_indices = 6;
}
return [mesh.pos,mesh.uv,mesh.color,mesh.indices]
}
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.blend = {
enabled:true,
src_rgb: "src_alpha", // 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: "one_minus_src_alpha",
op_rgb: "add", // add/sub/rev_sub/min/max
src_alpha: "one",
dst_alpha: "zero",
op_alpha: "add"
};
sprite_pipeline.target = {
color_targets: [{
format:"rgba8",
blend:sprite_pipeline.blend
}],
depth: "d32 float s8"
};
var rect_pipeline = Object.create(sprite_pipeline)
rect_pipeline.fragment = "rectangle.frag"
var circle_pipeline = Object.create(sprite_pipeline)
circle_pipeline.fragment = "circle.frag"
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.hasOwnProperty("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 = 16
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}`
if (shader_cache[file]) return shader_cache[file]
var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
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"
}
console.log(`making shader ${sh_file} of format ${shader_type}`)
shader.gpu = render._main.make_shader(shader)
shader.reflection = refl;
shader_cache[file] = shader
shader.file = sh_file
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 = [];
var hud_queue = [];
var current_queue = 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;
bufs.push(model[i]);
}
render._main.upload(this, bufs);
}
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, mesh)
{
if (sprites.length === 0) return;
for (var i = 0; i < sprites.length; i++) {
sprites[i].mesh = mesh;
sprites[i].first_index = i*6;
sprites[i].num_indices = 6;
}
return;
var groups = [];
var group = {image:sprites[0].image, first_index:0};
var count = 1;
for (var i = 1; i < sprites.length; i++) {
if (sprites[i].image === group.image) {
count++;
continue;
}
group.num_indices = count*6;
var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices};
group = newgroup;
groups.push(group);
count=1;
}
group.num_indices = count*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)
{
var pass;
try{
if (!camera.target) {
main_color.width = main_depth.width = camera.size.x;
main_color.height = main_depth.height = 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:"dont_care", // if clear, crash on dx12
store:"dont_care",
stencil_load:"dont_care", // ditto
stencil_store:"dont_care",
stencil_clear:0
}
};
}
var buffers = [];
buffers = buffers.concat(queue_sprite_mesh(render_queue));
var unique_meshes = [...new Set(render_queue.map(x => x.mesh))];
for (var q of unique_meshes)
buffers = buffers.concat([q.pos, q.color,q.uv,q.indices]);
buffers = buffers.concat(queue_sprite_mesh(hud_queue));
for (var q of hud_queue)
if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color,q.mesh.uv,q.mesh.indices]);
full_upload(buffers)
var pass = cmds.render_pass(camera.target);
var camera = prosperon.camera;
var pipeline;
var mesh;
var img;
var modelslot;
cmds.push_debug_group("draw")
for (var group of render_queue) {
if (pipeline != group.pipeline) {
pipeline = group.pipeline;
bind_pipeline(pass, pipeline);
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
if (typeof camslot !== 'undefined')
cmds.camera(camera, camslot);
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);
}
}
if (mesh != group.mesh) {
mesh = group.mesh;
bind_model(pass,pipeline,mesh);
}
if (group.image && img != group.image) {
img = group.image;
img.sampler = std_sampler;
bind_mat(pass,pipeline,{diffuse:img});
}
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
}
cmds.pop_debug_group()
cmds.push_debug_group("hud")
pipeline = undefined;
for (var group of hud_queue) {
if (pipeline != group.pipeline) {
pipeline = group.pipeline;
bind_pipeline(pass, pipeline);
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
if (typeof camslot !== 'undefined')
cmds.hud(camera.size, camslot);
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);
}
}
if (mesh != group.mesh) {
mesh = group.mesh;
bind_model(pass,pipeline,mesh);
}
if (group.image && img != group.image) {
img = group.image;
img.sampler = std_sampler;
bind_mat(pass,pipeline,{diffuse:img});
}
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
}
cmds.pop_debug_group();
} catch(e) {console.error(e); }
finally{
pass?.end();
render_queue = [];
hud_queue = [];
}
}
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;
}
prosperon.camera = {};
// If camera viewport is defined, will draw to the screen
// If target is defined, will render to a target, too
prosperon.camera.draw_rect = function(size)
{
var mode = this.presentation || "letterbox"
var vp = {
x:this.viewport.x,
y:1-this.viewport.y-this.viewport.height,
width:this.viewport.width,
height:this.viewport.height
}
var src_rect = {x:0,y:0,width:this.size.x,height:this.size.y}
var dst_rect = {x:vp.x*size.x,y:vp.y*size.y,width:vp.width*size.x,height:vp.height*size.y};
return mode_rect(src_rect,dst_rect,mode);
}
// Camera coordinates are [0,0]
prosperon.camera.screen2camera = function(pos)
{
var draw_rect = this.draw_rect(prosperon.window.size);
var ret = [pos.x-draw_rect.x, pos.y - draw_rect.y];
ret.x /= draw_rect.width;
ret.y /= draw_rect.height;
ret.y = 1 - ret.y;
return ret;
}
prosperon.camera.screen2hud = function(pos)
{
var cam = this.screen2camera(pos);
cam.x *= this.size.x;
cam.y *= this.size.y;
return cam;
}
prosperon.camera.screen2world = function(pos)
{
var hud = this.screen2hud(pos);
hud.x += this.transform.pos.x - this.size.x/2;
hud.y += this.transform.pos.y - this.size.y/2;
return hud;
}
var swaps = [];
function gpupresent()
{
try{
var cmds = render._main.acquire_cmd_buffer();
render_queue = sprites_to_queue(component.sprite_buckets()).concat(render_queue);
render_camera(cmds, prosperon.camera);
} catch(e) { console.error(e); } finally {
var swapchain_tex = cmds.acquire_swapchain();
if (!swapchain_tex)
cmds.cancel();
else {
var torect = prosperon.camera.draw_rect(prosperon.window.size);
torect.texture = swapchain_tex;
if (swapchain_tex) {
cmds.blit({
src: prosperon.camera.target.color_targets[0].texture,
dst: torect,
filter:"nearest",
load: "clear"
});
// imgui
cmds.push_debug_group("imgui")
imgui.prepend(cmds);
var pass = cmds.render_pass({
color_targets:[{texture:swapchain_tex}]});
imgui.endframe(cmds,pass);
pass.end();
cmds.pop_debug_group()
}
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];
prosperon.font = render.get_font('fonts/c64.ttf', 8);
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();
imgui.init(render._main, prosperon.window);
};
render.draw_sprites = true;
render.draw_particles = true;
render.draw_hud = true;
render.draw_gui = true;
render.draw_gizmos = true;
function insertion_sort(arr, cmp)
{
for (let i = 1; i < arr.length; i++) {
let key = arr[i]
let j = i - 1
while (j >= 0 && cmp(arr[j], key) > 0)
arr[j + 1] = arr[j--]
arr[j + 1] = key
}
return arr
}
function sprites_to_queue(sprites, ysort = false)
{
var pos = prosperon.camera.transform.pos;
var size = prosperon.camera.size;
var camrect = {
x:pos.x-size.x/2,
y:pos.y-size.y/2,
width:size.x,
height:size.y
};
var culled = sprite_qt.query(camrect)
if (culled.length == 0) return [];
//var culled = spritetree.find(prosperon.camera.pos, prosperon.camera.size);
//var culled = spritetree.find(prosperon.camera.transform.pos,prosperon.camera.size)
// var culled = os.cull_sprites(allsprites,prosperon.camera);
var cmd = render._main.make_sprite_queue(culled, prosperon.camera, sprite_pipeline);
return cmd;
var sprites = allsprites;
// for (var i = 0; i < sprites.length; i++)
// sprites[i].transform.clean();
var dirtysprites = allsprites.filter(x=>x.transform.dirty());
if (dirtysprites.length > 0) console.log(`there was ${dirtysprites.length} dirty sprites`)
// var sprites = os.cull_sprites(allsprites, prosperon.camera);
os.insertion_sort(sprites,render._main.sort_sprite)
// sprites.sort(render._main.sort_sprite)
var mesh = render._main.make_sprite_mesh(sprites);
var queue = [];
var image;
var first_index = 0;
var count = 0;
for (var i = 0; i < sprites.length; i++) {
var spr = sprites[i];
if (spr.image !== image) {
if (count > 0) queue.push({
type:'geometry',
mesh,
pipeline:sprite_pipeline,
image,
first_index,
num_indices:count*6
});
image = spr.image;
first_index = i*6;
count = 1;
} else count++
}
if (count > 0) queue.push({
type:'geometry',
mesh,
pipeline:sprite_pipeline,
image,
first_index,
num_indices: count*6
});
return queue;
}
render.circle = function render_circle(pos, radius, color, inner_radius = 1, pipeline = circle_pipeline) {
render.rectangle({x:pos.x, y:pos.y, width:radius*2,height:radius*2}, color, circle_pipeline);
return;
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);
};
// 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, pipeline = rect_pipeline) {
var mesh = os.make_line_prim(points,thickness, 0,0,color);
current_queue.push({
type: 'geometry',
mesh,
pipeline,
first_index:0,
num_indices:mesh.num_indices
});
};
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 = sprite_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 = sprite_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, pipeline = rect_pipeline) {
var T = os.make_transform();
T.rect(rect);
current_queue.push({
type:'sprite',
transform:T,
color,
pipeline
});
};
render.particles = function render_particles(emitter, pipeline = sprite_pipeline)
{
var mesh = render._main.make_sprite_mesh(emitter.particles);
if (mesh.num_indices === 0) return;
current_queue.push({
type:'geometry',
mesh,
image:emitter.diffuse,
pipeline,
first_index:0,
num_indices:mesh.num_indices
});
}
render.text = function text(text, rect, font = prosperon.font, size = 0, color = Color.white, wrap = 0, pipeline = sprite_pipeline) {
if (typeof font === 'string')
font = render.get_font(font)
var mesh = os.make_text_buffer(text, rect, 0, color, wrap, font);
// full_upload(mesh)
current_queue.push({
type: 'geometry',
mesh,
image: font,
texture:font.texture,
pipeline,
first_index:0,
num_indices:mesh.num_indices
});
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
};
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 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)
{
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];
}
function tile(image, rect = [0,0], color = Color.white, repeat = {})
{
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];
render._main.tile(image.texture, rect, image.rect, 1);
return;
}
render.image = function image(image, rect = [0,0], rotation = 0, color = Color.white, pipeline = sprite_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);
current_queue.push({
type: 'sprite',
transform: T,
image,
pipeline,
color
});
};
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);
}
var tile_def = {repeat_x:true, repeat_y:true};
render.tile = function(image, rect, color = Color.white, tile = tile_def, pipeline = sprite_pipeline)
{
if (!image) throw Error ('Need an image to render.')
if (typeof image === "string")
image = game.texture(image);
var mesh = render._main.tile(image.texture, {x:0,y:0,width:image.texture.width,height:image.texture.height}, rect, tile);
current_queue.push({
type:'geometry',
mesh,
image,
pipeline,
first_index:0,
num_indices:mesh.num_indices
});
}
// slice is given in pixels
var slice9_info = {
tile_top:true,
tile_bottom:true,
tile_left:true,
tile_right:true,
tile_center_x:true,
tile_center_right:true
};
render.slice9 = function slice9(image, rect = [0,0], slice = 0, color = Color.white, info = slice9_info, pipeline = sprite_pipeline) {
if (!image) throw Error ('Need an image to render.')
if (typeof image === "string")
image = game.texture(image);
var mesh = render._main.slice9(image.texture, rect, clay.normalizeSpacing(slice), info);
current_queue.push({
type: 'geometry',
mesh,
image,
pipeline,
first_index:0,
num_indices:mesh.num_indices
});
// render.image(image,rect,undefined,color);
// render._main.slice9(image.texture, rect, slice);
};
var textssbos = [];
var tdraw = 0;
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
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() {
imgui.newframe();
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);
};
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/240;
var last_frame_time = 0;
// Ran once per frame
var fpses = [];
prosperon.process = function process() {
var now = profile.now();
var dt = now - last_frame_time;
fpses.push(dt);
if (fpses.length > 300) fpses.shift();
console.log(1/(vector.sum(fpses)/fpses.length))
if (dt < waittime) os.sleep(waittime-dt);
last_frame_time = profile.now();
layout.newframe();
// check for hot reloading
if (dmon) dmon.poll(dmon_cb);
var dt = last_frame_time - frame_t;
frame_t = last_frame_time;
try {
game.engine_input(e => {
prosperon[e.type]?.(e);
});
} catch(e) { console.error(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);
}
*/
}
current_queue = render_queue;
try { prosperon.draw(); } catch(e) { console.error(e) }
for (var e of all_emitters())
render.particles(e);
current_queue = hud_queue;
try { prosperon.hud(); } catch(e) { console.error(e) }
try { imgui_fn(); } catch(e) { console.error(e) }
render._main.present();
tracy.end_frame();
};
return { render };