Skip to content

Commit

Permalink
Move Sprite rendering to C3
Browse files Browse the repository at this point in the history
  • Loading branch information
rexim committed Sep 11, 2024
1 parent 5f7104c commit e0d29e8
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 134 deletions.
211 changes: 94 additions & 117 deletions client.c3
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import std::math;
import std::sort;
import common;

const float NEAR_CLIPPING_PLANE = 0.1;
Expand All @@ -25,8 +26,8 @@ struct Sprite {
Vector2 position;
float z;
float scale;
Vector2 crop_position;
Vector2 crop_size;
IVector2 crop_position;
IVector2 crop_size;

float dist; // Actual distance.
float pdist; // Perpendicular distance.
Expand All @@ -36,12 +37,12 @@ struct Sprite {
struct SpritePool {
Sprite[SPRITE_POOL_CAPACITY] items;
int length;
Sprite[SPRITE_POOL_CAPACITY] visible_items;
Sprite*[SPRITE_POOL_CAPACITY] visible_items;
int visible_length;
}

fn Sprite *allocate_sprite_pool() @extern("allocate_sprite_pool") @wasm {
return mem::new(Sprite);
fn SpritePool *allocate_sprite_pool() @extern("allocate_sprite_pool") @wasm {
return mem::new(SpritePool);
}

fn void reset_sprite_pool(SpritePool *sprite_pool) @extern("reset_sprite_pool") @wasm {
Expand Down Expand Up @@ -333,127 +334,103 @@ fn void render_minimap(Color *display, int display_width, int display_height,
// ctx.restore();
}

// TODO: @translate
fn void cull_and_sort_sprites(float camera_position_x, float camera_position_y, float camera_direction,
SpritePool *sprite_pool) @extern("cull_and_sort_sprites") @wasm {
// const sp = new Vector2();
// const dir = new Vector2().setPolar(camera.direction);
// const fov = camera.fovRight.clone().sub(camera.fovLeft);

// visibleSprites.length = 0;
// for (let i = 0; i < spritePool.length; ++i) {
// const sprite = spritePool.items[i];

// sp.copy(sprite.position).sub(camera.position);
// const spl = sp.length();
// if (spl <= NEAR_CLIPPING_PLANE) continue; // Sprite is too close
// if (spl >= FAR_CLIPPING_PLANE) continue; // Sprite is too far

// const cos = sp.dot(dir)/spl;
// // TODO: @perf the sprites that are invisible on the screen but within FOV 180° are not culled
// // It may or may not impact the performance of renderSprites()
// if (cos < 0) continue; // Sprite is outside of the maximal FOV 180°
// sprite.dist = NEAR_CLIPPING_PLANE/cos;
// sp.norm().scale(sprite.dist).add(camera.position).sub(camera.fovLeft);
// sprite.t = sp.length()/fov.length()*Math.sign(sp.dot(fov));
// sprite.pdist = sprite.position.clone().sub(camera.position).dot(dir);

// // TODO: I'm not sure if these checks are necessary considering the `spl <= NEAR_CLIPPING_PLANE` above
// if (sprite.pdist < NEAR_CLIPPING_PLANE) continue;
// if (sprite.pdist >= FAR_CLIPPING_PLANE) continue;

// visibleSprites.push(sprite);
// }
Camera camera = { .position = {camera_position_x, camera_position_y}, .direction = camera_direction };
camera.update();

// visibleSprites.sort((a, b) => b.pdist - a.pdist);
Vector2 dir = from_polar(camera_direction, 1.0f);
Vector2 fov = camera.fovRight - camera.fovLeft;

sprite_pool.visible_length = 0;
for (int i = 0; i < sprite_pool.length; ++i) {
Sprite *sprite = &sprite_pool.items[i];

Vector2 sp = sprite.position - camera.position;
float spl = sp.length();
if (spl <= NEAR_CLIPPING_PLANE) continue; // Sprite is too close
if (spl >= FAR_CLIPPING_PLANE) continue; // Sprite is too far

float cos = sp.dot(dir)/spl;
// TODO: @perf the sprites that are invisible on the screen but within FOV 180° are not culled
// It may or may not impact the performance of renderSprites()
if (cos < 0) continue; // Sprite is outside of the maximal FOV 180°
sprite.dist = NEAR_CLIPPING_PLANE/cos;
sp = (sp.normalize()*sprite.dist) + camera.position - camera.fovLeft;
sprite.t = sp.length()/fov.length()*math::copysign(1.0f, sp.dot(fov));
sprite.pdist = (sprite.position - camera.position).dot(dir);

// TODO: I'm not sure if these checks are necessary considering the `spl <= NEAR_CLIPPING_PLANE` above
if (sprite.pdist < NEAR_CLIPPING_PLANE) continue;
if (sprite.pdist >= FAR_CLIPPING_PLANE) continue;

sprite_pool.visible_items[sprite_pool.visible_length++] = sprite;
}

quicksort(sprite_pool.visible_items[0..sprite_pool.visible_length-1],
fn int(Sprite *a, Sprite *b) => (int)math::copysign(1.0f, b.pdist - a.pdist));
}

// TODO: @translate
fn void push_sprite(SpritePool *sprite_pool,
Color *image_pixels, int image_width, int image_height,
float x, float y, float z,
float scale,
float crop_position_x, float crop_position_y,
float crop_size_x, float crop_size_y) @extern("push_sprite") @wasm {
// if (spritePool.length >= spritePool.items.length) {
// spritePool.items.push({
// image,
// position: new Vector2(),
// z,
// scale,
// pdist: 0,
// dist: 0,
// t: 0,
// cropPosition: new Vector2(),
// cropSize: new Vector2(),
// })
// }

// const last = spritePool.length;

// spritePool.items[last].image = image;
// spritePool.items[last].position.copy(position);
// spritePool.items[last].z = z;
// spritePool.items[last].scale = scale;
// spritePool.items[last].pdist = 0;
// spritePool.items[last].dist = 0;
// spritePool.items[last].t = 0;

// if (image instanceof WasmImage) {
// if (cropPosition === undefined) {
// spritePool.items[last].cropPosition.set(0, 0);
// } else {
// spritePool.items[last].cropPosition.copy(cropPosition);
// }
// if (cropSize === undefined) {
// spritePool.items[last]
// .cropSize
// .set(image.width, image.height)
// .sub(spritePool.items[last].cropPosition);
// } else {
// spritePool.items[last].cropSize.copy(cropSize);
// }
// } else {
// spritePool.items[last].cropPosition.set(0, 0);
// spritePool.items[last].cropSize.set(0, 0);
// }

// spritePool.length += 1;
int crop_position_x, int crop_position_y,
int crop_size_x, int crop_size_y) @extern("push_sprite") @wasm {
if (sprite_pool.length >= SPRITE_POOL_CAPACITY) return;

usz last = sprite_pool.length;

sprite_pool.items[last].image.pixels = image_pixels;
sprite_pool.items[last].image.width = image_width;
sprite_pool.items[last].image.height = image_height;
sprite_pool.items[last].position.x = x;
sprite_pool.items[last].position.y = y;
sprite_pool.items[last].z = z;
sprite_pool.items[last].scale = scale;
sprite_pool.items[last].pdist = 0;
sprite_pool.items[last].dist = 0;
sprite_pool.items[last].t = 0;
sprite_pool.items[last].crop_position.x = crop_position_x;
sprite_pool.items[last].crop_position.y = crop_position_y;
sprite_pool.items[last].crop_size.x = crop_size_x;
sprite_pool.items[last].crop_size.y = crop_size_y;

sprite_pool.length += 1;
}

// TODO: @translate
fn void render_sprites(Color *display, int display_width, int display_height, SpritePool *sprite_pool) @extern("render_sprites") @wasm {
// const backImageData = new Uint8ClampedArray(wasmClient.memory.buffer, display.backImagePtr, display.backImageWidth*display.backImageHeight*4);
// const zBuffer = new Float32Array(wasmClient.memory.buffer, display.zBufferPtr, display.backImageWidth);
// for (let sprite of sprites) {
// const cx = display.backImageWidth*sprite.t;
// const cy = display.backImageHeight*0.5;
// const maxSpriteSize = display.backImageHeight/sprite.pdist;
// const spriteSize = maxSpriteSize*sprite.scale;
// const x1 = Math.floor(cx - spriteSize*0.5);
// const x2 = Math.floor(x1 + spriteSize - 1);
// const bx1 = Math.max(0, x1);
// const bx2 = Math.min(display.backImageWidth-1, x2);
// const y1 = Math.floor(cy + maxSpriteSize*0.5 - maxSpriteSize*sprite.z);
// const y2 = Math.floor(y1 + spriteSize - 1);
// const by1 = Math.max(0, y1);
// const by2 = Math.min(display.backImageHeight-1, y2);

// const src = new Uint8ClampedArray(wasmClient.memory.buffer, sprite.image.ptr, sprite.image.width*sprite.image.height*4);
// const dest = backImageData;
// for (let x = bx1; x <= bx2; ++x) {
// if (sprite.pdist < zBuffer[x]) {
// for (let y = by1; y <= by2; ++y) {
// const tx = Math.floor((x - x1)/spriteSize*sprite.cropSize.x);
// const ty = Math.floor((y - y1)/spriteSize*sprite.cropSize.y);
// const srcP = ((ty + sprite.cropPosition.y)*sprite.image.width + (tx + sprite.cropPosition.x))*4;
// const destP = (y*display.backImageWidth + x)*4;
// const alpha = src[srcP + 3]/255;
// dest[destP + 0] = dest[destP + 0]*(1 - alpha) + src[srcP + 0]*alpha;
// dest[destP + 1] = dest[destP + 1]*(1 - alpha) + src[srcP + 1]*alpha;
// dest[destP + 2] = dest[destP + 2]*(1 - alpha) + src[srcP + 2]*alpha;
// }
// }
// }
// }
fn void render_sprites(Color *display, int display_width, int display_height, float *zbuffer,
SpritePool *sprite_pool) @extern("render_sprites") @wasm {
for (int i = 0; i < sprite_pool.visible_length; ++i) {
Sprite *sprite = sprite_pool.visible_items[i];
float cx = display_width*sprite.t;
float cy = display_height*0.5f;
float maxSpriteSize = display_height/sprite.pdist;
float spriteSize = maxSpriteSize*sprite.scale;
int x1 = (int)math::floor(cx - spriteSize*0.5f);
int x2 = (int)math::floor(x1 + spriteSize - 1.0f);
int bx1 = math::max(0, x1);
int bx2 = math::min(display_width-1, x2);
int y1 = (int)math::floor(cy + maxSpriteSize*0.5f - maxSpriteSize*sprite.z);
int y2 = (int)math::floor(y1 + spriteSize - 1);
int by1 = math::max(0, y1);
int by2 = math::min(display_height-1, y2);

Color *src = sprite.image.pixels;
Color *dest = display;
for (int x = bx1; x <= bx2; ++x) {
if (sprite.pdist < zbuffer[x]) {
for (int y = by1; y <= by2; ++y) {
int tx = (int)math::floor((float)(x - x1)/spriteSize*sprite.crop_size.x);
int ty = (int)math::floor((float)(y - y1)/spriteSize*sprite.crop_size.y);
int srcP = (ty + sprite.crop_position.y)*sprite.image.width + (tx + sprite.crop_position.x);
int destP = y*display_width + x;
float alpha = src[srcP].a/255.0f;
dest[destP].r = (char)(dest[destP].r*(1 - alpha) + src[srcP].r*alpha);
dest[destP].g = (char)(dest[destP].g*(1 - alpha) + src[srcP].g*alpha);
dest[destP].b = (char)(dest[destP].b*(1 - alpha) + src[srcP].b*alpha);
}
}
}
}
}
2 changes: 1 addition & 1 deletion client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function cullAndSortSprites(wasmClient, camera, spritePool) {
wasmClient.cull_and_sort_sprites(camera.position.x, camera.position.y, camera.direction, spritePool.ptr);
}
function renderSprites(display, wasmClient, spritePool) {
wasmClient.render_sprites(display.backImagePtr, display.backImageWidth, display.backImageHeight, spritePool.ptr);
wasmClient.render_sprites(display.backImagePtr, display.backImageWidth, display.backImageHeight, display.zBufferPtr, spritePool.ptr);
}
function pushSprite(wasmClient, spritePool, image, position, z, scale, cropPosition, cropSize) {
const cropPosition1 = new Vector2();
Expand Down
19 changes: 3 additions & 16 deletions client.mts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ interface WasmClient extends common.WasmCommon {
scale: number,
crop_position_x: number, crop_position_y: number,
crop_size_x: number, crop_size_y: number) => void;
render_sprites: (display: number, display_width: number, display_height: number, sprite_pool: number) => void,
render_sprites: (display: number, display_width: number, display_height: number, zbuffer: number, sprite_pool: number) => void,
}

function createDisplay(ctx: CanvasRenderingContext2D, wasmClient: WasmClient, backImageWidth: number, backImageHeight: number): Display {
Expand Down Expand Up @@ -167,25 +167,12 @@ function displaySwapBackImageData(display: Display, wasmClient: WasmClient) {
display.ctx.drawImage(display.backCtx.canvas, 0, 0, display.ctx.canvas.width, display.ctx.canvas.height);
}

interface Sprite {
image: WasmImage;
position: Vector2;
z: number;
scale: number;
cropPosition: Vector2;
cropSize: Vector2;

dist: number; // Actual distance.
pdist: number; // Perpendicular distance.
t: number; // Normalized horizontal position on the screen
}

function cullAndSortSprites(wasmClient: WasmClient, camera: Camera, spritePool: SpritePool) {
wasmClient.cull_and_sort_sprites(camera.position.x, camera.position.y, camera.direction, spritePool.ptr);
}

function renderSprites(display: Display, wasmClient: WasmClient, spritePool: SpritePool) {
wasmClient.render_sprites(display.backImagePtr, display.backImageWidth, display.backImageHeight, spritePool.ptr)
wasmClient.render_sprites(display.backImagePtr, display.backImageWidth, display.backImageHeight, display.zBufferPtr, spritePool.ptr)
}

function pushSprite(wasmClient: WasmClient, spritePool: SpritePool, image: WasmImage, position: Vector2, z: number, scale: number, cropPosition?: Vector2, cropSize?: Vector2) {
Expand Down Expand Up @@ -427,7 +414,7 @@ async function instantiateWasmClient(url: string): Promise<WasmClient> {
render_minimap: wasm.instance.exports.render_minimap as (display: number, display_width: number, display_height: number, camera_position_x: number, camera_position_y: number, camera_direction: number, player_position_x: number, player_position_y: number, scene: number, scene_width: number, scene_height: number, sprite_pool: number) => void,
cull_and_sort_sprites: wasm.instance.exports.cull_and_sort_sprites as (camera_position_x: number, camera_position_y: number, camera_direction: number, sprite_pool: number) => void,
push_sprite: wasm.instance.exports.push_sprite as (sprite_pool: number, image_pixels: number, image_width: number, image_height: number, x: number, y: number, z: number, scale: number, crop_position_x: number, crop_position_y: number, crop_size_x: number, crop_size_y: number) => void,
render_sprites: wasm.instance.exports.render_sprites as (display: number, display_width: number, display_height: number, sprite_pool: number) => void,
render_sprites: wasm.instance.exports.render_sprites as (display: number, display_width: number, display_height: number, zbuffer: number, sprite_pool: number) => void,
};
}

Expand Down
Binary file modified client.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions common.c3
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module common;
import std::math;

def Vector2 = float[<2>];
def IVector2 = int[<2>];
def Vector3 = float[<3>];
def Vector4 = float[<4>];

Expand Down

0 comments on commit e0d29e8

Please sign in to comment.