Items
OkItem is a renderable mesh: vertex data, indices, an optional texture and the shared transform (position, rotation, scale, hierarchy). It derives from OkObject, which provides the transform API used across the engine. OkItemGroup bundles several items so they move and render as one unit and can be tagged for selective visibility. OkTexture wraps a loaded GPU texture.
OkObject
Every drawable inherits these:
| Method | Purpose |
|---|---|
void setPosition(float x, float y, float z) |
Set the local position. |
void move(float dx, float dy, float dz) |
Translate by a delta. |
void setRotation(float x, float y, float z) |
Set Euler rotation (radians). |
void rotate(float dx, float dy, float dz) |
Rotate by a delta (radians). |
void setScaling(float x, float y, float z) |
Set the scale. |
void attachTo(OkObject *parent) |
Parent this object (transforms compose). |
void attach(OkObject *child) |
Add a child to this object. |
void detachFromParent() |
Detach from the parent. |
bool shouldDraw() const |
Override to skip this object AND its children this frame. |
bool shouldStep(float dt) const |
The same for the update. |
void setDrawOriginAxis(bool) |
Toggle the debug origin axis gizmo. |
Deleting an object takes it out of its parent and leaves its children detached, not deleted — whoever created them still owns them. A parent that is the owner of its children has to delete them before itself.
The two questions are what makes a hierarchy worth having: see Scene.
OkItem
An item carries its own bounding sphere, computed from the mesh when it
is handed over: getRadius() and getSphereCenter(). The centre is
not the item's origin -- a mesh baked in the coordinates of the
region it came from has its origin at that region's corner, so the two
can be a long way apart. Anything measuring where an item really is (a
parent working out how far its children reach) wants the centre.
| Method | Purpose |
|---|---|
OkItem(name, vertexData, vertexCount, indexData, indexCount, stride = 5) |
Construct from interleaved vertex data and indices. Stride 5 (x,y,z,u,v) computes vertex normals from the triangle list — de-indexed meshes get exact flat face normals, indexed meshes get smoothed ones; stride 8 (x,y,z,u,v,nx,ny,nz) takes caller normals verbatim. Internally vertices are always stored with stride 8. |
void setWireframe(bool) |
Draw this item as wireframe. |
void setWireframeGlobal(bool) |
Whether the scene-wide graphics.wireframe switch reaches this item (default yes; see below). |
void setCastsShadow(bool) |
Whether this item is drawn into the shadow maps (default yes). Turn it off for things that are LIGHT rather than matter — a lamp's corona, an emissive pane, the sky — which have geometry but would otherwise be recorded as occluders, so that a glow casts a shadow of its own quad. |
void setVisible(bool) |
Show or hide the item. |
void setFade(float) |
Cross-fade amount, 1 solid and 0 gone (see below). |
void setFadeInverted(bool) |
Use the opposite half of the dither pattern. |
void setDrawMode(GLenum mode) |
Set the GL primitive (GL_TRIANGLES, GL_LINES, ...). |
void loadTextureFromFile(const std::string &path) |
Load and apply a texture from disk. |
void addMaterialFromFile(long firstIndex, long indexCount, const std::string &path) |
Give a range of the index buffer its own texture (see below). |
OkItem(name) |
An item with no geometry yet, to be filled with addMesh (see below). |
void addMesh(vertexData, vertexCount, indexData, indexCount, texturePath, stride = 5) |
Append a piece of mesh wearing one texture. The piece's indices count from its own first vertex and are moved along by what the item already holds. |
void upload() |
Hand the assembled mesh to the GPU. Call once, when every piece has been added. |
void clearMaterials() |
Drop every material slot and its texture reference. |
size_t getMaterialCount() const |
How many material slots the item has (0 = single-material). |
void setTexture(const std::string &name, OkTexture *tex) |
Apply an already-loaded texture. The item takes its own reference (see below). |
void setFillColor(float r, float g, float b, float a = 1) |
Untextured fill colour; the alpha is honoured by blended passes (the GUI). |
void setTintColor(float r, float g, float b, float a) |
Multiplied over the texture in the fill pass (white = untouched); how GUI text is coloured. The alpha is the opacity. |
void setBlendMode(OkBlendMode mode) |
How the item's pixels meet what is behind them: opaque, cutout, alpha or additive (see Materials). |
void setOpacity(float a) |
How much of the item shows, 0 to 1. Counts in the two blended modes. |
void setAdditive(bool on) |
setBlendMode(OK_BLEND_ADDITIVE), as a switch. |
void setUnlit(bool on) |
Skip the lights and the scene's tint: a light source, a debug line. |
bool setRangeMaterial(size_t range, const OkMaterial &mat) |
Give one range of faces a material of its own (see Materials). |
const OkMaterial *getRangeMaterial(size_t range) const |
The material a range is drawn with: its own, or the item's. |
void clearRangeMaterial(size_t range) |
Put a range back on the item's material. |
const OkMaterial &getMaterial() const |
The item's own material, to read. |
void setTintMask(const std::string &path) |
A second texture whose red, green and blue are the weights of material slots 0, 1 and 2 (see below). Empty removes it. |
void setMaterialTint(int slot, float r, float g, float b) |
The colour a slot's zone is tinted with. |
void setMaterialLuminance(int slot, bool on) |
Whether a slot's tint multiplies the texture (off) or replaces its hue and keeps its luminance (on). |
void setAlphaCutout(bool on) |
setBlendMode(OK_BLEND_CUTOUT), as a switch: read the texture's alpha as coverage and drop pixels under one half (see below). |
void updateVertexData(float *data, long count) |
Replace the vertex data in place (stride-5 contract; normals recomputed against the item's indices). |
float getRadius() const |
The mesh's maximum dimension. |
bool intersectRay(const OkRay &ray, float *outDistance) const |
Whether a ray crosses this item's own triangles, and how far along (see below). |
Asking an item whether a ray hits it
intersectRay takes a world-space ray — from
OkCamera::rayThroughPixel, or built by hand for
a line-of-sight check — and answers against the item's own triangles,
not against a box around it.
The item is the only one that can answer it. It owns its vertex and index buffers and its place in the world and hands neither back, so anything outside would have to keep a second copy of the mesh to ask — which for a large scene is tens of megabytes, and a copy that can fall out of step with what is actually drawn.
Two tests, in the order that makes the second cheap: the bounding sphere the frustum already culls with, and only then the triangles. Both run in the item's local space, reached by transforming the ray rather than the mesh; OkRay explains why the distance still comes back in world units.
It answers about geometry and nothing else. Whether the item is visible, whether it is the sort of thing this application lets a user choose, and which of several hits wins are the caller's decisions — an engine that made them here would be making them for every application at once. So a selection loop is the caller's, and it is short:
OkRay ray = camera->rayThroughPixel(mouseX, mouseY, width, height);
OkItem *chosen = nullptr;
float nearest = 0.0f;
for (size_t i = 0; i < candidates.size(); i++) {
float distance = 0.0f;
if (!candidates[i]->getVisible()) {
continue; // this application's rule, not the engine's
}
if (!candidates[i]->intersectRay(ray, &distance)) {
continue;
}
if (chosen == nullptr || distance < nearest) {
chosen = candidates[i];
nearest = distance;
}
}
Items drawn as lines or points are never hit: there is no surface to
cross, and reading such an index list in threes would invent triangles out
of unrelated vertices. An OkInstancedItem is answered for its base copy
only.
Wireframe
An item draws as wireframe when its own setWireframe(true) is set, or when
the graphics.wireframe config key is on. The key is a scene-wide switch — a
way of reading how the world is triangulated, which no amount of looking at
the shaded result can tell you — and it reaches everything drawn afterwards,
including meshes that are created later.
Items that are interface rather than scene opt out with
setWireframeGlobal(false). The GUI classes already do: a wireframed font
atlas is a screenful of empty boxes, and the console that turns the switch back
off would be the first thing to become unreadable. Their own setWireframe
still works either way.
OkInstancedItem honours both, drawing its overlay as a second instanced pass,
so objects drawn in bulk show their triangles like everything else.
Cross-fading between two versions
setFade drops a share of an item's pixels on an ordered 4x4 pattern:
1 draws it whole, 0 drops it entirely, and values between dissolve it.
The point is swapping one version of an object for another — a detailed
build for a cheap stand-in — without a frame where everything changes
at once.
A dither rather than transparency, because a dithered item stays in the opaque pass: no blending, no sorting, and the depth buffer keeps working. An alpha fade of anything with depth to it (a building, a whole block) shows the object fighting with itself.
The two sides of a handover must drop opposite pixels, so call
setFadeInverted(true) on exactly one of the pair. With the same
pattern on both, each keeps the same half of the pixels and the other
half shows the background straight through.
detailed->setFade(t); // 0 -> 1 as it takes over
standIn->setFade(1.0f - t);
standIn->setFadeInverted(true); // opposite half of the pattern
Material slots
A mesh often wants more than one material: a crate with a metal lid, a wall and its roof, a vehicle body and its glass. Splitting it into separate items to get them is the wrong trade — one mesh becomes two objects, with two transforms, two bounding spheres and two culling tests, and the seam between them has to be kept aligned by hand.
Instead the index buffer is carved into ranges, each with its own texture, drawn back to back from the same vertex and index buffers. This is the usual arrangement elsewhere too: submeshes, material slots, glTF primitives.
// faces are laid out [sides ... | lid ...] in the index buffer
OkItem *crate = new OkItem("crate", verts.data(), (long)verts.size(),
idx.data(), (long)idx.size());
crate->addMaterialFromFile(0, sidesCount, "assets/wood.png");
crate->addMaterialFromFile(sidesCount, lidCount, "assets/metal.png");
Rules:
- Ranges are drawn in the order added, and should cover the index buffer without overlapping. Nothing enforces it: a gap simply never draws, an overlap draws twice.
- Adding no slots leaves the item single-material, drawing its whole
index buffer with
texture. That is the common case and costs nothing. - Each slot holds its own texture reference, released with the item, on the same terms as texture ownership below.
- A slot with an empty path draws its range in the item's fill colour.
- Everything that is per item — the transform, the frustum test, the fade, the lights near it — is done once for the whole mesh, whatever the slot count. Only the texture bind, the material's state and the draw call repeat.
- Every slot is drawn with the item's material unless it has been given one of its own: see below.
Materials and blend modes
Apart from its texture, a surface is drawn with a material
(OkMaterial, in okinawa/item/material.hpp): how it blends with what
is behind it, whether the lights model it, the colour and opacity it is
tinted with, and its tint mask with the colours of its three zones.
| Field | What it is |
|---|---|
OkBlendMode blend |
How its pixels meet what is behind them (below). Opaque by default. |
bool unlit |
Skip the lights and the scene's tint. |
std::array<float, 4> tint |
Multiplied over the texture; the alpha is the opacity. |
slotTint, slotLuminance |
The colour each zone of the tint mask takes, and how it is applied. |
tintMaskName |
The tint mask, by path. |
An item has one material, set through its own methods (setBlendMode,
setOpacity, setTintColor, setUnlit, setTintMask,
setMaterialTint...). That is all a single-material item needs, and it
is what every range of a many-material item wears by default.
How a material blends is one property with four answers, because the four exclude each other:
| Mode | What it does |
|---|---|
OK_BLEND_OPAQUE |
Solid. The texture's alpha takes no part in whether a pixel is drawn. |
OK_BLEND_CUTOUT |
All or nothing: a pixel under half covered is not drawn, the rest are solid. No blending, no sorting. |
OK_BLEND_ALPHA |
Seen through: mixed with what is behind it by the pixel's coverage times the material's opacity. Glass, water, an overlay. |
OK_BLEND_ADDITIVE |
Adds light to what is behind it. A halo, a glow. |
In every mode a texture's alpha means one thing: coverage. How much of the pixel the material covers. The mode says what is done with it, and nothing else is ever stored there -- a code written into an alpha channel does not survive filtering.
The two blended modes are drawn after everything solid, do not write depth, and are left out of the shadow maps: what is seen through, or is light, is not matter. See the scene's two passes.
// An overlay laid over the ground, two thirds there.
overlay->setBlendMode(OK_BLEND_ALPHA);
overlay->setOpacity(0.66f);
A range of faces can have a material of its own, which is what lets one object be made of things that are drawn differently: a window whose frame is solid and whose pane is seen through, a lamp whose post is lit and whose bulb is not.
OkItem *window = new OkItem("window");
window->addMesh(frame.data(), frameFloats, frameIdx.data(), frameCount,
"assets/frame.png");
window->addMesh(pane.data(), paneFloats, paneIdx.data(), paneCount,
"assets/pane.png");
window->upload();
OkMaterial glass;
glass.blend = OK_BLEND_ALPHA;
glass.tint[3] = 0.35f; // opacity
window->setRangeMaterial(1, glass);
Ranges count in the order they were added. From then on that range ignores the item's material. The item is drawn in both passes, each time only the ranges that belong there: the frame with the solid geometry, the pane with the blended. It is still one object -- one transform, one bounding sphere, one culling test.
A material handed to setRangeMaterial is copied. Its tint mask is taken
by name (tintMaskName): the item loads it and holds the reference, as
it does for its own.
Blended surfaces are drawn furthest first, since each is mixed with
what is already behind it. Between objects the order is by root object;
inside one item, its blended ranges are ordered by the middle of each
range as the pass's view sees it, so two panes of one object come out
right from either side. A pass that sends its own view matrix to the
program says so with OkItem::setPassView(view); the engine's frame,
interface and preview passes do. Two ranges that cross each other have no right
order, and are drawn by their middles like any others.
An untextured surface takes its material too. With no texture the fill colour is multiplied by the tint, so a flat-coloured range can be tinted and seen through like a textured one.
Assembling one from pieces
The example above knows where each material's faces sit in the index
buffer, because it built the buffer itself. When the pieces arrive
separately -- a mesh per material, each indexed from its own first
vertex -- use addMesh, which concatenates the geometry and moves each
piece's indices along by what is already there:
OkItem *crate = new OkItem("crate"); // empty, no geometry yet
crate->addMesh(sidesV.data(), (long)sidesV.size(),
sidesI.data(), (long)sidesI.size(), "assets/wood.png");
crate->addMesh(lidV.data(), (long)lidV.size(),
lidI.data(), (long)lidI.size(), "assets/metal.png");
crate->upload(); // hands it to the GPU, once
Each piece becomes one range, so this is the same item as before, built
the other way round. Nothing is drawn until upload().
It exists because that index arithmetic is easy to get wrong and does not announce itself: an index that is off but still inside the buffer draws the wrong triangles rather than failing. Callers that merged meshes by hand each wrote it out again.
Because a vertex carries one set of texture coordinates, vertices on the seam between two materials usually have to be split: same position, one copy per material, each with its own UV. That is normal — the same reason a cube corner needs three vertices for its three normals — and it costs vertices, never triangles.
Texture ownership
Both texture setters leave the item holding one reference in
OkTextureHandler, released when the item is destroyed. setTexture
therefore adds a reference even though the caller already has the
pointer: a texture shared by many items (a sprite sheet, the font atlas)
would otherwise be freed by the first item to die, leaving the rest with
a dangling pointer.
The consequence for callers: code that creates a texture and hands it to a single item should release its own creation reference straight after, so destroying the item frees the texture.
OkTexture *tex = OkFont::bake("label", "TEXT", 8, fg, bg);
billboard->setTexture("label", tex);
OkTextureHandler::getInstance()->removeReference("label"); // item owns it now
Code that keeps the texture alive for its own use (a sheet, a cached atlas) simply keeps its reference and does nothing extra.
Tint masks and cutouts
One image often has to serve many objects in different colours: the same door with a different frame on every house, the same sign lit in a different colour at night. Duplicating the image per colour wastes memory and multiplies the draws; a tint mask recolours zones of one image per item instead.
The mask is a second texture, laid over the same texture coordinates as the item's own. Its red, green and blue channels are weights: how much of each pixel belongs to material slot 0, 1 and 2. Each zone takes its slot's tint:
item->loadTextureFromFile("assets/door.png");
item->setTintMask("assets/door-tint.png");
item->setMaterialTint(0, 0.35f, 0.20f, 0.12f); // red zone: the frame
item->setMaterialTint(1, 1.00f, 0.85f, 0.55f); // green zone: the glass
item->setMaterialLuminance(1, true); // lit: the tint is the hue
- A slot's tint (
setMaterialTint) multiplies the drawing within its zone, so the artwork's shading survives: a white tint leaves the zone as drawn. - Luminance mode (
setMaterialLuminance(slot, true)) keeps only the drawing's luminance and lets the tint set the hue. That is what an emissive surface wants: the artwork supplies the shading, the tint the colour of the light. Off, the default, the tint multiplies the drawing as it is. - Where the weights add up to less than one, the rest of the pixel keeps the drawing's own colour, so a mask only needs to paint the zones that change. Where they add up to more, they are scaled back to one.
The mask is loaded like any other texture -- through the texture cache, shared between the items that name it, mipmapped and linearly filtered -- and the shader samples it with the same filtered lookup as the colour, so the drawing and its weights agree at every distance.
Why weights and not codes. An easier-looking encoding writes a code per pixel -- 0.25 for one zone, 0.5 for the next -- in a single channel. A code is a label, and a texture unit does not know that: the mipmaps and the linear filter average every pixel with its neighbours, and the average of two labels is a third label or none at all. Where two zones meet the pixel changes zone, and where a zone meets empty space it reads as some other zone or as a hole. A weight survives the same filter, because halfway between a pixel that is all one zone and one that is all the next really is half of each, which is also the colour the eye expects there.
Alpha cutout. With setAlphaCutout(true) -- the blend mode
OK_BLEND_CUTOUT -- the texture's alpha is coverage taken as all or
nothing: a pixel whose filtered alpha is under one half is not drawn.
That is how a grille, a leaf or a railing is cut out of a quad without
blending or sorting, and it is independent of the tint mask. Off, which
is the default, the alpha decides nothing about which pixels are drawn.
The threshold is half because the filtered edge of a hole crosses it
halfway between the last covered texel and the first empty one, so the
cut lands where the drawing put it.
A cutout at a distance. Ordinary mipmaps average coverage away: a
grille of bars one texel wide in six is a sixth covered, and from the
level where a texel spans a bar and its gap the average is under one
half and every bar is dropped. OkTexture::keepCutoutCoverage()
rebuilds the mipmaps so each level passes the cutout over as many
texels as the full image does, scaling alpha up (never down) and
averaging colour over the covered texels only; the grille thins with
distance instead of vanishing. Its second argument keeps the share
square by square rather than over the whole image, which is what an
atlas wants: over the whole of it, solid images beside the grille gain
coverage as their padding spreads, that gain pays for the grille's
loss, and the grille is dropped anyway.
OkTexture *atlas = OkTextureHandler::getInstance()->getTexture("assets/atlas.png");
atlas->keepCutoutCoverage(OK_ALPHA_CUTOUT_THRESHOLD, 64); // 64-texel squares
OkTextureHandler::getInstance()->removeReference("assets/atlas.png");
It reads the image back from the GPU, so it runs where the context is current, once per texture (later calls return at once), and logs what it cost.
Padding an atlas. When several images share one texture, each cell
has to be padded with copies of its own edge -- colour, alpha and tint
weights alike -- and never with the atlas background. A filter reaching
past the cell then reads more of the cell. Padded with background, the
lower mipmaps mix the cell with the empty space around it, and a solid
surface seen from afar or at a slant comes out pierced along every
cell edge. How wide the padding needs to be depends on how far the
image is seen from: each level doubles how far the filter reaches, so
a padding of p pixels keeps level L inside the cell while
1.5 * 2^L - 0.5 <= p -- 8 pixels cover levels 0 to 2, 24 cover 0 to
4. Past that, the far levels blend neighbouring cells again, and
OkTexture::setMaxMipLevel(L) stops the chain at the last level the
padding covers: a little aliasing at a great distance instead of
colours bleeding across and holes along every edge.
OkTexture *atlas = OkTextureHandler::getInstance()->getTexture("assets/atlas.png");
atlas->setMaxMipLevel(4); // padded 24 px: levels 0..4 stay in their cell
OkTextureHandler::getInstance()->removeReference("assets/atlas.png");
Say it where the texture is named. Neither the cutout nor the tint mask can be read off the image: the shader is told, per item, and an item that is not told reads the alpha as nothing and draws the texture as it is, holes filled with whatever colour the empty pixels carry. Since the answer belongs to the texture rather than to each piece of geometry drawn with it, keep it in one place beside the texture's name and have every item ask there, rather than setting it at every call site where one of them can forget.
Debug helpers
Every object can draw a few things about itself on top of the thing it is: the axes at its origin, the sphere it claims to fit in, the box around that sphere, and where that sphere is centred. They answer "where is this actually", and that question only has an answer when a whole world answers it at once — one object's axes say nothing about whether it is anchored the way its neighbours are.
The switches are config keys (debug.origins, debug.spheres,
debug.boxes, debug.centres) and each object reads them while it
draws itself:
OkConfig::setBool("debug.spheres", true); // or `set debug.spheres 1`
item->setDrawOriginAxis(true); // this one, switch or not
Two decisions worth knowing, because both were mistakes first:
- Nothing walks the scene switching a flag on in every object. That is the scene knowing about gizmos, which is not its business, and it is state to keep in step with: the next object attached after the walk is not in step. The switches are read once a frame into plain booleans — a config lookup by string, fourteen thousand times a frame, is milliseconds spent on a switch that is usually off.
- An object adds lines to a buffer; it does not draw them. One draw call for the frame, and one pair of GL buffers, instead of one of each per object: the first version created and destroyed a vertex array for three lines, per object, per frame.
A subclass extends drawDebugHelpers() with whatever only it knows —
OkItem adds its sphere and its centre, because the base knows where it
is but not how far it reaches. The axes are scaled to the object's own
radius, since a lamp post and a district cannot share one length and
both be legible.
A mesh shared between items
An item owns its geometry: it keeps the vertices in memory and uploads buffers of its own. That is right for a mesh drawn once and wrong for a drawing that stands in a thousand places, because the same triangles then arrive once per item that draws them -- in memory and on the card -- and each of those uploads is rounded up to a page whether it needs one or not.
The constructor that takes a mesh key shares instead:
OkItem *lamp = new OkItem("lamp_42", "street_lamp",
verts, vertexCount, idx, indexCount);
The key names the DRAWING, never the drawer: two items asking for
street_lamp mean the same triangles, and an item that put its own name
in would share nothing with anybody. The first caller uploads; every
later one gets what is there, so the vertices may be a temporary. The
store (OkMeshHandler) is reference counted like the textures, and the
geometry goes when the last item drawing it does.
What stays with the item is everything that is about the drawer rather than the drawing: its transform, its materials and tints, its visibility -- and its own vertex array, which is where an instanced item hangs the buffer of where its copies stand. Sharing one of those would hand every item the last one's instances.
A shared mesh is read-only: addMesh and updateVertexData refuse and
say so, because growing it here would grow it under everybody else
drawing it.
Measured on one scene of a hundred and twenty thousand repeated pieces: 12512 groups holding 75 distinct meshes between them went from 800 MB to under 100 MB.
OkInstancedItem
An instanced item is an OkItem, and it draws with the same material
state: the texture, the blend mode and opacity, the tint mask and the
three material tints all apply. It is the item's one material: an
instanced item has no ranges.
The colours of the tint mask's zones can differ per instance.
setInstanceMaterialTint(index, slot, r, g, b) gives one instance its
own colour for a zone, and clearInstanceMaterialTints(index) puts it
back on the item's. The zones an instance does not name keep the item's
colours. Only the colours vary -- the mask, the texture, the blend mode
and each zone's luminance mode stay the item's, so a variation of those
is an item of its own -- and the draw is still one call: the colours
travel with each instance's transform. An item none of whose instances
is tinted uploads nothing extra.
int lit = panes->addInstance(x, y, z, yaw);
panes->setInstanceMaterialTint(lit, 1, 1.0f, 0.85f, 0.55f);
An instance is placed within its item, not in the world: the item's own transform applies on top, so an instanced item can hang off a parent and move with it, and the instance positions can be written relative to whatever owns them. Each instance is frustum-tested where it actually stands, through that transform.
One mesh drawn many times in a single draw call: the base OkItem holds
the mesh (uploaded once, and shareable with other items -- see above),
and this subclass adds a per-instance
buffer of world transforms (position, uniform scale, Y rotation) wired
with an attribute divisor. A thousand copies cost one draw call instead
of a thousand items, which is what makes large numbers of repeated
objects affordable.
Instances are addressable, not anonymous triangles — each one can be moved, hidden or removed at runtime, and the buffer is recomposed cheaply. The mesh is authored around its own origin and each instance places a copy of it. Instances are frustum-culled individually -- the mesh bounding sphere at each instance position, taken through the item's transform, so an instance is tested where it actually stands -- and only what is on screen is uploaded and drawn.
The logical side of an object — state, collision, its light — belongs to the game, never to this class: collision never comes from render triangles.
| Method | Purpose |
|---|---|
OkInstancedItem(name, vertexData, vertexCount, indexData, indexCount, stride = 5) |
Same mesh contract as OkItem. |
int addInstance(x, y, z, yaw = 0, scale = 1) |
Place a copy; returns its index. |
void setInstance(index, x, y, z, yaw, scale) |
Move an existing instance. |
void setInstanceVisible(index, bool) |
Hide/show one instance. |
void clearInstances() |
Drop every instance. |
int getInstanceCount() const / int getDrawnCount() const |
Total instances, and how many survived frustum culling last frame. |
OkSpriteSheet
One texture holding many named regions. The image is uploaded once (through the texture handler, so it is refcounted and shared like any other texture) and the regions are metadata: rectangles with a name. A region is never a texture of its own — that is the whole point of a sheet: one upload, one bind, and the chance to draw many different pieces in a single call.
The description file is read in the Aseprite / TexturePacker JSON dialect, which is what pixel-art and packing tools emit. An artist can redraw or repack the sheet in their tool of choice, export, and the application picks it up with no code change. Only the parser speaks that vocabulary; the API calls the pieces regions, and reserves the word frame for actual animation (each region's duration and the sheet's tags are kept for that).
| Method | Purpose |
|---|---|
bool load(jsonPath, imageOverride = "") |
Read the description and the image it names. |
OkTexture *getTexture() const |
The shared GPU texture, to hand to items. |
const OkSpriteRegion *getRegion(name) const |
A region's pixel rect and ready-to-use UVs, or null. |
bool hasRegion(name) const |
Whether a region exists. |
std::vector<std::string> getRegionNames() const |
Every region, in sheet order. |
std::vector<std::string> getGroup(tag) const |
The regions covered by a tag, for picking a member of a family. |
int getWidth() / getHeight() const |
Sheet size in pixels. |
OkSpriteRegion carries x, y, w, h in pixels and u0, v0, u1, v1
ready for a quad, already accounting for the engine loading textures
flipped for GL.
OkItemGroup
| Method | Purpose |
|---|---|
OkItemGroup(name) |
Construct an empty group. |
void addItem(OkItem *item, const std::string &tag) |
Add an item with a tag. |
std::vector<OkItem *> getItemsWithTag(const std::string &tag) |
Items carrying a tag. |
int getItemCountWithTag(const std::string &tag) |
Count items with a tag. |
void setVisible(bool) |
Show or hide every item. |
void setWireframe(bool) |
Wireframe every item. |
A group is walked in both passes of a frame and each member draws in the pass it belongs to, so a member seen through is drawn after every solid one, in the group and out of it.
Example
float vertices[] = {
// position // uv
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
};
unsigned int indices[] = { 0, 1, 3, 1, 2, 3 };
OkItem *quad = new OkItem("quad", vertices, 20, indices, 6);
quad->setPosition(0.0f, 0.0f, -5.0f);
quad->setWireframe(true);
scene->addObject(quad);
OkBillboard
OkBillboard is an OkItem subclass: a rectangular quad that always
renders facing the active camera (a classic billboard). The quad is
centred on the item's position, width along X and height along Y;
each frame the item aligns itself with the view plane of
OkCore::getCamera() (it adopts the camera's pitch and yaw, roll stays
0), so its content stays screen-aligned at every angle -- including
straight under a top-down camera, where position-based facing
degenerates into an arbitrary roll. Texturing, visibility and every
other OkItem method work unchanged. Billboards are expected to live
at scene root (not attached under a transformed parent).
| Method | Purpose |
|---|---|
OkBillboard(name, width, height) |
Construct a width x height camera-facing quad. |
void setCameraOffset(float metres) |
Depth bias: draw the quad metres closer to the camera along the view ray. |
void setProximityFade(float nearDist) |
Fade the tint alpha out as the camera approaches (0 disables). |
static OkRotation facingRotation(from, to) |
The rotation that points a +Z quad at from toward to. |
Camera offset (depth bias)
A billboard placed at a solid object intersects it: the quad is flat, the object is not, and the sprite gets sliced along a hard straight edge wherever the geometry pokes through. Typical cases are a glow sprite centred on the thing that emits it, or a marker pinned to a character's head.
setCameraOffset(metres) draws the quad a short distance toward the
camera along the view ray, recomputed every frame, so it wins the depth
test against the object it belongs to.
Because the displacement follows the view ray, a point moved along it projects to the same pixel: the quad does not shift on screen at all, only its depth changes. Orbiting the object keeps the sprite exactly where it was from every angle.
setPosition() keeps meaning "the anchor": the offset is applied on top
of it each frame and never accumulates, and moving the billboard from
outside re-anchors it automatically.
Its limit: it only resolves intersections with geometry nearer to the anchor than the offset. For sprites that must blend against arbitrary geometry, a depth bias is not enough: those cases need the sprite to fade by its depth difference with the scene.
OkBillboard *label = new OkBillboard("label", 8.0f, 4.0f);
label->setPosition(10.0f, 30.0f, -20.0f);
label->setTexture("label-tex", texture); // any OkTexture
scene->addObject(label);