Writing Domain Encoders
A domain encoder is the only component that changes between application domains. It converts raw data into partition coordinates that the GPU pipeline can process.
Encoder Architecture
Every encoder has two parts that work together: a CPU preprocessor written in TypeScript and a GPU shader written in GLSL. The CPU part normalises and loads the data, the GPU part does the per-pixel encoding at massively parallel throughput.
CPU Preprocessor
- • Load and validate input data
- • Normalise to [0, 1] range
- • Convert to RGBA pixel array
- • Upload as WebGL texture
GPU Shader (GLSL)
- • Read texel at current UV coordinate
- • Compute local statistics (mean, variance)
- • Derive partition features
- • Output encoded vec4 for next pass
The S-Entropy Contract
Sk + St + Se = 1
Every encoder must produce outputs that satisfy this conservation law.
The three entropy components partition the total information content of each pixel:
- S_k (kinetic) — gradient magnitude, translational disorder. How rapidly the signal changes in the local neighbourhood.
- S_t (thermal) — local variance, configurational disorder. How much the signal varies within a local window.
- S_e (emission) — ternary state probability from the oxygen model. The fraction of information encoded in the invisible modality.
The encoder does not need to compute S_k, S_t, S_e directly — that happens in the entropy pass (pass 4). But the encoder must output features from which these values can be derived, and the resulting triple must sum to unity.
Example: Microscopy Encoder
The built-in microscopy encoder (encode_microscopy.glsl) demonstrates the full pattern. It computes luminance, local statistics, and Shannon entropy from a 3x3 neighbourhood.
encode_microscopy.glsl — key functions
#version 300 es
precision highp float;
uniform sampler2D u_image;
uniform float u_nmax;
in vec2 v_uv;
out vec4 fragColor;
float luminance(vec3 c) {
return dot(c, vec3(0.2126, 0.7152, 0.0722));
}
// Sample 3x3 neighbourhood for local statistics
void neighbourhood(sampler2D tex, vec2 uv, vec2 texelSize,
out float mean, out float variance) {
float sum = 0.0, sum2 = 0.0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
float v = luminance(
texture(tex, uv + vec2(float(dx), float(dy)) * texelSize).rgb
);
sum += v;
sum2 += v * v;
}
}
mean = sum / 9.0;
variance = sum2 / 9.0 - mean * mean;
}
// Shannon entropy from local 3x3 histogram (4 bins)
float localEntropy(sampler2D tex, vec2 uv, vec2 ts) {
// ... bin pixel values, compute -sum(p * log2(p))
}The encoder outputs a vec4 containing:
- R: quantised principal level n (partition depth)
- G: angular level l from local variance
- B: magnetic number m from gradient direction
- A: spin s from Shannon entropy
Writing Your Own Encoder
To add a new domain (e.g. spectroscopy, chromatography, genomics), you need to:
- 1. Create a GLSL file
encode_<domain>.glslinsrc/shaders/. It must acceptu_image(sampler2D) andu_nmax(float), and output avec4of partition features. - 2. Export the shader from
src/shaders/index.ts. - 3. Import the shader in
ObservationEngine.tsand add it to the shader compilation map. - 4. Write a CPU preprocessor function that converts your domain data into an RGBA pixel array (Uint8Array or Float32Array) suitable for texture upload.
- 5. Pass the encoder name to
observe()to select it at runtime.
The rest of the pipeline — partition, interference, entropy, and display passes — is universal and does not change between domains. Only the encoder changes.