transform handling

This commit is contained in:
2025-01-15 10:09:35 -06:00
parent aec3656b76
commit a24d4da3c2
8 changed files with 112 additions and 422 deletions

View File

@@ -1,321 +1,5 @@
var component = {};
class LooseQuadtree {
constructor(boundary, capacity=4, looseness=1.25) {
this.boundary = boundary // {x, y, width, height}
this.capacity = capacity // max items before subdivision
this.looseness = looseness // factor by which nodes expand
this.sprites = [] // sprite objects stored here
this.divided = false
// "loose boundary": expand our nodes bounding box
// so items that cross the boundary lines can be placed deeper
this.looseBounds = this.getLooseBounds(boundary)
}
// Expand boundary by the looseness factor (looseness >= 1)
getLooseBounds(boundary) {
// For each dimension, we expand half on each side
let marginW = (this.looseness - 1) * boundary.width / 2
let marginH = (this.looseness - 1) * boundary.height / 2
return {
x: boundary.x - marginW,
y: boundary.y - marginH,
width: boundary.width + marginW * 2,
height: boundary.height + marginH * 2
}
}
subdivide() {
let x = this.boundary.x
let y = this.boundary.y
let w = this.boundary.width / 2
let h = this.boundary.height / 2
this.northwest = new LooseQuadtree({x, y, width: w, height: h}, this.capacity, this.looseness)
this.northeast = new LooseQuadtree({x: x + w, y, width: w, height: h}, this.capacity, this.looseness)
this.southwest = new LooseQuadtree({x, y: y + h, width: w, height: h}, this.capacity, this.looseness)
this.southeast = new LooseQuadtree({x: x + w, y: y + h, width: w, height: h}, this.capacity, this.looseness)
this.divided = true
}
insert(sprite) {
let rect = sprite.rect;
// If outside *loose* bounds, ignore
if (!geometry.rect_intersects(this.looseBounds, rect)) return false
// If we have room and no subdivision, store it here
if (this.sprites.length < this.capacity && !this.divided) {
this.sprites.push(sprite)
return true
}
// Otherwise, subdivide if not already
if (!this.divided) this.subdivide()
// Try placing into children
if (this.northwest.insert(sprite)) return true
if (this.northeast.insert(sprite)) return true
if (this.southwest.insert(sprite)) return true
if (this.southeast.insert(sprite)) return true
// If it doesn't cleanly fit (overlaps multiple child boundaries),
// store at this level
this.sprites.push(sprite)
return true
}
query(range, found=[]) {
// If query doesn't intersect our *loose* boundary, no need to check further
if (!geometry.rect_intersects(this.looseBounds, range)) return found
// Check sprites in this node
for (let s of this.sprites)
if (geometry.rect_intersects(s.rect, range)) found.push(s)
// If subdivided, recurse
if (this.divided) {
this.northwest.query(range, found)
this.northeast.query(range, found)
this.southwest.query(range, found)
this.southeast.query(range, found)
}
return found
}
}
class Quadtree {
constructor(boundary, capacity=4) {
this.boundary = boundary // {x, y, width, height} for this node
this.capacity = capacity // max sprites before subdividing
this.sprites = [] // sprite objects stored in this node
this.divided = false // has this node subdivided?
}
subdivide() {
let x = this.boundary.x
let y = this.boundary.y
let w = this.boundary.width / 2
let h = this.boundary.height / 2
this.northwest = new Quadtree({x, y, width: w, height: h}, this.capacity)
this.northeast = new Quadtree({x: x + w, y, width: w, height: h}, this.capacity)
this.southwest = new Quadtree({x, y: y + h, width: w, height: h}, this.capacity)
this.southeast = new Quadtree({x: x + w, y: y + h, width: w, height: h}, this.capacity)
this.divided = true
}
insert(sprite) {
// Get the sprite's bounding rect
let rect = sprite.rect;
// If it doesn't intersect this quadtree's boundary, ignore
if (!geometry.rect_intersects(this.boundary, rect)) return false
// If there's room here and no subdivision yet, just store it
if (this.sprites.length < this.capacity && !this.divided) {
this.sprites.push(sprite)
return true
}
// Otherwise, subdivide if not done yet
if (!this.divided) this.subdivide()
// Try inserting into children
if (this.northwest.insert(sprite)) return true
if (this.northeast.insert(sprite)) return true
if (this.southwest.insert(sprite)) return true
if (this.southeast.insert(sprite)) return true
// If it doesn't cleanly fit into a child (spans multiple quadrants), store here
this.sprites.push(sprite)
return true
}
// Query all sprites that intersect the given range (e.g. your camera rect)
query(range, found=[]) {
// If there's no overlap, nothing to do
if (!geometry.rect_intersects(range, this.boundary)) return found
// Check sprites in this node
for (let s of this.sprites)
if (geometry.rect_intersects(range, s.rect)) found.push(s)
// Recursively query children if subdivided
if (this.divided) {
this.northwest.query(range, found)
this.northeast.query(range, found)
this.southwest.query(range, found)
this.southeast.query(range, found)
}
return found
}
}
class RNode {
constructor() {
this.children = []
this.bbox = null
this.leaf = true
this.height = 1
}
}
class RTree {
constructor(maxEntries = 4) {
this.maxEntries = maxEntries
this.minEntries = Math.max(2, Math.floor(maxEntries * 0.4))
this.root = new RNode()
}
_getBBox(child) {
return child instanceof RNode ? child.bbox : child.rect
}
_unionBBox(a, b) {
if (!a) return {...b}
if (!b) return {...a}
const minX = Math.min(a.x, b.x)
const minY = Math.min(a.y, b.y)
const maxX = Math.max(a.x + a.width, b.x + b.width)
const maxY = Math.max(a.y + a.height, b.y + b.height)
return {x: minX, y: minY, width: maxX - minX, height: maxY - minY}
}
insert(item) {
if (!item) return
if (this.root.leaf && this.root.children.length === 0) {
this.root.children.push(item)
this.root.bbox = {...item.rect}
return
}
let node = this._chooseSubtree(this.root, item)
node.children.push(item)
this._extend(node, item)
while (node && node.children.length > this.maxEntries) {
this._split(node)
node = this._findParent(this.root, node)
}
}
_chooseSubtree(node, item) {
while (!node.leaf) {
let minEnlargement = Infinity
let bestChild = null
for (const child of node.children) {
const enlargement = this._enlargement(child.bbox, item)
if (enlargement < minEnlargement) {
minEnlargement = enlargement
bestChild = child
}
}
node = bestChild || node.children[0]
}
return node
}
_enlargement(bbox, item) {
if (!bbox) return Number.MAX_VALUE
const itemBBox = this._getBBox(item)
const union = this._unionBBox(bbox, itemBBox)
return (union.width * union.height) - (bbox.width * bbox.height)
}
_split(node) {
const items = node.children
items.sort((a, b) => {
const aBBox = this._getBBox(a)
const bBBox = this._getBBox(b)
return aBBox.x - bBBox.x
})
const splitIndex = Math.ceil(items.length / 2)
const group1 = items.slice(0, splitIndex)
const group2 = items.slice(splitIndex)
if (node === this.root) {
this.root = new RNode()
this.root.leaf = false
node.children = group1
node.bbox = this._getBBoxes(group1)
const sibling = new RNode()
sibling.leaf = node.leaf
sibling.children = group2
sibling.bbox = this._getBBoxes(group2)
this.root.children = [node, sibling]
this.root.bbox = this._unionBBox(node.bbox, sibling.bbox)
} else {
node.children = group1
node.bbox = this._getBBoxes(group1)
const sibling = new RNode()
sibling.leaf = node.leaf
sibling.children = group2
sibling.bbox = this._getBBoxes(group2)
const parent = this._findParent(this.root, node)
parent.children.push(sibling)
parent.bbox = this._unionBBox(parent.bbox, sibling.bbox)
}
}
_getBBoxes(items) {
let bbox = null
for (const item of items) {
bbox = this._unionBBox(bbox, this._getBBox(item))
}
return bbox
}
_extend(node, item) {
const itemBBox = this._getBBox(item)
node.bbox = node.bbox ? this._unionBBox(node.bbox, itemBBox) : {...itemBBox}
}
_findParent(node, target, parent = null) {
if (!node || node === target) return parent
if (!node.leaf) {
for (const child of node.children) {
if (child instanceof RNode) {
const result = this._findParent(child, target, node)
if (result) return result
}
}
}
return null
}
query(range, results = []) {
const stack = [this.root]
while (stack.length > 0) {
const node = stack.pop()
if (!node.bbox || !geometry.rect_intersects(node.bbox, range)) continue
if (node.leaf) {
for (const item of node.children) {
if (geometry.rect_intersects(item.rect, range)) {
results.push(item)
}
}
} else {
stack.push(...node.children)
}
}
return results
}
}
function make_point_obj(o, p) {
return {
pos: p,
@@ -337,17 +21,8 @@ frog = {
}
*/
var world = {x:-1000,y:-1000, width:3000, height:3000};
//globalThis.sprite_qt = new Quadtree({x:-1000,y:-1000, width:3000, height:3000}, 10);
//globalThis.sprite_qt = new LooseQuadtree(world, 10, 1.2);
//globalThis.sprite_qt = new RTree(10)
//globalThis.sprite_qt = os.make_quadtree(world, 4);
//globalThis.sprite_qt = os.make_qtree(world);
globalThis.sprite_qt = os.make_rtree();
var spritetree = os.make_quadtree([0,0], [10000,10000]);
globalThis.spritetree = spritetree;
var sprite = {
image: undefined,
get diffuse() { return this.image; },
@@ -564,6 +239,11 @@ component.sprite = function (obj) {
sp.transform.parent = obj.transform;
sp.guid = prosperon.guid();
allsprites.push(sp);
sp.transform.change_hook = function() {
sprite_qt.remove(sp);
sp.rect = sp.transform.torect();
sprite_qt.insert(sp);
}
return sp;
};

View File

@@ -75,8 +75,8 @@ emitter.step = function step(dt) {
}
}
for (var p of this.particles)
p.transform.clean();
// for (var p of this.particles)
// p.transform.clean();
};
emitter.burst = function (count, t) {

View File

@@ -756,19 +756,8 @@ function insertion_sort(arr, cmp)
return arr
}
var culled;
var treed;
function sprites_to_queue(sprites, ysort = false)
{
if (!treed) {
for (var sp of allsprites) {
sp.rect = sp.transform.torect();
sprite_qt.insert(sp)
}
treed = true;
}
var pos = prosperon.camera.transform.pos;
var size = prosperon.camera.size;
var camrect = {

View File

@@ -371,8 +371,6 @@ struct lrtb {
float b;
};
static JSContext *global_js;
static SDL_GPUDevice *global_gpu;
SDL_GPUGraphicsPipelineTargetInfo js2SDL_GPUGraphicsPipelineTargetInfo(JSContext *js, JSValue v)
@@ -3312,7 +3310,7 @@ JSC_CCALL(renderer_make_sprite_mesh,
size_t base = i * 4;
HMM_Mat3 trmat = tr->gcache3;
// HMM_Mat3 trmat = transform2mat3_global(tr);
HMM_Vec3 base_quad[4] = {
{0.0,0.0,1.0},
@@ -3321,8 +3319,8 @@ JSC_CCALL(renderer_make_sprite_mesh,
{1.0,1.0,1.0}
};
for (int j = 0; j < 4; j++)
posdata[base+j] = HMM_MulM3V3(trmat, base_quad[j]).xy;
// for (int j = 0; j < 4; j++)
// posdata[base+j] = HMM_MulM3V3(trmat, base_quad[j]).xy;
// Define the UV coordinates based on the source rectangle
uvdata[base + 0] = (HMM_Vec2){ src.x, src.y + src.h };
@@ -3989,12 +3987,10 @@ JSC_CCALL(gpu_sort_sprite,
JS_GETATOM(js,blayer,b,layer_atom,number)
if (alayer != blayer) return number2js(js,alayer - blayer);
transform *atr, *btr;
JS_GETATOM(js,atr,a,transform_atom,transform)
JS_GETATOM(js,btr,b,transform_atom,transform);
HMM_Mat3 am3 = transform2mat3_global(atr);
HMM_Mat3 bm3 = transform2mat3_global(btr);
if (am3.Columns[2].y != bm3.Columns[2].y) return number2js(js,bm3.Columns[2].y - am3.Columns[2].y);
rect ar, br;
JS_GETATOM(js,ar,a,rect_atom,rect)
JS_GETATOM(js,br,b,rect_atom,rect)
if (ar.y != br.y) return number2js(js,br.y-ar.y);
JSValue aimg,bimg;
aimg = JS_GetProperty(js,a,image_atom);
@@ -4148,8 +4144,7 @@ JSC_CCALL(gpu_make_sprite_mesh,
JS_FreeValue(js,sub);
size_t base = i*4;
HMM_Mat3 trmat = tr->gcache3;
// HMM_Mat3 trmat = transform2mat3(tr);
HMM_Mat3 trmat = transform2mat3(tr);
for (int j = 0; j < 4; j++)
posdata[base+j] = HMM_MulM3V3(trmat, base_quad[j]).xy;
@@ -5769,7 +5764,7 @@ JSC_CCALL(transform_lookat,
transform *go = js2transform(js,self);
HMM_Mat4 m = HMM_LookAt_RH(go->pos, point, vUP);
go->rotation = HMM_M4ToQ_RH(m);
go->dirty = true;
transform_apply(go);
)
JSC_CCALL(transform_rotate,
@@ -5811,7 +5806,6 @@ JSC_CCALL(transform_unit,
t->rotation = QUAT1;
t->scale = v3one;
transform_apply(t);
t->parent = NULL;
)
JSC_CCALL(transform_trs,
@@ -5839,6 +5833,20 @@ JSC_CCALL(transform_array,
JS_SetPropertyUint32(js,ret,i, number2js(js,m.em[i]));
)
static JSValue js_transform_get_change_hook(JSContext *js, JSValueConst self)
{
transform *t = js2transform(js,self);
return JS_DupValue(js,t->change_hook);
}
static JSValue js_transform_set_change_hook(JSContext *js, JSValueConst self, JSValue v)
{
transform *t = js2transform(js,self);
if (!JS_IsUndefined(v) && !JS_IsFunction(js,v)) return JS_ThrowReferenceError(js, "Hook must be a function.");
JS_FreeValue(js,t->change_hook);
t->change_hook = JS_DupValue(js,v);
}
static JSValue js_transform_get_parent(JSContext *js, JSValueConst self)
{
transform *t = js2transform(js,self);
@@ -5848,26 +5856,42 @@ static JSValue js_transform_get_parent(JSContext *js, JSValueConst self)
static JSValue js_transform_set_parent(JSContext *js, JSValueConst self, JSValue v)
{
transform *p = js2transform(js,v);
if (!p)
return JS_ThrowReferenceError(js,"Parent must be another transform.");
transform *t = js2transform(js,self);
if (t->parent)
if (t->parent) {
JS_FreeValue(js,t->jsparent);
transform *p = js2transform(js,v);
for (int i = 0; i < arrlen(p->children); i++) {
if (p->children[i] == t) {
arrdelswap(p->children,i);
break;
}
}
for (int i = 0; i < arrlen(p->jschildren); i++) {
if (JS_SameValue(js,p->jschildren[i],self)) {
JS_FreeValue(js,p->jschildren[i]);
arrdelswap(p->jschildren,i);
break;
}
}
}
t->parent = p;
if (p)
t->jsparent = JS_DupValue(js,v);
t->jsparent = JS_DupValue(js,v);
arrput(p->children, t);
JSValue child = JS_DupValue(js,self);
arrput(p->jschildren,child);
transform_apply(t);
return JS_UNDEFINED;
}
JSC_CCALL(transform_clean,
transform2mat3_global(js2transform(js,self));
)
JSC_CCALL(transform_dirty,
return JS_NewBool(js,transform_dirty_chain(js2transform(js,self)));
)
JSC_CCALL(transform_torect,
transform *t = js2transform(js,self);
return rect2js(js,transform2rect(t));
@@ -5877,7 +5901,8 @@ static const JSCFunctionListEntry js_transform_funcs[] = {
CGETSET_ADD(transform, pos),
CGETSET_ADD(transform, scale),
CGETSET_ADD(transform, rotation),
CGETSET_ADD(transform, parent),
CGETSET_ADD(transform, parent),
CGETSET_ADD(transform, change_hook),
MIST_FUNC_DEF(transform, trs, 3),
MIST_FUNC_DEF(transform, phys2d, 3),
MIST_FUNC_DEF(transform, move, 1),
@@ -5888,8 +5913,6 @@ static const JSCFunctionListEntry js_transform_funcs[] = {
MIST_FUNC_DEF(transform, unit, 0),
MIST_FUNC_DEF(transform, rect, 1),
MIST_FUNC_DEF(transform, array, 0),
MIST_FUNC_DEF(transform, clean, 0),
MIST_FUNC_DEF(transform, dirty, 0),
MIST_FUNC_DEF(transform, torect, 0),
};

View File

@@ -13,6 +13,8 @@
static JSContext *js = NULL;
static JSRuntime *rt = NULL;
JSContext *global_js = NULL;
#ifndef NDEBUG
#define JS_EVAL_FLAGS JS_EVAL_FLAG_STRICT
#else

View File

@@ -10,4 +10,6 @@ void script_evalf(const char *format, ...);
JSValue script_eval(const char *file, const char *script);
void script_call_sym(JSValue sym, int argc, JSValue *argv);
extern JSContext *global_js;
#endif

View File

@@ -1,6 +1,9 @@
#include "transform.h"
#include <string.h>
#include <stdio.h>
#include "script.h"
#include "stb_ds.h"
transform *make_transform()
{
@@ -9,10 +12,50 @@ transform *make_transform()
t->scale = (HMM_Vec3){1,1,1};
t->rotation = (HMM_Quat){0,0,0,1};
transform_apply(t);
t->change_hook = JS_UNDEFINED;
return t;
}
void transform_free(JSRuntime *rt, transform *t) { free(t); }
void transform_clean(transform *t)
{
if (!t->dirty) return;
t->dirty = 0;
t->cache = HMM_M4TRS(t->pos,t->rotation,t->scale);
if (t->parent)
t->gcache = HMM_MulM4(t->parent->gcache, t->cache);
else
t->gcache = t->cache;
HMM_Mat4 m = t->gcache;
rect r = {0};
r.x = m.Columns[3].x;
r.y = m.Columns[3].y;
r.w = m.Columns[0].x;
r.h = m.Columns[1].y;
t->rcache = r;
for (int i = 0; i < arrlen(t->children); i++) {
t->children[i]->dirty = 1;
transform_clean(t->children[i]);
}
if (!JS_IsUndefined(t->change_hook)) {
JSValue ret = JS_Call(global_js, t->change_hook, JS_UNDEFINED, 0, NULL);
JS_FreeValue(global_js,ret);
}
}
void transform_free(JSRuntime *rt, transform *t) {
JS_FreeValueRT(rt,t->change_hook);
JS_FreeValueRT(rt,t->jsparent);
for (int i = 0; i < arrlen(t->jschildren); i++)
JS_FreeValueRT(rt,t->jschildren[i]);
arrfree(t->jschildren);
arrfree(t->children);
free(t);
printf("FREED TRANSFORM!\n");
}
void transform_move(transform *t, HMM_Vec3 v)
{
t->pos = HMM_AddV3(t->pos, v);
@@ -52,76 +95,30 @@ HMM_Vec3 mat3_t_dir(HMM_Mat4 m, HMM_Vec3 dir)
return mat3_t_pos(m, dir);
}
static inline void transform_clean(transform *t)
{
if (!t->dirty) return;
t->dirty = 0;
t->cache = HMM_M4TRS(t->pos,t->rotation,t->scale);
t->cache3 = HMM_M3TRS(t->pos.xy,t->rotation.x,t->scale.xy);
}
HMM_Mat4 transform2mat(transform *t)
{
transform_clean(t);
return t->cache;
}
HMM_Mat3 transform2mat3(transform *t)
{
transform_clean(t);
return t->cache3;
return HMM_M3TRS(t->pos.xy, t->rotation.x, t->scale.xy);
}
HMM_Mat4 transform2mat4_global(transform *t)
{
if (!transform_dirty_chain(t)) return t->gcache;
HMM_Mat4 tm = transform2mat(t);
if (t->parent)
t->gcache = HMM_MulM4(transform2mat(t->parent), tm);
else
t->gcache = tm;
return t->gcache;
}
rect transform2rect(transform *t)
{
if (!transform_dirty_chain(t)) return t->rcache;
HMM_Mat4 m3 = transform2mat4_global(t);
rect r = {0};
r.x = m3.Columns[3].x;
r.y = m3.Columns[3].y;
r.w = m3.Columns[0].x;
r.h = m3.Columns[1].y;
t->rcache = r;
return t->rcache;
}
HMM_Mat3 transform2mat3_global(transform *t)
{
if (!transform_dirty_chain(t)) return t->gcache3;
HMM_Mat3 tm = transform2mat3(t);
if (t->parent)
t->gcache3 = HMM_MulM3(transform2mat3(t->parent), tm);
else
t->gcache3 = tm;
return t->gcache3;
}
int transform_dirty_chain(transform *t)
{
if (t->dirty) return 1;
if (t->parent)
return transform_dirty_chain(t->parent);
return 0;
}
void transform_apply(transform *t)
{
t->dirty = 1;
transform_clean(t);
}
HMM_Quat angle2rotation(float angle)

View File

@@ -10,15 +10,14 @@ typedef struct transform {
HMM_Vec3 scale;
HMM_Quat rotation;
HMM_Mat4 cache;
HMM_Mat3 cache3;
HMM_Mat3 gcache3;
HMM_Mat4 gcache;
rect rcache;
int dirty;
struct transform *parent;
JSValue jsparent;
struct transform *children;
struct transform **children;
JSValue *jschildren;
JSValue change_hook;
} transform;
transform *make_transform();
@@ -52,9 +51,7 @@ HMM_Mat4 transform2mat(transform *t);
HMM_Mat3 transform2mat3(transform *t);
HMM_Mat4 transform2mat4_global(transform *t);
HMM_Mat3 transform2mat3_global(transform *t);
rect transform2rect(transform *t);
int transform_dirty_chain(transform *t);
transform mat2transform(HMM_Mat4 m);