okinawaengine GitHub

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.