Files
suaveplan/packages/foundation/bitpacker/bitpacker-game/src/quantization/quantization.md
2026-05-27 23:47:53 +01:00

4.4 KiB

Game Quantization Helpers

(no submodules)

Purpose

Coordinate-rounding helpers for game-state vectors and rotations. The functions in this module trade absolute precision for compact wire representations: quantizePosition({ x: 1.234, y: 5.678, z: 9.012 }, 0.01) rounds each component to two decimal places before packing, halving the bit-width versus a raw float. This is the standard tactic for bandwidth-constrained multiplayer games — a typical ±1024 m world quantized to 0.01 m precision packs into ~17 bits per axis instead of 32 bits.

The 2-D and 3-D variants exist because some games (top-down, side-scrolling) work in Vector2 and would otherwise pay the cost of a wasted z component. quantizeRotation separately handles yaw and pitch with their own precisions; interpolateAngle handles the 360°/0° wrap so a transition from 350° to 10° interpolates through the short arc, not the long one. validateRange is a defensive check used at API boundaries.

Features

  • quantizePosition(position, precision = 0.01) — round a Vector3 to a precision divisor
  • quantizePosition2D(position, precision = 0.01) — round a Vector2
  • quantizeRotation(rotation, yawPrecision = 1, pitchPrecision = 1) — round yaw + pitch
  • interpolateAngle(a, b, t) — interpolate angles in degrees, taking the short arc through the wrap
  • validateRange(value, min, max, name?) — throw if out of range, with an optional field name for the error

Usage: Basic Example

import { quantizePosition, quantizeRotation } from "@suaveplan/bitpacker-game";

const pos = quantizePosition({ x: 1.234, y: 5.678, z: 9.012 }, 0.01);
// { x: 1.23, y: 5.68, z: 9.01 }

const rot = quantizeRotation({ yaw: 90.55, pitch: -45.4 }, 1, 1);
// { yaw: 91, pitch: -45 }

Usage: Advanced Example

import {
    quantizePosition,
    quantizeRotation,
    interpolateAngle,
    validateRange,
} from "@suaveplan/bitpacker-game";

function tick(prev: { yaw: number }, target: { yaw: number }, t: number) {
    validateRange(t, 0, 1, "interpolation t");
    return { yaw: interpolateAngle(prev.yaw, target.yaw, t) };
}

// Quantize before pack to match the schema's fixed-point precision exactly:
function preparePacket(position: { x: number; y: number; z: number }) {
    const q = quantizePosition(position, 0.01);
    validateRange(q.x, -1024, 1024, "x");
    validateRange(q.y, -1024, 1024, "y");
    validateRange(q.z, -128, 128, "z");
    return q;
}

API Reference

quantizePosition(position: Vector3, precision = 0.01): Vector3

Returns a new Vector3 with each component rounded to precision (e.g. 0.01 for two decimal places, 1 for whole units). The result is the nearest grid point on a precision-divisor lattice.

quantizePosition2D(position: Vector2, precision = 0.01): Vector2

Same as quantizePosition for a 2-D vector. No z component.

quantizeRotation(rotation: Rotation, yawPrecision = 1, pitchPrecision = 1): Rotation

Returns a new Rotation with yaw and pitch rounded to their respective precisions. The two axes have separate precisions because yaw typically needs less granularity than pitch (or vice versa).

interpolateAngle(a: number, b: number, t: number): number

Returns the interpolation between a and b taking the short arc through the 360°/0° wrap. t ∈ [0, 1]. Equivalent to slerp on the unit circle for two-axis cases.

validateRange(value: number, min: number, max: number, name?: string): void

Throws if value is outside [min, max]. The optional name is included in the error message to identify the offending field.

Implementation Notes

  • Quantization is round-to-nearest-grid. Math.round(value / precision) * precision produces banker's rounding behaviour at the exact half. Consumers that need round-half-up should pre-add a half-precision offset before quantizing.
  • No clamp. These helpers do not clamp out-of-range values; pair them with validateRange if your wire format has a hard cap. Quantizing without clamping can produce values outside the schema's bit-width and cause pack failures.
  • interpolateAngle is not unwrap-aware. It takes the short arc per pair; chains of three or more interpolated angles can drift unless the consumer normalizes between calls.
  • Defensive-but-cheap. validateRange is O(1) and emits a clear error message including the field name; it's appropriate at every API boundary, not just hot-path entries.