Files
cell/docs/ops.md
2025-01-05 16:11:31 -06:00

9.9 KiB

RENDERING PIPELINE

The basic flow for developing graphics here:

  1. develop a render graph
  2. decide what to draw

The render graph is the "big idea" of how the data flows through a render; inside the execution, you utilize "what to draw".

Prosperon provides you with functions to facilitate the creation of rendering pipelines. For example, you could use "shadow_vol" function to create buffer geometry with shadow volume data.

Unity has a "graphics.rendermesh" function that you can call, and that unity automatically calls for renderer components. It is the same here. But there are a handful of other types to draw, particularly for 2d.

2D

Anatomy of a 2d renderer

Traditionally, 2d rendering is a mix of tilemaps and sprites. Today, it is still more cost effective to render tilemaps, but we have a lot more flexibility.

NES Nes had 1 tilemap and up to 8 sprites per scanline.

SNES Up to 4 tilemap backgrounds, with priority, and flipping capability. 32 sprites per scanline, and by setting the priority correctly, they could appear behind background layers.

GB One background layer, 10 sprites per scanline/40 per frame.

GBA Up to 4 layers, sprites with affine transforms!

DS Up to 4 layers; many sprites; and a 3d layer!

Sega saturn This and everything else with generic vertex processing could do as many background layers and sprites as desired. This is what you get with prosperon on most modern computers. For more limited hardware, your options become limited too!

Prosperon rendering

Layers Every drawable 2d thing has a layer. This is an integer that goes from -9223372036854775808 to 9223372036854775808.

!!! On hardware that supports only a limited number of layers, this value must go from 0 to (layer #).

Layer sort Within a layer, objects are sorted based on a given criteria. By default, this is nothing, and the engine may reorder the draws to optimize for performance. Instead, you can choose to sort by their y axis position, for example.

Parallax Layers can have a defined parallax value, set at the engine level. Anything on that layer will move with the provided parallax. Each layer has an implicit parallax value of "1", which means it moves "as expected". Below 1 makes it move slower (0 makes it not move at all), 2 makes it move twice as fast, etc.

Tilemaps These are highly efficient and work just like tilemaps on old consoles. When you submit one of these to draw, Prosperon can efficientally cull what can't be seen by the camera. You can have massive levels with these without any concern for performance. A tilemap is all on its own layer.

Tiles can be flipped; and the entire tilemap can have an affine transformation applied to it.

Sprites each have their own layer and affine transform. Tilemaps are just like a large sprite.

In addition to all of this, objects can have a "draw" event, wherein you can issue direct drawing commands like "render.sprite", "render.text", "render.circle", and so on. This can be useful for special effects, like multi draw passes (set stencil -> draw -> revert stencil). In this case, it is the draw event itself with the layer setting.

3D

3d models are like 3d sprites. Add them to the world, and then the engine handles drawing them. If you want special effects, its "draw" command can be overridden.

As sprites and 3d models are sent to render, they are added to a list; sorted; and then finally rendered.

THE RENDERER

Fully scriptable

The render layer is where you do larger scale organizing. For example, for a single outline, you might have an object's draw method be the standard:

  • draw the model, setting stencil
  • draw a scaled up model with a single color

But, since each object doing this won't merge their outlines, you need a larger order solution, wherein you draw all models that will be outlined, and then draw all scaled up models with a single color. The render graph is how you could do that. The render graph calls draw and render functions; so with a tag system, you can essentially choose to draw whatever you want. You can add new shadow passes; whatever. Of course, prosperon is packed with some standard render graphs to utilize right away.

Each graphical drawing command has a specific pipeline. A pipeline is a static object that defines every rendering detail of a drawing command.

A drawing command is composed of:

  • a model
  • a material
  • a pipeline

The engine handles sorting these and rendering them effectively. There exist helper functions, like "render.image" which will in turn create a material and use the correct model.

You execute a list of drawing commands onto a render target. This might be the computer screen; it might be an offscreen target.

The material's properties are copied into the shader on a given pipeline; they also can have extra properties like "castshadows", "getshadows", and so on.

An image is a struct { texture: GPU texture rect: UV coordinates }

2D drawing commands

The 2d drawing commands ultimately interface with a VERY limited subset of backend knowledge, and so are easily adaptable for a wide variety of hardware and screen APIs.

The basic 2D drawing techniques are: Sprite - arbitrarily blit a bitmap to the screen with a given affine transformation and color Tiles - Uniform squares in a grid pattern, drawn all on a single layer Text - Generates whatever is needed to display text wrapped in a particular way at a particular coordinate Particles - a higher order construction Geometry - programmer called for circles or any other arbitrary shape. Might be slow!

Effects

An "effect" is essentially a sequence of render commands. Typically, a sprite draws itself to a screen. It may have a unique pipeline for a special effect. But it might also have an "effect", which is actually a sequence of draw instructions. An example might be an outline scenario, where the sprite draws a black version of it scaled 1.1x, and then draws with the typical pipeline afterwards.

A frame

During a frame, the engine finds everything that needs rendered. This includes enabled models, enabled sprites, tilemaps, etc. This also includes programmer directions inside of the draw() and hud() functions.

This high level commands are culled down, accounting for off screen sprites, etc, into a more compact command queue. This command queue is then rendered in whichever way the backend sees fit. Each "command queue" maps roughly into a "render pass" in vulkan. Once you submit a command queue, the data is sorted, required data is uploaded, and a render pass draws it to the specified frame.

A command is kicked off with a "batch" command.

var batch = render.batch(target, clearcolor) // target is the target buffer to draw onto target must be known when the batch starts because it must ensure the pipelines fed into it are compatible. If clearcolor is undefined, it does not erase what is present on the target before drawing. To disable depth, simply do not include a depth attachment in the target.

batch.draw(mesh, material, pipeline) This is the most fundamental draw command you can do. In modern parlance, the pipeline sets up the GPU completely for rendering (stencil, blend, shaders, etc); the material plugs data into the pipeline, via reflection; the mesh determines the geometry that is drawn. A mesh defines everything that's needed to kick of a draw call, including if the buffers are indexed or not, the number of indices to draw, and the first index to draw from.

batch.viewport()

batch.sprite

batch.text // a text object. faster than doing each letter as a sprite, but less flexible // etc batch.render(camera)

Batches can be saved to be executed again and again. So, one set of batches can be created, and then drawn from many cameras' perspectives. batch.render must take a camera

Behind the scenes, a batch tries to merge geometry, and does reordering for minimum pipeline changes behind the scenes.

Each render command can use its own unique pipeline, which entails its own shader, stencil buffer setup, everything. It is extremely flexible. Sprites can have their own pipeline.

ULTIMATELY::: This is a much more functional style than what is typically presented from graphics APIs. Behind the scenes these are all translated to OpenGL or whatever; being functional at this level helps to optimize.

IMPORTANT NOTE: Optimization only happens at the object level. If you have two pipelines with the exact same characteristics, they will not be batched. Use the exact same pipeline object to batch.

SCENARIOS

BLOOM BULLETS You want to draw a background; some ships; and some bullets that have glow to them. This amounts to two ideas:

  1. draw the background and ships
  2. draw bullets to a texture
  3. apply bloom on the bullet
  4. draw bullets+bloom over the background and ships

Steps 1, and 2-3, can be done in parallel. They constitute their own command queues. When both are done, the composite can then happen.

var bg_batch = render.batch(surf1, camera); bg_batch.draw(background) bg_batch.draw(ships) bg_batch.end()

var bullet_batch = render.batch(surf2, camera); bullet_batch.draw(bullets) bullet_batch.end()

var bloom = render.batch(surf3, postcam) bloom.draw(bullet_batch.color, bloom_pipeline) bloom.end()

var final = render.batch(swapchain) final.draw(bg_batch.color) final.draw(bloom.color) final.end()

When 'batch.end' is called, it reorders as needed, uploads data, and then does a render pass.

3D GAME WITH DIRECTIONAL LIGHT SHADOW MAP

var shadow_batch = render.batch(shadow_surf, dir_T) shadow_batch.draw(scene, depth_mat) // scene returns a list of non culled 3d obejcts; we force it to use depth_mat shadow_batch.end()

base_mat.shadowmap = shadow_batch.color;

var main_batch = render.batch(swapchain, camera) main_batch.draw(scene) main_batch.end()

FIERY LETTERS This pseudo code draws a "hello world" cutout, with fire behind it, and then draws the game's sprites over that

var main = render.batch(swapchain, 2dcam) main.draw("hello world", undefined, stencil_pipeline) main.draw(fire) main.draw(fullscreen, undefined, stencil_reset) main.draw(game) main.end()