Core
OkCore owns the window, the OpenGL context, the cameras and the main loop. It is a static class (the constructor is deleted), so every entry point is called through OkCore::. It also exposes the engine's scene handler, input and (optionally) the in-engine MCP server.
OkCamera is a transformable view onto the scene. It derives from OkObject (see Items for the shared transform API) and adds projection and view matrices.
OkCore methods
| Method | Purpose |
|---|---|
static bool initialize() |
Create the window and OpenGL context. Returns false on failure. The window is brought inside the desktop's usable area, frame included (see below). |
static void loop(step, draw) |
Run the main loop, calling the step and draw callbacks each frame. |
static void askForExit() |
Request the loop to end (typically from the step callback). |
static void exit() |
Tear down and exit. |
static OkSceneHandler *getSceneHandler() |
Access the scene handler. |
static OkCamera *getCamera() |
The current camera. |
static OkInput *getInput() |
The input subsystem. |
static void addCamera(OkCamera *camera) |
Register an additional camera. |
static void switchCamera(int index) |
Make the camera at index current. |
static OkCamera *getCameraAt(int index) |
Camera at index (nullptr out of range). |
static int findCamera(const std::string &name) |
Index of the camera registered with that name, -1 when not found. |
static void enableMcpServer(int port = 8765) |
Start the in-engine MCP server (see MCP server). |
static void setIgnoreUserInput(bool ignore) |
Ignore physical input (MCP-driven instances). |
static void setOverlayCallback(cb) |
Draw over the finished frame, just before the swap. |
static void setExitCallback(cb) |
Run something once when the loop ends, before anything is torn down. |
static void setCloseCallback(cb) |
Say whether a close may go through; false takes it back. |
static bool setWindowIcon(const std::vector<std::string> &pngPaths) |
Give the window an icon, from square RGBA PNGs at several sizes. |
The loop callbacks share the signature void(float deltaTime).
Drawing inside the frame or over it
The draw callback given to loop() runs inside the frame, before the
post-process composite. Anything it draws belongs to the world and is
treated as such: bloomed, fogged, and blurred by depth of field along
with everything else. That is what you want for a debug overlay meant to
sit in the scene, and wrong for an interface meant to be read.
setOverlayCallback() runs after the composite and after the
interface pass, with the default framebuffer bound. Use it for anything
painted on the finished image -- an immediate-mode interface library,
for instance, which draws its own geometry and expects the final target.
OkCore::setOverlayCallback([](float dt) {
(void)dt;
// draw over the finished frame
});
OkCore::loop(stepCallback, drawCallback);
Pass an empty function to remove it. The order within a frame is: scene, draw callback, post-process composite, cameras, interface pass, overlay callback, swap.
Icons, and where an application is run from
setWindowIcon takes the same picture at several sizes and lets the
window system choose; a 32 and a 64 covers a title bar and a task bar.
Drawing each size separately beats handing over one large one, because
an icon is mostly seen small and a downscaled large one goes soft.
On macOS it lands on the Dock tile. Windows there carry no icon at
all, so glfwSetWindowIcon has nothing to set — the application's icon
on that platform is the tile, and AppKit is the only way to it. The
engine does that for you from the same call, so an application asks once
and needs no #ifdef.
A macOS bundle says the same thing statically —
Contents/Resources/<name>.icns, named by CFBundleIconFile in
Info.plist — and that is what the Finder and the application switcher
read, which the runtime call cannot reach. The two are complementary:
the bundle for an application that is installed and double-clicked, the
call for a binary run straight out of a build directory, which has no
bundle to be read from.
Elsewhere the packaging differs again: Windows wants the icon compiled
into the executable as a resource for the Explorer to show, and Linux
desktops read a .desktop entry and the hicolor theme. One picture,
three packaging stories, one call at runtime.
Packaging an application as a macOS bundle changes one more thing: a
bundle opened from the desktop starts with a working directory of /,
and GLFW moves it again on its own, into Contents/Resources. The
engine turns that off (GLFW_COCOA_CHDIR_RESOURCES) — where an
application works from is the application's decision — and instead the
asset search settles it: it looks up from the working directory first,
and then up from the executable's own directory, moving the working
directory to what it finds. An application started from anywhere
therefore resolves its own relative paths — its data, its assets, the
files it writes — against its project, exactly as it does in
development. OkFiles::executableDirectory() is the piece that answers
where the binary itself is, asked of the operating system rather than of
argv[0], which a caller can set to anything.
The last thing an application does
Most applications have something to write down on the way out — where
the user was, what they had open — and the obvious places to do it are
both wrong. After loop() returns is too late: it calls exit() before
returning, and that deletes the scene, the input and every camera, so an
application asking where its camera was is asking a deleted object.
Doing it every frame in case this one is the last is paying, sixty times
a second, for something that happens once.
setExitCallback() runs in between: once, on the loop thread, with the
loop stopped and the scene still intact. It does not care what ended the
loop — the window's close button, askForExit(), or the MCP
quit tool all arrive here.
OkCore::setExitCallback([]() {
saveWhereverTheUserWas(); // the camera is still alive here
});
Nothing runs when the process is killed from outside. Nothing can.
Taking a close back
A close is a request, not an event. An application holding something nobody has said what to do with — an unsaved document, a half-finished upload — has to be able to ask before it goes, and the exit callback is a frame too late for that: the loop has already stopped, so there is no frame left to put a question on.
setCloseCallback() is asked once per frame in which a close is pending,
after the window system has been polled and after any queued tool
commands have run, so one answer covers every way out: the close button,
askForExit() and the MCP quit tool. Return
false and the close is taken back and the loop carries on; the
application asks again with askForExit() once it has its answer.
OkCore::setCloseCallback([]() {
if (nothingIsPending()) {
return true;
}
askTheQuestion(); // a modal drawn on the frames that follow
return false;
});
OkCamera methods
| Method | Purpose |
|---|---|
OkCamera(name, width, height) |
Construct a camera for a given framebuffer size. |
void setPerspective(fovDegrees, near, far) |
Set the projection: a vanishing point, things further away drawn smaller. |
void setOrthographic(worldHeight, near, far) |
Set the projection to a box worldHeight metres tall, the width following the aspect ratio. No vanishing point. |
bool isOrthographic() const |
Which of the two the camera is on. |
float getOrthoHeight() const |
How much of the world fits vertically; changing it with setOrthographic is what zooming means in this projection. |
const glm::mat4 &getView() const |
The view matrix. |
const glm::mat4 &getProjection() const |
The projection matrix. |
float viewDistance() const |
How far the camera sits from what it observes (orbit distance, overhead height); 0 when the notion does not apply. |
void setViewDistance(float d) |
Write counterpart of viewDistance(): drive the distance/height directly. Base ignores it; subclasses apply and clamp. |
OkRay rayThroughPixel(x, y, width, height) const |
The ray through a point on the surface being drawn to. |
bool pixelOfPoint(const OkPoint &world, width, height, double *outX, double *outY) const |
Where a world point lands on it, or false when the point is behind the camera. |
OkCamera also inherits setPosition, setRotation and the rest of the OkObject transform API.
Two projections, and what zooming means in each
A perspective camera zooms by moving: closer is bigger, because the
further something is the smaller it is drawn. An orthographic one
cannot -- there is no vanishing point, so moving towards a thing
changes nothing about its size. What changes is how much world the
box holds, which is setOrthographic's first argument, and a camera
driven the other way appears frozen while its position marches past
what it is looking at.
rayThroughPixel and pixelOfPoint work under both without being
told which: they unproject through the matrix rather than assuming
rays meet at the eye.
Between the screen and the world
rayThroughPixel and pixelOfPoint are inverses, and they are documented
together because they only mean anything as a pair: a projection and an
unprojection that disagree about which way y counts read as a picking bug
for an afternoon. Both count y downwards from the top, the way a
window system reports a cursor, and both take the size of the surface
being drawn to rather than reading the camera's aspect ratio — so a camera
rendering into an offscreen target answers about that target's pixels.
The ray is built by unprojecting at the near plane and again at the far one and subtracting, rather than from the camera's own position. That is right for a perspective camera and for an orthographic one alike: an orthographic ray does not pass through the eye, and the overhead views that most want a cursor are exactly the ones that tend to be orthographic.
Its direction is a unit vector, so a distance along it is a distance in
world units. See OkRay for what to do with
it, and OkItem::intersectRay for asking an
object whether the ray crosses it.
Cameras are identified by the name they are constructed with: findCamera resolves names to indices, and the MCP view tool selects cameras by that same name. Indices (and the number-key switching in the stock input handler) are an internal/debug detail.
Example
if (!OkCore::initialize()) {
return 1;
}
OkCamera *camera = OkCore::getCamera();
camera->setPosition(0.0f, 0.0f, 5.0f);
camera->setPerspective(45.0f, 0.1f, 1000.0f);
OkCore::loop(stepCallback, drawCallback);
A window is opened at a size the screen can show
window.width and window.height ask for the size of what the
application DRAWS. What the desktop has to find room for is that plus
the window's frame -- the title bar above it and whatever the platform
puts around the rest -- so a window asked for at exactly the usable
height opens with its lower edge, and everything drawn along it, below
the bottom of the screen. It cannot be dragged back into view either:
the title bar is the handle, and the title bar is the part that is
still on screen.
So once the window exists, initialize() measures its frame, compares
it against the monitor's usable area and, where it does not fit, sets
the drawn size to what is left and moves the window under the frame at
the area's own corner. A window that already fits is left exactly as it
was asked for, so the same configuration opens at the size requested on
a large screen and shrinks only where it has to. The frame can only be
measured once there is a window, which is why this happens after one is
made rather than being subtracted from the size asked for.