-
Notifications
You must be signed in to change notification settings - Fork 0
Writing a game
A game is three callbacks and a scene. There is no library to link and no package to install: games compile the engine's sources directly, magnolia-style.
#include <stdio.h>
#include <stdlib.h>
#include "../../external/sokol/sokol_app.h"
#include "../../hologram.h"
static HoloGpuScene gpu;
static HoloScene scene;
static char shader_src[65536];
sapp_desc sokol_main(int argc, char *argv[]) {
scene = (HoloScene){
.spheres = { { .center = {0, 1, 0}, .radius = 1,
.albedo = {0.85f, 0.25f, 0.35f} } },
.sphere_count = 1,
.has_floor = 1,
.floor_a = {0.85f, 0.85f, 0.85f}, .floor_b = {0.25f, 0.3f, 0.35f},
.sun_dir = {-3.0f/7, 6.0f/7, 2.0f/7},
.horizon = {1.0f, 0.9f, 0.8f}, .zenith = {0.25f, 0.45f, 0.9f},
};
HoloCamera cam = holo_camera_make(hv3(0, 1.6f, 5), hv3(0, 1, 0),
hv3(0, 1, 0), 55.0f, 1.0f);
holo_gpu_scene_fill(&gpu, &scene, &cam, 0); /* 0 = RGB, 1 = spectral */
FILE *f = fopen("shaders\\trace.hlsl", "rb");
size_t n = fread(shader_src, 1, sizeof shader_src - 1, f);
shader_src[n] = 0;
fclose(f);
return holo_display_app(&(HoloDisplayDesc){
.title = "my game",
.fs_source = shader_src,
.uniforms = &gpu,
.uniforms_size = sizeof gpu,
});
}sokol owns main(), so a game's entry point is sokol_main() returning
holo_display_app(). The shader is read from disk at startup, which means you
can edit the tracer and relaunch without recompiling.
The camera's aspect argument is a placeholder here (1.0f). The shader
derives the real aspect from the framebuffer size, so the image stays correct
when the window is resized. It matters only when you build a camera for the
oracle, which renders at a known size.
Three callbacks, filled in as needed:
before_frame |
Simulate, then write the camera into your uniform block. |
after_frame |
Runs after the draw: readback, diffing, quitting. |
event_cb |
Every sokol event; hand it to holo_input_event. |
static HoloWalkWorld world;
static HoloWalker walker;
static HoloInput input;
static float yaw = 3.14159f, pitch;
static HoloCamera camera(void) {
HoloV3 eye = hv3(walker.pos.x, walker.pos.y + 1.55f, walker.pos.z);
HoloV3 fwd = hv3(sinf(yaw) * cosf(pitch), sinf(pitch),
cosf(yaw) * cosf(pitch));
return holo_camera_make(eye, hv3_add(eye, fwd), hv3(0, 1, 0), 70.0f, 1.0f);
}
static void before_frame(void) {
float dx, dy;
holo_input_look(&input, &dx, &dy);
yaw -= dx * 0.003f;
pitch -= dy * 0.003f;
if (pitch > 1.5f) pitch = 1.5f;
if (pitch < -1.5f) pitch = -1.5f;
timestep_advance((float)sapp_frame_duration());
for (int s = 0; s < timestep_steps(); s++) {
float fx = sinf(yaw), fz = cosf(yaw);
float f = (float)(holo_input_held(&input, SAPP_KEYCODE_W)
- holo_input_held(&input, SAPP_KEYCODE_S));
float r = (float)(holo_input_held(&input, SAPP_KEYCODE_D)
- holo_input_held(&input, SAPP_KEYCODE_A));
walker.vel.x = (fx * f - fz * r) * 3.5f;
walker.vel.z = (fz * f + fx * r) * 3.5f;
if (walker.grounded && holo_input_held(&input, SAPP_KEYCODE_SPACE)) {
walker.vel.y = 6.0f;
}
holo_walk_step(&walker, &world, timestep_dt());
}
HoloCamera cam = camera();
holo_gpu_scene_fill(&gpu, &scene, &cam, spectral_on);
}
static void event_cb(const struct sapp_event *ev) {
holo_input_event(&input, ev);
}Call timestep_set_hz(120) once at startup. Mouse look is per frame;
walking is per step. The input module handles capture itself (click to
lock the mouse, Escape to release) and clears held keys on focus loss so
nothing sticks.
Build it by adding a line to build.bat alongside the examples; the pattern is
%PURE% plus display.c, oracle.c and input.c.
Any game can hold its own frames to the CPU tracer, and it costs about ten
lines. Add a --diff mode: render a few frames, then compare and exit with
the verdict.
static void after_frame(void) {
frames_drawn++;
if (diff_mode && frames_drawn == 5) {
/* The oracle camera needs the REAL aspect. */
HoloCamera c = camera();
c = holo_camera_make(c.pos, hv3_add(c.pos, c.forward), hv3(0, 1, 0),
70.0f,
(float)sapp_width() / (float)sapp_height());
HoloOracleStats st;
int ok = holo_oracle_diff(&scene, &c, spectral_on, &st);
printf("DIFF %s: mean %.4f/255, %.3f%% outliers\n",
ok ? "OK" : "FAIL", st.mean, st.outlier_pct);
exit(ok ? 0 : 1);
}
}Wait five frames or so, past the swapchain's first-present wrinkles. Spectral CPU rendering takes a few seconds at 640×480, which is expected; the oracle is allowed to be slow.
If it fails, both frames land in build\diff_gpu.ppm and build\diff_cpu.ppm.
holo_gpu_scene_fill(&gpu, &scene, &cam, spectral) picks per frame:
- 0, RGB. Three-channel throughput, glass at its D-line index. Correct for any scene with no dispersive or polarizing element, and much cheaper.
- 1, spectral. Twelve wavelengths, Stokes vectors, Mueller matrices. Required for dispersion, polarizers, waveplates and gratings.
The interactive examples bind this to T, which is a good habit: toggling it
is the fastest way to see which of your effects are real optics and which are
just materials.
Design the scene in physical units. Metres and real indices of refraction.
A dish's focal length really is curv_r / 2; if you want the focus at eye
height on a walk line, solve for the apex position rather than nudging.
The sun disk is a rendering choice, not a light. It changes what a ray
sees when it escapes; it does not change Lambert shading, which always uses
sun_dir. Turn it on when reflections should be able to find the sun.
Neutral albedos are exact in both pipelines; saturated ones are not. A gray scene renders identically through RGB and spectral. Strongly coloured albedos survive the wavelength round trip only approximately, which is fine for art but not for colorimetry.
Collision is a second, simpler world. Nothing derives it from the render scene. Mirror walls are paper-thin surfaces; give them slab colliders.
Keep the twins twins. If you add an optical effect, it lands in
cpu_trace.c and shaders/trace.hlsl, with the same caps, the same push
order and the same cull threshold. And read
Shader constraints before you write a branch that pushes
more than one ray.
Reference
Guides
History