sprites don't warp
This commit is contained in:
@@ -1,655 +0,0 @@
|
||||
var render = {}
|
||||
|
||||
var io = use('io')
|
||||
var controller = use('controller')
|
||||
var tracy = use('tracy')
|
||||
var graphics = use('graphics')
|
||||
var imgui = use('imgui')
|
||||
var transform = use('transform')
|
||||
var color = use('color')
|
||||
|
||||
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: "zero", // 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 sprite_pipeline = Object.create(base_pipeline);
|
||||
sprite_pipeline.blend = {
|
||||
enabled:true,
|
||||
src_rgb: "src_alpha",
|
||||
dst_rgb: "one_minus_src_alpha",
|
||||
op_rgb: "add", // add/sub/rev_sub/min/max
|
||||
src_alpha: "one",
|
||||
dst_alpha: "zero",
|
||||
op_alpha: "add"
|
||||
};
|
||||
|
||||
var context;
|
||||
|
||||
sprite_pipeline.target = {
|
||||
color_targets: [{
|
||||
format:"rgba8",
|
||||
blend:sprite_pipeline.blend
|
||||
}],
|
||||
depth: "d32 float s8"
|
||||
};
|
||||
|
||||
var driver = "vulkan"
|
||||
switch(os.platform()) {
|
||||
case "Linux":
|
||||
driver = "vulkan"
|
||||
break
|
||||
case "Windows":
|
||||
// driver = "direct3d12"
|
||||
driver = "vulkan"
|
||||
break
|
||||
case "macOS":
|
||||
driver = "metal"
|
||||
break
|
||||
}
|
||||
|
||||
var unit_transform = new transform;
|
||||
|
||||
var cur = {};
|
||||
cur.images = [];
|
||||
cur.samplers = [];
|
||||
|
||||
var tbuffer;
|
||||
function full_upload(buffers) {
|
||||
var cmds = context.acquire_cmd_buffer();
|
||||
tbuffer = context.upload(cmds, buffers, tbuffer);
|
||||
cmds.submit();
|
||||
}
|
||||
|
||||
function bind_pipeline(pass, pipeline) {
|
||||
make_pipeline(pipeline)
|
||||
pass.bind_pipeline(pipeline.gpu)
|
||||
pass.pipeline = pipeline;
|
||||
}
|
||||
|
||||
var main_pass;
|
||||
|
||||
var cornflower = [62/255,96/255,113/255,1];
|
||||
|
||||
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 null;
|
||||
}
|
||||
|
||||
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;
|
||||
default: throw new Error("Unknown or unsupported float-based type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
var sprite_model_ubo = {
|
||||
model: unit_transform,
|
||||
color: [1,1,1,1]
|
||||
};
|
||||
|
||||
var shader_cache = {};
|
||||
var shader_times = {};
|
||||
|
||||
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 = context.make_pipeline(pipeline);
|
||||
return;
|
||||
}
|
||||
|
||||
var inputs = refl.inputs
|
||||
var buffer_descriptions = []
|
||||
var attributes = []
|
||||
|
||||
// 2) Build buffer + attribute for each reflection input
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var inp = inputs[i]
|
||||
var typeStr = inp.type
|
||||
var nameStr = (inp.name || "").toUpperCase()
|
||||
var pitch = 4
|
||||
var fmt = "float1"
|
||||
|
||||
if (typeStr == "vec2") {
|
||||
pitch = 8
|
||||
fmt = "float2"
|
||||
} else if (typeStr == "vec3") {
|
||||
pitch = 12
|
||||
fmt = "float3"
|
||||
} else if (typeStr == "vec4") {
|
||||
if (nameStr.indexOf("COLOR") >= 0) {
|
||||
pitch = 16
|
||||
fmt = "color"
|
||||
} else {
|
||||
pitch = 16
|
||||
fmt = "float4"
|
||||
}
|
||||
}
|
||||
|
||||
buffer_descriptions.push({
|
||||
slot: i,
|
||||
pitch: pitch,
|
||||
input_rate: "vertex",
|
||||
instance_step_rate: 0,
|
||||
name:inp.name.split(".").pop()
|
||||
})
|
||||
|
||||
attributes.push({
|
||||
location: inp.location,
|
||||
buffer_slot: i,
|
||||
format: fmt,
|
||||
offset: 0
|
||||
})
|
||||
}
|
||||
|
||||
pipeline.vertex_buffer_descriptions = buffer_descriptions
|
||||
pipeline.vertex_attributes = attributes
|
||||
|
||||
pipeline.gpu = context.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"
|
||||
}
|
||||
|
||||
shader.gpu = context.make_shader(shader)
|
||||
shader.reflection = refl;
|
||||
shader_cache[file] = shader
|
||||
shader.file = sh_file
|
||||
return shader
|
||||
}
|
||||
|
||||
var render_queue = [];
|
||||
var hud_queue = [];
|
||||
|
||||
var current_queue = render_queue;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
function upload_model(model) {
|
||||
var bufs = [];
|
||||
for (var i in model) {
|
||||
if (typeof model[i] != 'object') continue;
|
||||
bufs.push(model[i]);
|
||||
}
|
||||
context.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;
|
||||
// The code below is an alternate approach to grouping by image. Currently not in use.
|
||||
/*
|
||||
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;
|
||||
delete camera.target // TODO: HORRIBLE
|
||||
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: context.texture(main_color),
|
||||
mip_level:0,
|
||||
layer: 0,
|
||||
load:"clear",
|
||||
store:"store",
|
||||
clear: cornflower
|
||||
}],
|
||||
depth_stencil: {
|
||||
texture: context.texture(main_depth),
|
||||
clear:1,
|
||||
load:"dont_care",
|
||||
store:"dont_care",
|
||||
stencil_load:"dont_care",
|
||||
stencil_store:"dont_care",
|
||||
stencil_clear:0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var buffers = [];
|
||||
buffers = buffers.concat(graphics.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(graphics.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 pipeline = sprite_pipeline;
|
||||
bind_pipeline(pass,pipeline);
|
||||
|
||||
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
||||
if (camslot != null)
|
||||
cmds.camera(camera, camslot);
|
||||
|
||||
modelslot = get_pipeline_ubo_slot(pipeline, "model");
|
||||
if (modelslot != null) {
|
||||
var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo);
|
||||
cmds.push_vertex_uniform_data(modelslot, ubo);
|
||||
}
|
||||
|
||||
var mesh;
|
||||
var img;
|
||||
var modelslot;
|
||||
|
||||
cmds.push_debug_group("draw")
|
||||
for (var group of render_queue) {
|
||||
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")
|
||||
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
||||
if (camslot != null)
|
||||
cmds.hud(camera.size, camslot);
|
||||
|
||||
for (var group of hud_queue) {
|
||||
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();
|
||||
|
||||
pass?.end();
|
||||
|
||||
render_queue = [];
|
||||
hud_queue = [];
|
||||
}
|
||||
|
||||
var swaps = [];
|
||||
render.present = function() {
|
||||
os.clean_transforms();
|
||||
var cmds = context.acquire_cmd_buffer();
|
||||
render_camera(cmds, prosperon.camera);
|
||||
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"
|
||||
});
|
||||
|
||||
if (imgui) { // draws any imgui commands present
|
||||
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 stencil_write = {
|
||||
compare: "always",
|
||||
fail_op: "replace",
|
||||
depth_fail_op: "replace",
|
||||
pass_op: "replace"
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
// objects by default draw where the stencil buffer is 0
|
||||
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"
|
||||
};
|
||||
|
||||
function mask(image, pos, scale, rotation = 0, ref = 1) {
|
||||
if (typeof image == 'string')
|
||||
image = graphics.texture(image);
|
||||
|
||||
var tex = image.texture;
|
||||
if (scale) scale = scale.div([tex.width,tex.height]);
|
||||
else scale = [1,1,1]
|
||||
|
||||
var pipe = stencil_writer(ref);
|
||||
render.use_shader('sprite.cg', pipe);
|
||||
var t = new transform;
|
||||
t.trs(pos, null, scale);
|
||||
set_model(t);
|
||||
render.use_mat({
|
||||
diffuse:image.texture,
|
||||
rect: image.rect,
|
||||
shade: color.white
|
||||
});
|
||||
render.draw(shape.quad);
|
||||
}
|
||||
|
||||
render.viewport = function(rect) {
|
||||
context.viewport(rect);
|
||||
}
|
||||
|
||||
render.scissor = function(rect) {
|
||||
render.viewport(rect)
|
||||
}
|
||||
|
||||
var std_sampler
|
||||
|
||||
if (tracy) tracy.gpu_init()
|
||||
|
||||
render.queue = function(cmd) {
|
||||
if (Array.isArray(cmd))
|
||||
for (var i of cmd) current_queue.push(i)
|
||||
else
|
||||
current_queue.push(cmd)
|
||||
}
|
||||
|
||||
render.setup_draw = function() {
|
||||
current_queue = render_queue;
|
||||
prosperon.draw();
|
||||
}
|
||||
|
||||
render.setup_hud = function() {
|
||||
current_queue = hud_queue;
|
||||
prosperon.hud();
|
||||
}
|
||||
|
||||
render.initialize = function(config)
|
||||
{
|
||||
var default_conf = {
|
||||
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
|
||||
high_dpi:0,
|
||||
alpha:1,
|
||||
fullscreen:0,
|
||||
sample_count:1,
|
||||
enable_clipboard:true,
|
||||
enable_dragndrop: true,
|
||||
max_dropped_files: 1,
|
||||
swap_interval: 1,
|
||||
name: "Prosperon",
|
||||
version:prosperon.version + "-" + prosperon.revision,
|
||||
identifier: "world.pockle.prosperon",
|
||||
creator: "Pockle World LLC",
|
||||
copyright: "Copyright Pockle World 2025",
|
||||
type: "game",
|
||||
url: "https://prosperon.dev"
|
||||
}
|
||||
|
||||
config.__proto__ = default_conf
|
||||
|
||||
prosperon.camera = use('ext/camera').make()
|
||||
prosperon.camera.size = [config.width,config.height]
|
||||
|
||||
prosperon.window = prosperon.engine_start(config)
|
||||
|
||||
context = prosperon.window.make_gpu(false,driver)
|
||||
context.window = prosperon.window
|
||||
context.claim_window(prosperon.window)
|
||||
context.set_swapchain('sdr', 'vsync')
|
||||
|
||||
if (imgui) imgui.init(context, prosperon.window)
|
||||
|
||||
shader_type = context.shader_format()[0];
|
||||
|
||||
std_sampler = context.make_sampler({
|
||||
min_filter: "nearest",
|
||||
mag_filter: "nearest",
|
||||
mipmap_mode: "nearest",
|
||||
address_mode_u: "repeat",
|
||||
address_mode_v: "repeat",
|
||||
address_mode_w: "repeat"
|
||||
});
|
||||
}
|
||||
|
||||
return render
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
var graphics = use('graphics')
|
||||
var color = use('color')
|
||||
|
||||
var sprite = {
|
||||
image: null,
|
||||
set color(x) { this._sprite.color = x; },
|
||||
get color() { return this._sprite.color; },
|
||||
anim_speed: 1,
|
||||
play(str, loop = true, reverse = false, fn) {
|
||||
if (!this.animset) {
|
||||
fn?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof str == 'string') {
|
||||
if (!this.animset[str]) {
|
||||
fn?.();
|
||||
return;
|
||||
}
|
||||
this.anim = this.animset[str];
|
||||
}
|
||||
|
||||
var playing = this.anim;
|
||||
|
||||
var stop;
|
||||
|
||||
this.del_anim?.();
|
||||
this.del_anim = () => {
|
||||
this.del_anim = null;
|
||||
advance = null;
|
||||
stop?.();
|
||||
};
|
||||
|
||||
var f = 0;
|
||||
if (reverse) f = playing.frames.length - 1;
|
||||
|
||||
var advance = (time) => {
|
||||
var done = false;
|
||||
if (reverse) {
|
||||
f = (((f - 1) % playing.frames.length) + playing.frames.length) % playing.frames.length;
|
||||
if (f == playing.frames.length - 1) done = true;
|
||||
} else {
|
||||
f = (f + 1) % playing.frames.length;
|
||||
if (f == 0) done = true;
|
||||
}
|
||||
|
||||
this.image = playing.frames[f];
|
||||
|
||||
if (done) {
|
||||
// notify requestor
|
||||
fn?.();
|
||||
if (!loop) {
|
||||
this?.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return playing.frames[f].time/this.anim_speed;
|
||||
}
|
||||
stop = this.delay(advance, playing.frames[f].time/this.anim_speed);
|
||||
advance();
|
||||
},
|
||||
stop() {
|
||||
this.del_anim?.();
|
||||
},
|
||||
set path(p) {
|
||||
var image = graphics.texture(p);
|
||||
if (!image) {
|
||||
log.warn(`Could not find image ${p}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._p = p;
|
||||
|
||||
this.del_anim?.();
|
||||
if (image.texture)
|
||||
this.image = image;
|
||||
else if (image.frames) {
|
||||
// It's an animation
|
||||
this.anim = image;
|
||||
this.image = image.frames[0];
|
||||
this.animset = [this.anim]
|
||||
this.play()
|
||||
} else {
|
||||
// Maybe an animset; try to grab the first one
|
||||
for (var anim in image) {
|
||||
if (image[anim].frames) {
|
||||
this.anim = image[anim];
|
||||
this.image = image[anim].frames[0];
|
||||
this.animset = image;
|
||||
this.play()
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.transform.scale = this.image.texture.dim //[this.image.texture.width, this.image.texture.height]
|
||||
this._sprite.set_image(this.image)
|
||||
},
|
||||
get path() {
|
||||
return this._p;
|
||||
},
|
||||
garbage: function() {
|
||||
this.del_anim?.();
|
||||
this.anim = null;
|
||||
tree.delete(this._sprite)
|
||||
this.transform.parent = null
|
||||
for (var t of this.transform.children())
|
||||
t.parent = null
|
||||
delete this.transform.sprite
|
||||
delete this._sprite
|
||||
// log.console("CLEARED SPRITE")
|
||||
},
|
||||
anchor: [0, 0],
|
||||
set layer(v) { this._sprite.layer = v; },
|
||||
get layer() { return this._sprite.layer; },
|
||||
pick() {
|
||||
return this;
|
||||
},
|
||||
boundingbox() {
|
||||
return Object.freeze(this._sprite.rect) // freeze so it can't be modified on the outside
|
||||
}
|
||||
};
|
||||
|
||||
sprite.setanchor = function (anch) {
|
||||
var off = [0, 0];
|
||||
switch (anch) {
|
||||
case "ll":
|
||||
break;
|
||||
case "lm":
|
||||
off = [-0.5, 0];
|
||||
break;
|
||||
case "lr":
|
||||
off = [-1, 0];
|
||||
break;
|
||||
case "ml":
|
||||
off = [0, -0.5];
|
||||
break;
|
||||
case "mm":
|
||||
off = [-0.5, -0.5];
|
||||
break;
|
||||
case "mr":
|
||||
off = [-1, -0.5];
|
||||
break;
|
||||
case "ul":
|
||||
off = [0, -1];
|
||||
break;
|
||||
case "um":
|
||||
off = [-0.5, -1];
|
||||
break;
|
||||
case "ur":
|
||||
off = [-1, -1];
|
||||
break;
|
||||
}
|
||||
this.anchor = off;
|
||||
this.pos = this.dimensions().scale(off);
|
||||
};
|
||||
|
||||
sprite.inputs = {};
|
||||
sprite.inputs.kp9 = function () {
|
||||
this.setanchor("ll");
|
||||
};
|
||||
sprite.inputs.kp8 = function () {
|
||||
this.setanchor("lm");
|
||||
};
|
||||
sprite.inputs.kp7 = function () {
|
||||
this.setanchor("lr");
|
||||
};
|
||||
sprite.inputs.kp6 = function () {
|
||||
this.setanchor("ml");
|
||||
};
|
||||
sprite.inputs.kp5 = function () {
|
||||
this.setanchor("mm");
|
||||
};
|
||||
sprite.inputs.kp4 = function () {
|
||||
this.setanchor("mr");
|
||||
};
|
||||
sprite.inputs.kp3 = function () {
|
||||
this.setanchor("ur");
|
||||
};
|
||||
sprite.inputs.kp2 = function () {
|
||||
this.setanchor("um");
|
||||
};
|
||||
sprite.inputs.kp1 = function () {
|
||||
this.setanchor("ul");
|
||||
};
|
||||
|
||||
var rtree = use('rtree')
|
||||
|
||||
var tree = new rtree
|
||||
sprite.tree = tree;
|
||||
|
||||
var IN = Symbol()
|
||||
|
||||
sprite.t_hook = function() {
|
||||
var msp = this.sprite._sprite;
|
||||
if (this[IN])
|
||||
tree.delete(msp);
|
||||
msp.rect = this.torect()
|
||||
msp.set_affine(this)
|
||||
tree.add(msp)
|
||||
this[IN] = true
|
||||
}
|
||||
|
||||
Object.mixin(sprite,use("transform"))
|
||||
|
||||
sprite.to_queue = function(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.tree.query(camrect)
|
||||
if (culled.length == 0) return [];
|
||||
var cmd = graphics.make_sprite_queue(culled, prosperon.camera, null, 1);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
return sprite;
|
||||
|
||||
---
|
||||
|
||||
var color = use('color')
|
||||
var transform = use('transform')
|
||||
var sprite = use('sprite')
|
||||
|
||||
this.transform = new transform;
|
||||
if (this.overling.transform)
|
||||
this.transform.parent = this.overling.transform;
|
||||
|
||||
this.transform.change_hook = $.t_hook;
|
||||
var msp = new sprite
|
||||
this._sprite = msp;
|
||||
msp.color = color.white;
|
||||
this.transform.sprite = this
|
||||
|
||||
|
||||
@@ -1,504 +0,0 @@
|
||||
var os = use('os');
|
||||
var io = use('io');
|
||||
var transform = use('transform');
|
||||
var rasterize = use('rasterize');
|
||||
var time = use('time')
|
||||
var tilemap = use('tilemap')
|
||||
var json = use('json')
|
||||
|
||||
// Frame timing variables
|
||||
var framerate = 60
|
||||
|
||||
var game = args[0]
|
||||
|
||||
io.writepath(game)
|
||||
|
||||
var video
|
||||
|
||||
//var cnf = use('accio/config')
|
||||
|
||||
$_.start(e => {
|
||||
if (e.type != 'greet') return
|
||||
video = e.actor
|
||||
graphics = use('graphics', video)
|
||||
send(video, {kind:"window", op:"makeRenderer"}, e => {
|
||||
$_.start(e => {
|
||||
if (gameactor) return
|
||||
gameactor = e.actor
|
||||
$_.couple(gameactor)
|
||||
start_pipeline()
|
||||
}, args[0], $_, video)
|
||||
})
|
||||
}, 'prosperon/sdl_video', {})
|
||||
|
||||
var geometry = use('geometry')
|
||||
var dmon = use('dmon')
|
||||
var res = use('resources')
|
||||
|
||||
// Start watching for file changes
|
||||
dmon.watch('.')
|
||||
|
||||
var camera = {}
|
||||
|
||||
function updateCameraMatrix(cam) {
|
||||
def win_w = logical.width
|
||||
def win_h = logical.height
|
||||
def view_w = (cam.size?.[0] ?? win_w) / cam.zoom
|
||||
def view_h = (cam.size?.[1] ?? win_h) / cam.zoom
|
||||
|
||||
def ox = cam.pos[0] - view_w * (cam.anchor?.[0] ?? 0)
|
||||
def oy = cam.pos[1] - view_h * (cam.anchor?.[1] ?? 0)
|
||||
|
||||
def vx = (cam.viewport?.x ?? 0) * win_w
|
||||
def vy = (cam.viewport?.y ?? 0) * win_h
|
||||
def vw = (cam.viewport?.width ?? 1) * win_w
|
||||
def vh = (cam.viewport?.height ?? 1) * win_h
|
||||
|
||||
def sx = vw / view_w
|
||||
def sy = vh / view_h // flip-Y later
|
||||
|
||||
/* affine matrix that SDL wants (Y going down) */
|
||||
cam.a = sx
|
||||
cam.c = vx - sx * ox
|
||||
cam.e = -sy // <-- minus = flip Y
|
||||
cam.f = vy + vh + sy * oy
|
||||
|
||||
/* convenience inverses */
|
||||
cam.ia = 1 / cam.a
|
||||
cam.ic = -cam.c / cam.a
|
||||
cam.ie = 1 / cam.e
|
||||
cam.if = -cam.f / cam.e
|
||||
|
||||
camera = cam
|
||||
}
|
||||
|
||||
//---- forward transform ----
|
||||
function worldToScreenPoint([x,y], camera) {
|
||||
return {
|
||||
x: camera.a * x + camera.c,
|
||||
y: camera.e * y + camera.f
|
||||
};
|
||||
}
|
||||
|
||||
//---- inverse transform ----
|
||||
function screenToWorldPoint(pos, camera) {
|
||||
return {
|
||||
x: camera.ia * pos[0] + camera.ic,
|
||||
y: camera.ie * pos[1] + camera.if
|
||||
};
|
||||
}
|
||||
|
||||
//---- rectangle (two corner) ----
|
||||
function worldToScreenRect({x,y,width,height}, camera) {
|
||||
// map bottom-left and top-right
|
||||
def x1 = camera.a * x + camera.c;
|
||||
def y1 = camera.e * y + camera.f;
|
||||
def x2 = camera.a * (x + width) + camera.c;
|
||||
def y2 = camera.e * (y + height) + camera.f;
|
||||
|
||||
return {
|
||||
x:Math.min(x1,x2),
|
||||
y:Math.min(y1,y2),
|
||||
width:Math.abs(x2-x1),
|
||||
height:Math.abs(y2-y1)
|
||||
}
|
||||
}
|
||||
|
||||
var graphics
|
||||
|
||||
var gameactor
|
||||
|
||||
var images = {}
|
||||
|
||||
var renderer_commands = []
|
||||
|
||||
var win_size = {width:500,height:500}
|
||||
var logical = {width:500,height:500}
|
||||
|
||||
// Convert high-level draw commands to low-level renderer commands
|
||||
function translate_draw_commands(commands) {
|
||||
if (!graphics) return
|
||||
|
||||
renderer_commands.length = 0
|
||||
|
||||
commands.forEach(function(cmd) {
|
||||
if (cmd.material && cmd.material.color) {
|
||||
renderer_commands.push({
|
||||
op: "set",
|
||||
prop: "drawColor",
|
||||
value: cmd.material.color
|
||||
})
|
||||
}
|
||||
|
||||
switch(cmd.cmd) {
|
||||
case "camera":
|
||||
updateCameraMatrix(cmd.camera, win_size.width, win_size.height)
|
||||
break
|
||||
|
||||
case "draw_rect":
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
// Handle rectangles with optional rounding and thickness
|
||||
if (cmd.opt && cmd.opt.radius && cmd.opt.radius > 0) {
|
||||
// Rounded rectangle
|
||||
var thickness = (cmd.opt.thickness == 0) ? 0 : (cmd.opt.thickness || 1)
|
||||
var raster_result = rasterize.round_rect(cmd.rect, cmd.opt.radius, thickness)
|
||||
|
||||
if (raster_result.type == 'rect') {
|
||||
renderer_commands.push({
|
||||
op: "fillRect",
|
||||
data: {rect: raster_result.data}
|
||||
})
|
||||
} else if (raster_result.type == 'rects') {
|
||||
raster_result.data.forEach(function(rect) {
|
||||
renderer_commands.push({
|
||||
op: "fillRect",
|
||||
data: {rect: rect}
|
||||
})
|
||||
})
|
||||
}
|
||||
} else if (cmd.opt && cmd.opt.thickness && cmd.opt.thickness > 0) {
|
||||
// Outlined rectangle
|
||||
var raster_result = rasterize.outline_rect(cmd.rect, cmd.opt.thickness)
|
||||
|
||||
if (raster_result.type == 'rect') {
|
||||
renderer_commands.push({
|
||||
op: "fillRect",
|
||||
data: {rect: raster_result.data}
|
||||
})
|
||||
} else if (raster_result.type == 'rects') {
|
||||
renderer_commands.push({
|
||||
op: "rects",
|
||||
data: {rects: raster_result.data}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
renderer_commands.push({
|
||||
op: "fillRect",
|
||||
data: {rect: cmd.rect}
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case "draw_circle":
|
||||
case "draw_ellipse":
|
||||
cmd.pos = worldToScreenPoint(cmd.pos, camera)
|
||||
// Rasterize ellipse to points or rects
|
||||
var radii = cmd.radii || [cmd.radius, cmd.radius]
|
||||
var raster_result = rasterize.ellipse(cmd.pos, radii, cmd.opt || {})
|
||||
|
||||
if (raster_result.type == 'points') {
|
||||
renderer_commands.push({
|
||||
op: "point",
|
||||
data: {points: raster_result.data}
|
||||
})
|
||||
} else if (raster_result.type == 'rects') {
|
||||
// Use 'rects' operation for multiple rectangles
|
||||
renderer_commands.push({
|
||||
op: "rects",
|
||||
data: {rects: raster_result.data}
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case "draw_line":
|
||||
renderer_commands.push({
|
||||
op: "line",
|
||||
data: {points: cmd.points.map(p => {
|
||||
var pt = worldToScreenPoint(p, camera)
|
||||
return [pt.x, pt.y]
|
||||
})}
|
||||
})
|
||||
break
|
||||
|
||||
case "draw_point":
|
||||
cmd.pos = worldToScreenPoint(cmd.pos, camera)
|
||||
renderer_commands.push({
|
||||
op: "point",
|
||||
data: {points: [cmd.pos]}
|
||||
})
|
||||
break
|
||||
|
||||
case "draw_image":
|
||||
var img = graphics.texture(cmd.image)
|
||||
var gpu = img.gpu
|
||||
if (!gpu) break
|
||||
|
||||
cmd.rect.width ??= img.width
|
||||
cmd.rect.height ??= img.height
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
|
||||
renderer_commands.push({
|
||||
op: "texture",
|
||||
data: {
|
||||
texture_id: gpu.id,
|
||||
dst: cmd.rect,
|
||||
src: img.rect
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case "draw_text":
|
||||
if (!cmd.text) break
|
||||
if (!cmd.pos) break
|
||||
var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera)
|
||||
var pos = {x: rect.x, y: rect.y}
|
||||
renderer_commands.push({
|
||||
op: "debugText",
|
||||
data: {
|
||||
pos,
|
||||
text: cmd.text
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case "draw_slice9":
|
||||
var img = graphics.texture(cmd.image)
|
||||
var gpu = img.gpu
|
||||
if (!gpu) break
|
||||
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
|
||||
renderer_commands.push({
|
||||
op: "texture9Grid",
|
||||
data: {
|
||||
texture_id: gpu.id,
|
||||
src: img.rect,
|
||||
leftWidth: cmd.slice,
|
||||
rightWidth: cmd.slice,
|
||||
topHeight: cmd.slice,
|
||||
bottomHeight: cmd.slice,
|
||||
scale: 1.0,
|
||||
dst: cmd.rect
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case "tilemap":
|
||||
// Group tiles by texture to batch draw calls
|
||||
var textureGroups = {}
|
||||
var tilePositions = []
|
||||
|
||||
// Collect all tiles and their positions
|
||||
tilemap.for(cmd.tilemap, (tile, {x,y}) => {
|
||||
if (tile) {
|
||||
tilePositions.push({tile, x, y})
|
||||
}
|
||||
})
|
||||
|
||||
// Group tiles by texture
|
||||
tilePositions.forEach(({tile, x, y}) => {
|
||||
var img = graphics.texture(tile)
|
||||
if (img && img.gpu) {
|
||||
var texId = img.gpu.id
|
||||
if (!textureGroups[texId]) {
|
||||
textureGroups[texId] = {
|
||||
texture: img,
|
||||
tiles: []
|
||||
}
|
||||
}
|
||||
textureGroups[texId].tiles.push({x, y, img})
|
||||
}
|
||||
})
|
||||
|
||||
// Generate draw commands for each texture group
|
||||
Object.keys(textureGroups).forEach(texId => {
|
||||
var group = textureGroups[texId]
|
||||
var tiles = group.tiles
|
||||
|
||||
// Create a temporary tilemap with only tiles from this texture
|
||||
// Apply tilemap position to the offset to shift the world coordinates
|
||||
var tempMap = {
|
||||
tiles: [],
|
||||
offset_x: cmd.tilemap.offset_x + (cmd.tilemap.pos.x / cmd.tilemap.size_x),
|
||||
offset_y: cmd.tilemap.offset_y + (cmd.tilemap.pos.y / cmd.tilemap.size_y),
|
||||
size_x: cmd.tilemap.size_x,
|
||||
size_y: cmd.tilemap.size_y
|
||||
}
|
||||
|
||||
// Build sparse array for this texture's tiles
|
||||
tiles.forEach(({x, y, img}) => {
|
||||
var arrayX = x - cmd.tilemap.offset_x
|
||||
var arrayY = y - cmd.tilemap.offset_y
|
||||
if (!tempMap.tiles[arrayX]) tempMap.tiles[arrayX] = []
|
||||
tempMap.tiles[arrayX][arrayY] = img
|
||||
})
|
||||
|
||||
// Generate geometry for this texture group
|
||||
var geom = geometry.tilemap_to_data(tempMap)
|
||||
geom.texture_id = parseInt(texId)
|
||||
|
||||
renderer_commands.push({
|
||||
op: "geometry_raw",
|
||||
data: geom
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return renderer_commands
|
||||
}
|
||||
|
||||
var parseq = use('parseq', $_.delay)
|
||||
|
||||
// Wrap `send(actor,msg,cb)` into a parseq “requestor”
|
||||
// • on success: cb(data) → value=data, reason=null
|
||||
// • on failure: cb(null,err)
|
||||
function rpc_req(actor, msg) {
|
||||
return (cb, _) => {
|
||||
send(actor, msg, data => {
|
||||
if (data.error)
|
||||
cb(null, data)
|
||||
else
|
||||
cb(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var pending_draw = null
|
||||
var pending_next = null
|
||||
var last_time = time.number()
|
||||
|
||||
var input = use('input')
|
||||
|
||||
var input_state = {
|
||||
poll: 1/framerate
|
||||
}
|
||||
|
||||
// 1) input runs completely independently
|
||||
function poll_input() {
|
||||
send(video, {kind:'input', op:'get'}, evs => {
|
||||
for (var ev of evs) {
|
||||
if (ev.type == 'window_pixel_size_changed') {
|
||||
win_size.width = ev.width
|
||||
win_size.height = ev.height
|
||||
}
|
||||
|
||||
if (ev.type == 'quit')
|
||||
$_.stop()
|
||||
|
||||
if (ev.type.includes('key')) {
|
||||
if (ev.key)
|
||||
ev.key = input.keyname(ev.key)
|
||||
}
|
||||
|
||||
if (ev.type.startsWith('mouse_'))
|
||||
ev.pos.y = -ev.pos.y + logical.height
|
||||
}
|
||||
|
||||
send(gameactor, evs)
|
||||
})
|
||||
$_.delay(poll_input, input_state.poll)
|
||||
}
|
||||
|
||||
// 2) helper to build & send a batch, then call done()
|
||||
function create_batch(draw_cmds, done) {
|
||||
def batch = [
|
||||
{op:'set', prop:'drawColor', value:{r:0.1,g:0.1,b:0.15,a:1}},
|
||||
{op:'clear'}
|
||||
]
|
||||
if (draw_cmds && draw_cmds.length)
|
||||
batch.push(...translate_draw_commands(draw_cmds))
|
||||
|
||||
batch.push(
|
||||
{op:'set', prop:'drawColor', value:{r:1,g:1,b:1,a:1}},
|
||||
{op:'imgui_render'},
|
||||
{op:'present'}
|
||||
)
|
||||
|
||||
send(video, {kind:'renderer', op:'batch', data:batch}, done)
|
||||
}
|
||||
|
||||
// File watching loop
|
||||
function poll_file_changes() {
|
||||
dmon.poll(e => {
|
||||
if (e.action == 'modify' || e.action == 'create') {
|
||||
// Check if it's an image file
|
||||
var ext = e.file.split('.').pop().toLowerCase()
|
||||
var imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tga', 'webp', 'qoi', 'ase', 'aseprite']
|
||||
|
||||
if (imageExts.includes(ext)) {
|
||||
// Try to find the full path for this image
|
||||
var possiblePaths = [
|
||||
e.file,
|
||||
e.root + e.file,
|
||||
res.find_image(e.file.split('/').pop().split('.')[0])
|
||||
].filter(p => p)
|
||||
|
||||
for (var path of possiblePaths) {
|
||||
graphics.tex_hotreload(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Schedule next poll in 0.5 seconds
|
||||
$_.delay(poll_file_changes, 0.5)
|
||||
}
|
||||
|
||||
// 3) kick off the very first update→draw
|
||||
function start_pipeline() {
|
||||
poll_input()
|
||||
poll_file_changes() // Start file watching loop
|
||||
send(gameactor, {kind:'update', dt:0}, () => {
|
||||
send(gameactor, {kind:'draw'}, cmds => {
|
||||
pending_draw = cmds
|
||||
render_step()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function render_step() {
|
||||
// a) Calculate actual dt since last frame
|
||||
def now = time.number()
|
||||
def dt = now - last_time
|
||||
last_time = now
|
||||
|
||||
// b) Send update with actual dt, then wait for draw response
|
||||
send(gameactor, {kind:'update', dt}, () => {
|
||||
send(gameactor, {kind:'draw'}, cmds => {
|
||||
// Only render after receiving draw commands
|
||||
pending_draw = cmds
|
||||
|
||||
// c) render the current frame
|
||||
create_batch(pending_draw, _ => { // time to render
|
||||
def frame_end = time.number()
|
||||
def wait_time = Math.max(0, (frame_end - now) - 1/framerate)
|
||||
|
||||
// e) Schedule next frame
|
||||
$_.delay(render_step, wait_time)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$_.receiver(e => {
|
||||
switch(e.op) {
|
||||
case 'resolution':
|
||||
log.console(json.encode(e))
|
||||
send(video, {
|
||||
kind:'renderer',
|
||||
op:'set',
|
||||
prop:'logicalPresentation',
|
||||
value: {...e}
|
||||
})
|
||||
logical.width = e.width
|
||||
logical.height = e.height
|
||||
break
|
||||
case 'window_size':
|
||||
send(video, {
|
||||
kind:'window',
|
||||
op:'set',
|
||||
data: {property: 'size', value: [e.width, e.height]}
|
||||
})
|
||||
break
|
||||
case 'framerate':
|
||||
// Allow setting target framerate dynamically
|
||||
if (e.fps && e.fps > 0) {
|
||||
framerate = e.fps
|
||||
input_state.poll = 1/framerate
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
@@ -48,7 +48,6 @@ function make_camera_pblob(camera) {
|
||||
return geometry.array_blob(mat);
|
||||
}
|
||||
|
||||
|
||||
var driver = "vulkan"
|
||||
switch(os.platform()) {
|
||||
case "Linux":
|
||||
@@ -689,8 +688,10 @@ cmd_fns.draw_image = function(cmd)
|
||||
else
|
||||
img = cmd.image
|
||||
|
||||
cmd.rect.width ??= img.width
|
||||
cmd.rect.height ??= img.height
|
||||
if (cmd.rect.width && !cmd.rect.height)
|
||||
cmd.rect.height = cmd.rect.width * img.height / img.width
|
||||
else if (cmd.rect.height && !cmd.rect.width)
|
||||
cmd.rect.width = cmd.rect.height * img.width / img.height
|
||||
|
||||
var geom = geometry.make_rect_quad(cmd.rect)
|
||||
geom.indices = geometry.make_quad_indices(1)
|
||||
|
||||
@@ -1,502 +0,0 @@
|
||||
var prosperon = {}
|
||||
|
||||
// This file is hard coded for the SDL renderer case
|
||||
|
||||
var video = use('sdl_video')
|
||||
var imgui = use('imgui')
|
||||
var surface = use('surface')
|
||||
|
||||
var default_window = {
|
||||
// Basic properties
|
||||
title: "Prosperon Window",
|
||||
width: 640,
|
||||
height: 480,
|
||||
|
||||
// Position - can be numbers or "centered"
|
||||
x: null, // SDL_WINDOWPOS_null by default
|
||||
y: null, // SDL_WINDOWPOS_null by default
|
||||
|
||||
// Window behavior flags
|
||||
resizable: true,
|
||||
fullscreen: false,
|
||||
hidden: false,
|
||||
borderless: false,
|
||||
alwaysOnTop: false,
|
||||
minimized: false,
|
||||
maximized: false,
|
||||
|
||||
// Input grabbing
|
||||
mouseGrabbed: false,
|
||||
keyboardGrabbed: false,
|
||||
|
||||
// Display properties
|
||||
highPixelDensity: false,
|
||||
transparent: false,
|
||||
opacity: 1.0, // 0.0 to 1.0
|
||||
|
||||
// Focus behavior
|
||||
notFocusable: false,
|
||||
|
||||
// Special window types (mutually exclusive)
|
||||
utility: false, // Utility window (not in taskbar)
|
||||
tooltip: false, // Tooltip window (requires parent)
|
||||
popupMenu: false, // Popup menu window (requires parent)
|
||||
|
||||
// Graphics API flags (let SDL choose if not specified)
|
||||
opengl: false, // Force OpenGL context
|
||||
vulkan: false, // Force Vulkan context
|
||||
metal: false, // Force Metal context (macOS)
|
||||
|
||||
// Advanced properties
|
||||
parent: null, // Parent window for tooltips/popups/modal
|
||||
modal: false, // Modal to parent window (requires parent)
|
||||
externalGraphicsContext: false, // Use external graphics context
|
||||
|
||||
// Input handling
|
||||
textInput: true, // Enable text input on creation
|
||||
}
|
||||
|
||||
var win_config = arg[0] || {}
|
||||
win_config.__proto__ = default_window
|
||||
|
||||
var window = new video.window(win_config)
|
||||
var renderer = window.make_renderer()
|
||||
|
||||
// Initialize ImGui with the window and renderer
|
||||
imgui.init(window, renderer)
|
||||
imgui.newframe()
|
||||
|
||||
var os = use('os');
|
||||
var io = use('io');
|
||||
var rasterize = use('rasterize');
|
||||
var time = use('time')
|
||||
var tilemap = use('tilemap')
|
||||
var geometry = use('geometry')
|
||||
var res = use('resources')
|
||||
var input = use('input')
|
||||
|
||||
var graphics = use('graphics')
|
||||
|
||||
var camera = {}
|
||||
|
||||
function updateCameraMatrix(cam) {
|
||||
def win_w = logical.width
|
||||
def win_h = logical.height
|
||||
def view_w = (cam.size?.[0] ?? win_w) / cam.zoom
|
||||
def view_h = (cam.size?.[1] ?? win_h) / cam.zoom
|
||||
|
||||
def ox = cam.pos[0] - view_w * (cam.anchor?.[0] ?? 0)
|
||||
def oy = cam.pos[1] - view_h * (cam.anchor?.[1] ?? 0)
|
||||
|
||||
def vx = (cam.viewport?.x ?? 0) * win_w
|
||||
def vy = (cam.viewport?.y ?? 0) * win_h
|
||||
def vw = (cam.viewport?.width ?? 1) * win_w
|
||||
def vh = (cam.viewport?.height ?? 1) * win_h
|
||||
|
||||
def sx = vw / view_w
|
||||
def sy = vh / view_h // flip-Y later
|
||||
|
||||
/* affine matrix that SDL wants (Y going down) */
|
||||
cam.a = sx
|
||||
cam.c = vx - sx * ox
|
||||
cam.e = -sy // <-- minus = flip Y
|
||||
cam.f = vy + vh + sy * oy
|
||||
|
||||
/* convenience inverses */
|
||||
cam.ia = 1 / cam.a
|
||||
cam.ic = -cam.c / cam.a
|
||||
cam.ie = 1 / cam.e
|
||||
cam.if = -cam.f / cam.e
|
||||
|
||||
camera = cam
|
||||
}
|
||||
|
||||
//---- forward transform ----
|
||||
function worldToScreenPoint([x,y], camera) {
|
||||
return {
|
||||
x: camera.a * x + camera.c,
|
||||
y: camera.e * y + camera.f
|
||||
};
|
||||
}
|
||||
|
||||
//---- inverse transform ----
|
||||
function screenToWorldPoint(pos, camera) {
|
||||
return {
|
||||
x: camera.ia * pos[0] + camera.ic,
|
||||
y: camera.ie * pos[1] + camera.if
|
||||
};
|
||||
}
|
||||
|
||||
//---- rectangle (two corner) ----
|
||||
function worldToScreenRect({x,y,width,height}, camera) {
|
||||
// map bottom-left and top-right
|
||||
def x1 = camera.a * x + camera.c;
|
||||
def y1 = camera.e * y + camera.f;
|
||||
def x2 = camera.a * (x + width) + camera.c;
|
||||
def y2 = camera.e * (y + height) + camera.f;
|
||||
|
||||
return {
|
||||
x:Math.min(x1,x2),
|
||||
y:Math.min(y1,y2),
|
||||
width:Math.abs(x2-x1),
|
||||
height:Math.abs(y2-y1)
|
||||
}
|
||||
}
|
||||
|
||||
var gameactor
|
||||
|
||||
var images = {}
|
||||
|
||||
var renderer_commands = []
|
||||
|
||||
var win_size = {width:500,height:500}
|
||||
var logical = {width:500,height:500}
|
||||
|
||||
function get_img_gpu(img)
|
||||
{
|
||||
if (img.gpu) return img.gpu
|
||||
var surf = new surface(img.cpu)
|
||||
img.gpu = renderer.load_texture(surf)
|
||||
return img.gpu
|
||||
}
|
||||
|
||||
// Convert high-level draw commands to low-level renderer commands
|
||||
function translate_draw_commands(commands) {
|
||||
renderer_commands.length = 0
|
||||
|
||||
commands.forEach(function(cmd) {
|
||||
if (cmd.material && cmd.material.color && typeof cmd.material.color == 'object') {
|
||||
renderer.drawColor = cmd.material.color
|
||||
}
|
||||
switch(cmd.cmd) {
|
||||
case "camera":
|
||||
updateCameraMatrix(cmd.camera, win_size.width, win_size.height)
|
||||
break
|
||||
|
||||
case "draw_rect":
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
renderer.fillRect(cmd.rect)
|
||||
break
|
||||
|
||||
case "draw_line":
|
||||
var points = cmd.points.map(p => {
|
||||
var pt = worldToScreenPoint(p, camera)
|
||||
return[pt.x, pt.y]
|
||||
})
|
||||
renderer.line(points)
|
||||
break
|
||||
|
||||
case "draw_point":
|
||||
cmd.pos = worldToScreenPoint(cmd.pos, camera)
|
||||
renderer.point(cmd.pos)
|
||||
break
|
||||
|
||||
case "draw_image":
|
||||
var img = graphics.texture(cmd.image)
|
||||
var gpu = get_img_gpu(img)
|
||||
|
||||
if (!cmd.scale) cmd.scale = {x:1,y:1}
|
||||
cmd.rect.width ??= img.width
|
||||
cmd.rect.height ??= img.height
|
||||
cmd.rect.width = cmd.rect.width * cmd.scale.x
|
||||
cmd.rect.height = cmd.rect.height * cmd.scale.y
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
renderer.texture(
|
||||
gpu,
|
||||
img.rect,
|
||||
cmd.rect,
|
||||
0,
|
||||
{x:0,y:0}
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
case "draw_slice9":
|
||||
var img = graphics.texture(cmd.image)
|
||||
var gpu = get_img_gpu(img)
|
||||
if (!gpu) break
|
||||
var rect = worldToScreenRect(cmd.rect, camera)
|
||||
renderer.texture9Grid(
|
||||
gpu,
|
||||
img.rect,
|
||||
cmd.slice,
|
||||
cmd.slice,
|
||||
cmd.slice,
|
||||
cmd.slice,
|
||||
1,
|
||||
rect
|
||||
)
|
||||
break
|
||||
|
||||
case "draw_text":
|
||||
if (!cmd.text) break
|
||||
if (!cmd.pos) break
|
||||
|
||||
// Get font from the font string (e.g., "smalle.16")
|
||||
var font = graphics.get_font(cmd.font)
|
||||
if (!font.gpu) {
|
||||
var surf = new surface(font.surface)
|
||||
font.gpu = renderer.load_texture(surf)
|
||||
}
|
||||
var gpu = font.gpu
|
||||
|
||||
// Create text geometry buffer
|
||||
var text_mesh = graphics.make_text_buffer(
|
||||
cmd.text,
|
||||
{x: cmd.pos.x, y: cmd.pos.y},
|
||||
[cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a],
|
||||
cmd.wrap || 0,
|
||||
font
|
||||
)
|
||||
|
||||
if (!text_mesh) break
|
||||
|
||||
if (text_mesh.xy.length == 0) break
|
||||
|
||||
// Transform XY coordinates using camera matrix
|
||||
var camera_params = [camera.a, camera.c, camera.e, camera.f]
|
||||
var transformed_xy = geometry.transform_xy_blob(text_mesh.xy, camera_params)
|
||||
|
||||
// Create transformed geometry object
|
||||
var geom = {
|
||||
xy: transformed_xy,
|
||||
xy_stride: text_mesh.xy_stride,
|
||||
uv: text_mesh.uv,
|
||||
uv_stride: text_mesh.uv_stride,
|
||||
color: text_mesh.color,
|
||||
color_stride: text_mesh.color_stride,
|
||||
indices: text_mesh.indices,
|
||||
num_vertices: text_mesh.num_vertices,
|
||||
num_indices: text_mesh.num_indices,
|
||||
size_indices: text_mesh.size_indices
|
||||
}
|
||||
|
||||
renderer.geometry_raw(gpu, geom.xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices)
|
||||
break
|
||||
|
||||
case "tilemap":
|
||||
var geometryCommands = cmd.tilemap.draw()
|
||||
|
||||
for (var geomCmd of geometryCommands) {
|
||||
var img = graphics.texture(geomCmd.image)
|
||||
if (!img) continue
|
||||
|
||||
if (!img.gpu) {
|
||||
var surf = new surface(img.cpu)
|
||||
img.gpu = renderer.load_texture(surf)
|
||||
}
|
||||
var gpu = img.gpu
|
||||
|
||||
var geom = geomCmd.geometry
|
||||
|
||||
var camera_params = [camera.a, camera.c, camera.e, camera.f]
|
||||
var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params)
|
||||
|
||||
renderer.geometry_raw(gpu, transformed_xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices)
|
||||
}
|
||||
break
|
||||
|
||||
case "geometry":
|
||||
var gpu = get_img_gpu(cmd.image)
|
||||
log.console(json.encode(cmd))
|
||||
var geom = cmd.geometry
|
||||
|
||||
var camera_params = [camera.a, camera.c, camera.e, camera.f]
|
||||
var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params)
|
||||
|
||||
renderer.geometry_raw(gpu, transformed_xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return renderer_commands
|
||||
}
|
||||
|
||||
///// input /////
|
||||
var input_cb
|
||||
var input_rate = 1/60
|
||||
function poll_input() {
|
||||
var evs = input.get_events()
|
||||
|
||||
// Filter and transform events
|
||||
if (renderer && Array.isArray(evs)) {
|
||||
var filteredEvents = []
|
||||
var wantMouse = imgui.wantmouse()
|
||||
var wantKeys = imgui.wantkeys()
|
||||
|
||||
for (var i = 0; i < evs.length; i++) {
|
||||
var event = evs[i]
|
||||
var shouldInclude = true
|
||||
|
||||
// Filter mouse events if ImGui wants mouse input
|
||||
if (wantMouse && (event.type == 'mouse_motion' ||
|
||||
event.type == 'mouse_button_down' ||
|
||||
event.type == 'mouse_button_up' ||
|
||||
event.type == 'mouse_wheel')) {
|
||||
shouldInclude = false
|
||||
}
|
||||
|
||||
// Filter keyboard events if ImGui wants keyboard input
|
||||
if (wantKeys && (event.type == 'key_down' ||
|
||||
event.type == 'key_up' ||
|
||||
event.type == 'text_input' ||
|
||||
event.type == 'text_editing')) {
|
||||
shouldInclude = false
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
// Transform mouse coordinates from window to renderer coordinates
|
||||
if (event.pos && (event.type == 'mouse_motion' ||
|
||||
event.type == 'mouse_button_down' ||
|
||||
event.type == 'mouse_button_up' ||
|
||||
event.type == 'mouse_wheel')) {
|
||||
// Convert window coordinates to renderer logical coordinates
|
||||
var logicalPos = renderer.coordsFromWindow(event.pos)
|
||||
event.pos = logicalPos
|
||||
}
|
||||
// Handle drop events which also have position
|
||||
if (event.pos && (event.type == 'drop_file' ||
|
||||
event.type == 'drop_text' ||
|
||||
event.type == 'drop_position')) {
|
||||
var logicalPos = renderer.coordsFromWindow(event.pos)
|
||||
event.pos = logicalPos
|
||||
}
|
||||
|
||||
// Handle window events
|
||||
if (event.type == 'window_pixel_size_changed') {
|
||||
win_size.width = event.width
|
||||
win_size.height = event.height
|
||||
}
|
||||
|
||||
if (event.type == 'quit')
|
||||
$_.stop()
|
||||
|
||||
if (event.type.includes('key')) {
|
||||
if (event.key)
|
||||
event.key = input.keyname(event.key)
|
||||
}
|
||||
|
||||
if (event.type.startsWith('mouse_') && event.pos && event.pos.y)
|
||||
event.pos.y = -event.pos.y + logical.height
|
||||
|
||||
filteredEvents.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
evs = filteredEvents
|
||||
}
|
||||
|
||||
input_cb(evs)
|
||||
$_.delay(poll_input, input_rate)
|
||||
}
|
||||
|
||||
prosperon.input = function(fn)
|
||||
{
|
||||
input_cb = fn
|
||||
poll_input()
|
||||
}
|
||||
|
||||
// 2) helper to build & send a batch, then call done()
|
||||
prosperon.create_batch = function create_batch(draw_cmds, done) {
|
||||
renderer.drawColor = {r:0.1,g:0.1,b:0.15,a:1}
|
||||
renderer.clear()
|
||||
|
||||
if (draw_cmds && draw_cmds.length)
|
||||
var commands = translate_draw_commands(draw_cmds)
|
||||
|
||||
renderer.drawColor = {r:1,g:1,b:1,a:1}
|
||||
imgui.endframe(renderer)
|
||||
imgui.newframe()
|
||||
|
||||
renderer.present()
|
||||
|
||||
if (done) done()
|
||||
}
|
||||
|
||||
////////// dmon hot reload ////////
|
||||
function poll_file_changes() {
|
||||
dmon.poll(e => {
|
||||
if (e.action == 'modify' || e.action == 'create') {
|
||||
// Check if it's an image file
|
||||
var ext = e.file.split('.').pop().toLowerCase()
|
||||
var imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tga', 'webp', 'qoi', 'ase', 'aseprite']
|
||||
|
||||
if (imageExts.includes(ext)) {
|
||||
// Try to find the full path for this image
|
||||
var possiblePaths = [
|
||||
e.file,
|
||||
e.root + e.file,
|
||||
res.find_image(e.file.split('/').pop().split('.')[0])
|
||||
].filter(p => p)
|
||||
|
||||
for (var path of possiblePaths) {
|
||||
graphics.tex_hotreload(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Schedule next poll in 0.5 seconds
|
||||
$_.delay(poll_file_changes, 0.5)
|
||||
}
|
||||
|
||||
var dmon = use('dmon')
|
||||
prosperon.dmon = function()
|
||||
{
|
||||
dmon.watch('.')
|
||||
poll_file_changes()
|
||||
}
|
||||
|
||||
var window_cmds = {
|
||||
size(size) {
|
||||
window.size = size
|
||||
},
|
||||
}
|
||||
|
||||
prosperon.set_window = function(config)
|
||||
{
|
||||
for (var c in config)
|
||||
if (window_cmds[c]) window_cmds[c](config[c])
|
||||
}
|
||||
|
||||
var renderer_cmds = {
|
||||
resolution(e) {
|
||||
logical.width = e.width
|
||||
logical.height = e.height
|
||||
renderer.logicalPresentation = {...e}
|
||||
}
|
||||
}
|
||||
|
||||
prosperon.set_renderer = function(config)
|
||||
{
|
||||
for (var c in config)
|
||||
if (renderer_cmds[c]) renderer_cmds[c](config[c])
|
||||
}
|
||||
|
||||
prosperon.init = function() {
|
||||
// No longer needed since we initialize directly
|
||||
}
|
||||
|
||||
// Function to load textures directly to the renderer
|
||||
prosperon.load_texture = function(surface_data) {
|
||||
var surf = new surface(surface_data)
|
||||
if (!surf) return null
|
||||
|
||||
var tex = renderer.load_texture(surf)
|
||||
if (!tex) return null
|
||||
|
||||
// Set pixel mode to nearest for all textures
|
||||
tex.scaleMode = "nearest"
|
||||
|
||||
var tex_id = allocate_id()
|
||||
resources.texture[tex_id] = tex
|
||||
|
||||
return {
|
||||
id: tex_id,
|
||||
texture: tex,
|
||||
width: tex.width,
|
||||
height: tex.height
|
||||
}
|
||||
}
|
||||
|
||||
return prosperon
|
||||
@@ -230,75 +230,7 @@ graphics.texture = function texture(path) {
|
||||
if (!cache[id]) {
|
||||
var ipath = res.find_image(id)
|
||||
|
||||
// If not found normally, check in accio/32k folder
|
||||
if (!ipath) {
|
||||
// First check if we have a cached QOI version
|
||||
var cache_dir = '.prosperon/texture_cache'
|
||||
var qoi_cache_path = `${cache_dir}/${id}.qoi`
|
||||
|
||||
if (io.exists(qoi_cache_path)) {
|
||||
// Load the cached QOI file
|
||||
var qoi_bytes = io.slurpbytes(qoi_cache_path)
|
||||
var cached_img = qoi.decode(qoi_bytes)
|
||||
if (cached_img) {
|
||||
var result = new graphics.Image(cached_img)
|
||||
cache[id] = result
|
||||
return cache[id]
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the file in accio/32k with any extension
|
||||
var accio_base = `accio/32k/${id}`
|
||||
var found_path = null
|
||||
|
||||
// Check for common image extensions
|
||||
var extensions = ['', '.qoi', '.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.gif', '.ase', '.aseprite']
|
||||
for (var ext of extensions) {
|
||||
var test_path = accio_base + ext
|
||||
if (io.exists(test_path)) {
|
||||
found_path = test_path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (found_path) {
|
||||
// Load the 32k image
|
||||
var bytes = io.slurpbytes(found_path)
|
||||
var img_32k = decode_image(bytes, found_path.ext())
|
||||
|
||||
// Handle single surface images
|
||||
if (img_32k && img_32k.width && img_32k.pixels) {
|
||||
// Get the surface module for scaling
|
||||
var surface = use('surface')
|
||||
|
||||
var surf_4k = surface.scale(img_32k, {
|
||||
width: Math.floor(img_32k.width / 8),
|
||||
height: Math.floor(img_32k.height / 8),
|
||||
mode: 'linear'
|
||||
})
|
||||
|
||||
var qoi_data = qoi.encode(surf_4k)
|
||||
|
||||
if (qoi_data && qoi_data.pixels) {
|
||||
if (!io.exists(cache_dir)) {
|
||||
io.mkdir(cache_dir)
|
||||
}
|
||||
io.slurpwrite(qoi_cache_path, qoi_data.pixels)
|
||||
}
|
||||
|
||||
// Use the 4k version
|
||||
var result = new graphics.Image(surf_4k)
|
||||
cache[id] = result
|
||||
return cache[id]
|
||||
}
|
||||
|
||||
// For now, if it's not a simple surface, just return it as-is
|
||||
// (animations, etc. would need more complex handling)
|
||||
var result = create_image(found_path)
|
||||
cache[id] = result
|
||||
return cache[id]
|
||||
}
|
||||
|
||||
// If still not found, return notex
|
||||
return graphics.texture('notex')
|
||||
}
|
||||
@@ -378,18 +310,6 @@ graphics.texture = function texture(path) {
|
||||
return cached
|
||||
}
|
||||
|
||||
graphics.texture.total_size = function() {
|
||||
var size = 0
|
||||
// Not yet implemented, presumably sum of (texture.width * texture.height * 4) for images in RAM
|
||||
return size
|
||||
}
|
||||
|
||||
graphics.texture.total_vram = function() {
|
||||
var vram = 0
|
||||
// Not yet implemented, presumably sum of GPU memory usage
|
||||
return vram
|
||||
}
|
||||
|
||||
graphics.tex_hotreload = function tex_hotreload(file) {
|
||||
var basename = file.split('/').pop().split('.')[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user