Mesh
Geometry uploaded to the GPU once and drawn many times. Vertices are interleaved and described by a layout: [3, 2] means each vertex is three floats of position followed by two of texture coordinate, bound to shader locations 0 and 1 in that order. Indices select vertices, so shared corners are stored once. Upload cost is paid at construction; Draw() is a single native call regardless of how many triangles it draws. That is deliberate -- per-vertex calls across the native boundary would be orders of magnitude slower.
Example
# a unit quad: position(3) + texcoord(2)
vertices := [-0.5, -0.5, 0.0, 0.0, 0.0,
0.5, -0.5, 0.0, 1.0, 0.0,
0.5, 0.5, 0.0, 1.0, 1.0,
-0.5, 0.5, 0.0, 0.0, 1.0];
indices := [0, 1, 2, 2, 3, 0];
mesh := Mesh->New(vertices, indices, [3, 2]);Operations
- New
- Build
- Corner
- Corner3
- Cube
- CubeInterior
- Draw
- DrawInstanced
- FaceField
- FlatNormal
- Free
- FromObj
- GetCenter
- GetInstanceGeneration
- GetMax
- GetMin
- GetRadius
- GetSize
- IsOk
- Measure
- NoMesh
- Plane
- PositionNormalTexcoord
- Quad
- Resolve
- SetInstanceMatrices
- SetInstances
- Sphere
- Words
Build #
What both constructors do. Objeck constructors cannot delegate to one another, so the body lives here rather than being written twice.
method : private : Build() ~ NilParameters
| Name | Type | Description |
|---|---|---|
Corner # function
Write one interleaved position/normal/texcoord vertex.
function : Corner() ~ NilParameters
| Name | Type | Description |
|---|---|---|
Corner3 # function
Write one face corner from an OBJ "v", "v/vt", "v//vn" or "v/vt/vn" token.
function : Corner3() ~ IntParameters
| Name | Type | Description |
|---|---|---|
Return
| Type | Description |
|---|---|
| Int | the next write offset |
Cube # function
A cube spanning -1..1 on every axis, ready for Scene and Box. Not a "unit" cube: it is 2x2x2, because Box multiplies it by a HALF-extent. A cube built to -0.5..0.5 would draw every box at half the size it claims while collision kept using the full extents. See the note on Box. Culling-safe: every face is wound counter-clockwise seen from outside, so GL_CULL_FACE can be enabled without anything vanishing.
function : Cube() ~ MeshReturn
| Type | Description |
|---|---|
| Mesh | the mesh; check IsOk() |
Cube # function
A cube of an arbitrary half-extent.
function : Cube(half:Float) ~ MeshParameters
| Name | Type | Description |
|---|---|---|
| half | Float | distance from the centre to each face |
Return
| Type | Description |
|---|---|
| Mesh | the mesh; check IsOk() |
CubeInterior # function
A cube seen from the INSIDE: a room, or a skybox. Mesh->Cube is wound counter-clockwise from outside, which is right for a solid object and useless for an enclosure -- put the camera inside one and every wall is back-facing, so GL_CULL_FACE removes the whole room and you are left looking through it. This is the same cube with its winding reversed and its normals turned inward, so it is both visible and lit from within. None of that is discoverable from the outside: building a room out of Cube and wondering where it went is the failure this exists to prevent.
function : CubeInterior(half:Float) ~ MeshParameters
| Name | Type | Description |
|---|---|---|
| half | Float | distance from the centre to each wall |
Return
| Type | Description |
|---|---|
| Mesh | the mesh; check IsOk() |
Draw #
Draw with a different topology. The same vertices and indices read as lines instead of triangles, which is how a wireframe, a debug ray, a grid or a plotted curve gets drawn -- and all of that was unreachable while the draw call hardcoded triangles. Note the indices have to suit the mode: an index list built as triangles (0,1,2, 2,3,0) drawn as GL_LINES pairs them up as (0,1), (2,2), (3,0), which is a mesh's outline plus some degenerate segments rather than a tidy wireframe. Build indices for the mode you mean.
method : public : Draw(mode:Int) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| mode | Int | a DrawMode |
DrawInstanced #
Draw many copies in one call, using the data from SetInstanceMatrices.
method : public : DrawInstanced(count:Int) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| count | Int | how many instances; 1 or fewer draws once, normally |
DrawInstanced #
Draw many copies with a given topology.
method : public : DrawInstanced(count:Int, mode:Int) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| count | Int | how many instances |
| mode | Int | a DrawMode |
FaceField # function
One slash-separated field of an OBJ face token: "12", "12/3", "12//4" or "12/3/4". Walks the string rather than using String->Split, which is wrong for this in two separate ways -- both found by a triangle that loaded successfully and rendered nothing: * A token containing NO separator splits into ZERO parts, not one. So "-3" yielded no fields at all, the position index came back missing, and every vertex of that face was left at the origin. A degenerate triangle draws nothing and raises no error. * Consecutive separators do not produce an empty field. "1//3" splits into "1" and "/3", so the `v//vn` form -- position and normal with no texture coordinate, which is what an exporter writes for an untextured model -- read "/3" as the texture index and lost the normal.
function : FaceField(token:String, index:Int) ~ StringParameters
| Name | Type | Description |
|---|---|---|
| token | String | the face token |
| index | Int | which field, 0 for the position |
Return
| Type | Description |
|---|---|
| String | the field, or an empty string when absent |
FlatNormal # function
Give three consecutive vertices the normal of the plane they lie in, for a file that carried none.
function : FlatNormal() ~ NilParameters
| Name | Type | Description |
|---|---|---|
Free #
Release the buffers. Call it while the GL context is still current -- see the shutdown order on the GL class.
method : public : Free() ~ NilFromObj # function
Load geometry from a Wavefront OBJ file. The gap this fills: every mesh was either a built-in primitive or an array of numbers written by hand, so nothing beyond boxes and spheres had any route in at all. OBJ is the right format to accept first -- it is text, every modelling tool exports it, and it needs no library. Produces the same [3, 3, 2] layout as the primitives, so it draws with the same shaders and no special handling. ## What it reads, and what it ignores Reads `v`, `vt`, `vn` and `f`. Faces may be triangles, quads or larger n-gons -- anything with more than three corners is fanned into triangles, which is correct for the convex faces a modeller produces. Indices may be negative, meaning relative to the end, which some exporters emit. Ignores `mtllib`, `usemtl`, `o`, `g`, `s` and comments. Materials are not read: a Mesh is geometry, and the texture is something the caller binds. That means a multi-material model loads as one mesh with one texture rather than as several -- worth knowing before wondering why a model came out one colour. ## Normals If the file has none -- and plenty do not -- a flat normal is computed per face from its winding. That is why every corner gets its own vertex rather than shared ones being deduplicated: sharing them would average the normals of adjoining faces and turn a hard edge into a smooth one, so a cube would shade like a ball. The cost is three vertices per triangle with no reuse, which for a ten-thousand-triangle model is a couple of megabytes. Deliberate: correct shading on arbitrary input beats a smaller buffer, and a loader that gets this wrong is very hard to diagnose from the picture. ## Winding OBJ is counter-clockwise seen from outside, the same convention as every primitive here, so GL_CULL_FACE works without anything extra. A model that vanishes under culling was exported inside out.
function : FromObj(file:String) ~ MeshParameters
| Name | Type | Description |
|---|---|---|
| file | String | the .obj path |
Return
| Type | Description |
|---|---|
| Mesh | the mesh; IsOk() is false when the file could not be read, held no faces, or was rejected by GL. Note the first two are indistinguishable from here -- GL->GetLastError only sees what the native side was handed, so a missing file and an empty one look the same. Check the path yourself if telling them apart matters. |
Example
model := Mesh->FromObj("ship.obj");
if(<>model->IsOk()) {
GL->GetLastError()->ErrorLine();
return;
};
# fit it in the scene, whatever size the file happened to use
transform := Transform->New();
transform->SetScale(2.0 / model->GetRadius());GetCenter #
The middle of the bounding box. Rarely the origin for a loaded model -- a file's author had no reason to centre it -- so this is what to translate BY (negated) to bring a model to the origin before rotating it, or it will orbit rather than spin.
method : public : GetCenter() ~ Vector3Return
| Type | Description |
|---|---|
| Vector3 | the centre |
GetInstanceGeneration #
How many times this mesh's instance data has been replaced. The instance buffer belongs to the mesh. Anything holding a copy of what it last uploaded can compare this against the value it saw then, and know its data is still there rather than assuming so -- which is what PropBatch does to make a static batch safe when two batches share one mesh.
method : public : GetInstanceGeneration() ~ IntReturn
| Type | Description |
|---|---|
| Int | a counter, starting at 0 |
GetMax #
The high corner of the bounding box.
method : public : GetMax() ~ Vector3Return
| Type | Description |
|---|---|
| Vector3 | the maximum on each axis |
GetMin #
The low corner of the bounding box, in model space.
method : public : GetMin() ~ Vector3Return
| Type | Description |
|---|---|
| Vector3 | the minimum on each axis |
GetRadius #
Distance from the centre to the furthest corner. The number that makes a loaded model usable: a file has whatever scale its author worked in, and a program that hardcodes a scale fits exactly one model. Dividing a target size by this fits any of them.
method : public : GetRadius() ~ FloatReturn
| Type | Description |
|---|---|
| Float | the radius, or 0 for an empty mesh |
Example
transform->SetScale(2.0 / mesh->GetRadius());GetSize #
The width, height and depth of the bounding box.
method : public : GetSize() ~ Vector3Return
| Type | Description |
|---|---|
| Vector3 | the extents on each axis |
Measure #
Work out the bounding box from the vertex data. Assumes the FIRST attribute is the position, which is the convention every primitive here follows and what PositionNormalTexcoord documents. A layout that puts something else first gets bounds of that instead -- there is no way to tell from a list of component counts which one is the position.
method : private : Measure() ~ NilParameters
| Name | Type | Description |
|---|---|---|
New # constructor
Upload geometry.
New(vertices:Float[], indices:Int[], layout:Int[])Parameters
| Name | Type | Description |
|---|---|---|
| vertices | Float | interleaved vertex data |
| indices | Int | triangle indices into the vertex list |
| layout | Int | component count per attribute, in shader location order |
New # constructor
Upload geometry with NO index buffer. The vertices are drawn in the order they are given, which is what a point cloud, a particle buffer, a debug line list and most generated geometry actually want. Building 0, 1, 2, 3, ... by hand purely to satisfy glDrawElements costs a second buffer and a loop to express the default. Which draw call is used is decided by the MESH, not by the caller: one built with indices always draws through them and one built without never does, so there is no way to ask for the wrong one. The vertex order still has to suit the topology -- six vertices drawn as GL_TRIANGLES are two triangles, and drawn as GL_LINES are three separate segments.
New(vertices:Float[], layout:Int[])Parameters
| Name | Type | Description |
|---|---|---|
| vertices | Float | interleaved vertex data |
| layout | Int | component count per attribute, in shader location order |
NoMesh # function
A Mesh that reports IsOk() false, for a load that could not proceed. Empty arrays rather than Nil, because a bare Nil in Objeck infers as Nil rather than as the parameter's array type and will not compile.
function : NoMesh() ~ MeshPlane # function
A flat square on the XZ plane at y = 0, normal pointing up. Floors and ground planes.
function : Plane(half:Float, tiles:Float) ~ MeshParameters
| Name | Type | Description |
|---|---|---|
| half | Float | distance from the centre to each edge |
| tiles | Float | how many times the texture repeats across the square; needs a texture built with TextureWrap->GL_REPEAT to be visible |
Return
| Type | Description |
|---|---|
| Mesh | the mesh; check IsOk() |
PositionNormalTexcoord # function
The layout every primitive above is built to: three floats of position, three of normal, two of texture coordinate, bound to shader locations 0, 1 and 2 in that order. Named rather than written as [3, 3, 2] at each site so a shader and the geometry it reads cannot drift apart silently -- a mismatch there binds the normal to the texcoord slot and produces a plausible-looking wrong picture.
function : PositionNormalTexcoord() ~ Int[]Return
| Type | Description |
|---|---|
| Int | the component counts |
Quad # function
A flat square on the XY plane at z = 0, normal pointing at the viewer. At half = 1.0 this exactly covers clip space, which is what a full-screen pass wants -- a post-process or a background drawn with an identity matrix.
function : Quad(half:Float) ~ MeshParameters
| Name | Type | Description |
|---|---|---|
| half | Float | distance from the centre to each edge |
Return
| Type | Description |
|---|---|
| Mesh | the mesh; check IsOk() |
Resolve # function
OBJ indices are 1-based, and may be NEGATIVE to mean "counting back from the end" -- which some exporters emit and a loader that assumes 1-based reads as a wild out-of-range index.
function : Resolve() ~ IntParameters
| Name | Type | Description |
|---|---|---|
Return
| Type | Description |
|---|---|
| Int | a 0-based index, or -1 when there is none |
SetInstanceMatrices #
Give this mesh a model matrix per instance, so many copies draw in one call. Each instance takes 16 floats, a column-major matrix, exactly as Matrix4 produces -- so an array of N*16 floats is N instances. Bound to attribute locations 3, 4, 5 and 6, because the primitives here use 0, 1 and 2 for position, normal and texture coordinate. Draw it with Shader->LitTexturedInstanced or an equivalent that declares the four mat4 slots. A shader expecting a "model" UNIFORM will not see these: instance data is per-vertex-stage input, not a uniform, which is the whole reason it does not cost a call per object. ## What this is for One draw call instead of one per object. A thousand boxes drawn the ordinary way is a thousand draw calls plus a thousand uniform uploads, and every native call here resolves its symbol by string -- so that cost lands twice. A grid, a particle system, a point cloud, a forest: anything where the same geometry appears many times with different placements. Below a few dozen objects it is not worth the bother. The break-even is lower here than in C, because of that per-call cost, but it is not zero.
method : public : SetInstanceMatrices(matrices:Float[]) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| matrices | Float | N * 16 floats, column-major |
Return
| Type | Description |
|---|---|
| Bool | true when the upload succeeded; GL->GetLastError says why not |
Example
grid := Float->New[count * 16];
for(i := 0; i < count; i += 1;) {
# write a matrix at i * 16
Matrix4->TranslationScaleInto(slot, x, y, z, s, s, s);
};
mesh->SetInstanceMatrices(grid);
# ... then, once per frame ...
mesh->DrawInstanced(count);SetInstances #
Per-instance data with an explicit layout, for anything that is not a model matrix -- a colour per instance, a scalar, a texture index.
method : public : SetInstances(data:Float[], layout:Int[], base_location:Int) ~ BoolParameters
| Name | Type | Description |
|---|---|---|
| data | Float | N * stride floats, where stride is the sum of the layout |
| layout | Int | component counts per attribute, each 1 to 4 |
| base_location | Int | the first attribute location to use; 3 is the first free one after the primitives' position, normal and texture coordinate |
Return
| Type | Description |
|---|---|
| Bool | true when the upload succeeded |
Sphere # function
A UV sphere of radius 1, centred on the origin. Normals are the vertex positions, which is exact for a unit sphere and is why this reads as round under lighting rather than faceted.
function : Sphere(segments:Int, rings:Int) ~ MeshParameters
| Name | Type | Description |
|---|---|---|
| segments | Int | divisions around the equator; 24 or more looks smooth |
| rings | Int | divisions from pole to pole; usually about half the segments |
Return
| Type | Description |
|---|---|
| Mesh | the mesh; check IsOk() |
Words # function
Split on whitespace, dropping the empty pieces a run of spaces produces and stripping a trailing carriage return. Returns Nil for a blank or comment line, so callers can test one thing.
function : Words() ~ Vector<String>Parameters
| Name | Type | Description |
|---|---|---|