ants/src/Renderer.ts

417 lines
16 KiB
TypeScript

import type { WebGLRenderTarget } from "three";
import * as THREE from "three";
import type { SceneCollection } from "./App";
import ColonyStats, { type ColonyStatsData } from "./ColonyStats";
import Config from "./Config";
import {
MAT_AIR,
MAT_DIRT,
MAT_FOOD,
MAT_HOME,
MAT_ROCK,
MAT_SAND,
} from "./constants";
import {
generateColorData,
generateLookupData,
MaterialRegistry,
} from "./materials";
import {
BEHAVIOR_GAS,
BEHAVIOR_LIQUID,
BEHAVIOR_POWDER,
BEHAVIOR_SOLID,
} from "./materials/types";
import { getBlockOffset } from "./sand/margolus";
import { generateSideViewWorld } from "./WorldInit";
interface Resources {
worldRenderTarget: THREE.WebGLRenderTarget;
worldRenderTargetCopy: THREE.WebGLRenderTarget;
worldBlurredRenderTarget: THREE.WebGLRenderTarget;
sandPhysicsRenderTarget: THREE.WebGLRenderTarget;
antsComputeTarget0: THREE.WebGLRenderTarget;
antsComputeTarget1: THREE.WebGLRenderTarget;
antsDiscreteRenderTarget: THREE.WebGLRenderTarget;
antsPresenceRenderTarget: THREE.WebGLRenderTarget;
}
export default class Renderer {
private renderer: THREE.WebGLRenderer;
public resources!: Resources;
private frameCounter = 0;
private colonyStats = new ColonyStats();
public readonly materialRegistry = new MaterialRegistry();
public readonly materialPropsTexture!: THREE.DataTexture;
public readonly materialColorTexture!: THREE.DataTexture;
constructor(public canvas: HTMLCanvasElement) {
this.renderer = new THREE.WebGLRenderer({ canvas });
this.initResources();
const propsData = generateLookupData(this.materialRegistry);
this.materialPropsTexture = new THREE.DataTexture(
propsData,
256,
1,
THREE.RGBAFormat,
THREE.FloatType,
);
this.materialPropsTexture.needsUpdate = true;
const colorData = generateColorData(this.materialRegistry);
this.materialColorTexture = new THREE.DataTexture(
colorData,
256,
1,
THREE.RGBAFormat,
THREE.FloatType,
);
this.materialColorTexture.needsUpdate = true;
}
private initResources() {
const antTextureSize = Math.round(Math.sqrt(2 ** Config.antsCount));
this.resources = {
worldRenderTarget: new THREE.WebGLRenderTarget(
Config.worldSize,
Config.worldSize,
{
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
},
),
worldRenderTargetCopy: new THREE.WebGLRenderTarget(
Config.worldSize,
Config.worldSize,
{
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
},
),
worldBlurredRenderTarget: new THREE.WebGLRenderTarget(
Config.worldSize,
Config.worldSize,
{
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
},
),
sandPhysicsRenderTarget: new THREE.WebGLRenderTarget(
Config.worldSize,
Config.worldSize,
{
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
},
),
antsComputeTarget0: Renderer.makeAntsMRT(antTextureSize),
antsComputeTarget1: Renderer.makeAntsMRT(antTextureSize),
antsDiscreteRenderTarget: new THREE.WebGLRenderTarget(
Config.worldSize,
Config.worldSize,
{
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
},
),
antsPresenceRenderTarget: new THREE.WebGLRenderTarget(
Config.worldSize,
Config.worldSize,
{
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
},
),
};
}
private static makeAntsMRT(size: number): THREE.WebGLRenderTarget {
const mrt = new THREE.WebGLRenderTarget(size, size, {
count: 2,
type: THREE.FloatType,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
depthBuffer: false,
});
// both attachments get the same texture params
for (const texture of mrt.textures) {
texture.type = THREE.FloatType;
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
}
return mrt;
}
public renderSimulation(scenes: SceneCollection) {
const [antsComputeSource, antsComputeTarget] =
scenes.ants.getRenderTargets();
// sand physics pass
const blockOffset = getBlockOffset(this.frameCounter);
this.setViewportFromRT(this.resources.sandPhysicsRenderTarget);
this.renderer.setRenderTarget(this.resources.sandPhysicsRenderTarget);
scenes.sandPhysics.material.uniforms.uWorld.value =
this.resources.worldRenderTarget.texture;
scenes.sandPhysics.material.uniforms.uMaterialProps.value =
this.materialPropsTexture;
scenes.sandPhysics.material.uniforms.uBlockOffset.value.set(
blockOffset.x,
blockOffset.y,
);
scenes.sandPhysics.material.uniforms.uFrame.value = this.frameCounter;
this.renderer.render(scenes.sandPhysics, scenes.sandPhysics.camera);
this.frameCounter++;
this.setViewportFromRT(this.resources.worldBlurredRenderTarget);
this.renderer.setRenderTarget(this.resources.worldBlurredRenderTarget);
scenes.worldBlur.material.uniforms.tWorld.value =
this.resources.sandPhysicsRenderTarget.texture;
scenes.worldBlur.material.uniforms.uMaterialProps.value =
this.materialPropsTexture;
this.renderer.render(scenes.worldBlur, scenes.worldBlur.camera);
this.renderer.setRenderTarget(this.resources.antsPresenceRenderTarget);
this.renderer.clear();
this.setViewportFromRT(antsComputeTarget);
this.renderer.setRenderTarget(antsComputeTarget);
scenes.ants.material.uniforms.tLastState.value =
antsComputeSource.textures[0];
scenes.ants.material.uniforms.tLastExtState.value =
antsComputeSource.textures[1];
scenes.ants.material.uniforms.tWorld.value =
this.resources.worldBlurredRenderTarget.texture;
scenes.ants.material.uniforms.tPresence.value =
this.resources.antsPresenceRenderTarget.texture;
scenes.ants.material.uniforms.uMaterialProps.value =
this.materialPropsTexture;
this.renderer.render(scenes.ants, scenes.ants.camera);
this.setViewportFromRT(this.resources.antsDiscreteRenderTarget);
this.renderer.setRenderTarget(this.resources.antsDiscreteRenderTarget);
scenes.discretize.material.uniforms.tDataCurrent.value =
antsComputeTarget.textures[0];
scenes.discretize.material.uniforms.tDataLast.value =
antsComputeSource.textures[0];
scenes.discretize.material.uniforms.tDataExtCurrent.value =
antsComputeTarget.textures[1];
this.renderer.render(scenes.discretize, scenes.discretize.camera);
this.setViewportFromRT(this.resources.worldRenderTarget);
this.renderer.setRenderTarget(this.resources.worldRenderTarget);
scenes.world.material.uniforms.tLastState.value =
this.resources.worldBlurredRenderTarget.texture;
scenes.world.material.uniforms.tDiscreteAnts.value =
this.resources.antsDiscreteRenderTarget.texture;
this.renderer.render(scenes.world, scenes.world.camera);
scenes.screen.material.uniforms.tData.value =
antsComputeTarget.textures[0];
scenes.screen.groundMaterial.uniforms.map.value =
this.resources.worldRenderTargetCopy.texture;
// colony stats readback — read ant state texture, compute aggregate stats on CPU
this.colonyStats.update(this.renderer, antsComputeTarget);
scenes.ants.material.uniforms.uForagerRatio.value =
this.colonyStats.data.foragerRatio;
}
private setViewportFromRT(rt: WebGLRenderTarget) {
this.renderer.setViewport(0, 0, rt.width, rt.height);
}
public renderToScreen(scenes: SceneCollection) {
this.setViewportFromRT(this.resources.worldRenderTargetCopy);
this.renderer.setRenderTarget(this.resources.worldRenderTargetCopy);
scenes.draw.material.uniforms.tWorld.value =
this.resources.worldRenderTarget.texture;
scenes.draw.material.uniforms.pointerPosition.value =
scenes.screen.pointerPosition;
scenes.draw.material.uniforms.drawMode.value =
scenes.screen.effectiveDrawMode;
scenes.draw.material.uniforms.brushRadius.value = Config.brushRadius;
this.renderer.render(scenes.draw, scenes.draw.camera);
this.renderer.copyFramebufferToTexture(
this.resources.worldRenderTarget.texture,
new THREE.Vector2(),
);
this.renderer.setViewport(
0,
0,
scenes.screen.renderWidth,
scenes.screen.renderHeight,
);
this.renderer.setRenderTarget(null);
this.renderer.render(scenes.screen, scenes.screen.camera);
}
public resizeCanvas(width: number, height: number) {
this.renderer.setSize(width, height, false);
}
public getCommonMaterialDefines(): Record<string, string> {
return {
WORLD_SIZE: Renderer.convertNumberToFloatString(Config.worldSize),
SCENT_THRESHOLD: Renderer.convertNumberToFloatString(
Config.scentThreshold,
),
SCENT_FADE_OUT_FACTOR: Renderer.convertNumberToFloatString(
Config.scentFadeOutFactor,
),
SCENT_BLUR_RADIUS: Renderer.convertNumberToFloatString(
Config.scentBlurRadius,
),
SCENT_MAX_STORAGE: Renderer.convertNumberToFloatString(
Config.scentMaxStorage,
),
SCENT_PER_MARKER: Renderer.convertNumberToFloatString(
Config.scentPerMarker,
),
SCENT_MAX_PER_CELL: Renderer.convertNumberToFloatString(
Config.scentMaxPerCell,
),
ANT_SPEED: Renderer.convertNumberToFloatString(Config.antSpeed),
ANT_ROTATION_ANGLE: Renderer.convertNumberToFloatString(
Config.antRotationAngle,
),
REPELLENT_FADE_OUT_FACTOR: Renderer.convertNumberToFloatString(
Config.repellentFadeOutFactor,
),
REPELLENT_BLUR_RADIUS: Renderer.convertNumberToFloatString(
Config.repellentBlurRadius,
),
REPELLENT_MAX_PER_CELL: Renderer.convertNumberToFloatString(
Config.repellentMaxPerCell,
),
REPELLENT_THRESHOLD: Renderer.convertNumberToFloatString(
Config.repellentThreshold,
),
MAT_AIR: String(MAT_AIR),
MAT_SAND: String(MAT_SAND),
MAT_DIRT: String(MAT_DIRT),
MAT_ROCK: String(MAT_ROCK),
MAT_FOOD: String(MAT_FOOD),
MAT_HOME: String(MAT_HOME),
BEHAVIOR_POWDER:
Renderer.convertNumberToFloatString(BEHAVIOR_POWDER),
BEHAVIOR_LIQUID:
Renderer.convertNumberToFloatString(BEHAVIOR_LIQUID),
BEHAVIOR_GAS: Renderer.convertNumberToFloatString(BEHAVIOR_GAS),
BEHAVIOR_SOLID: Renderer.convertNumberToFloatString(BEHAVIOR_SOLID),
VIEW_MODE_SIDE: Config.viewMode === "side" ? "1" : "0",
};
}
public reset(scenes: SceneCollection) {
const antTextureSize = Math.round(Math.sqrt(2 ** Config.antsCount));
this.resources.worldRenderTarget.setSize(
Config.worldSize,
Config.worldSize,
);
this.renderer.setRenderTarget(this.resources.worldRenderTarget);
this.renderer.clear();
if (Config.viewMode === "side") {
const data = generateSideViewWorld(Config.worldSize);
const initTexture = new THREE.DataTexture(
data,
Config.worldSize,
Config.worldSize,
THREE.RGBAFormat,
THREE.FloatType,
);
initTexture.needsUpdate = true;
this.renderer.copyTextureToTexture(
initTexture,
this.resources.worldRenderTarget.texture,
);
initTexture.dispose();
}
this.resources.worldRenderTargetCopy.setSize(
Config.worldSize,
Config.worldSize,
);
this.renderer.setRenderTarget(this.resources.worldRenderTargetCopy);
this.renderer.clear();
this.resources.worldBlurredRenderTarget.setSize(
Config.worldSize,
Config.worldSize,
);
this.renderer.setRenderTarget(this.resources.worldBlurredRenderTarget);
this.renderer.clear();
this.resources.sandPhysicsRenderTarget.setSize(
Config.worldSize,
Config.worldSize,
);
this.renderer.setRenderTarget(this.resources.sandPhysicsRenderTarget);
this.renderer.clear();
this.frameCounter = 0;
this.resources.antsComputeTarget0.setSize(
antTextureSize,
antTextureSize,
);
this.renderer.setRenderTarget(this.resources.antsComputeTarget0);
this.renderer.clear();
this.resources.antsComputeTarget1.setSize(
antTextureSize,
antTextureSize,
);
this.renderer.setRenderTarget(this.resources.antsComputeTarget1);
this.renderer.clear();
this.resources.antsDiscreteRenderTarget.setSize(
Config.worldSize,
Config.worldSize,
);
this.renderer.setRenderTarget(this.resources.antsDiscreteRenderTarget);
this.renderer.clear();
this.resources.antsPresenceRenderTarget.setSize(
Config.worldSize,
Config.worldSize,
);
this.renderer.setRenderTarget(this.resources.antsPresenceRenderTarget);
this.renderer.clear();
for (const scene of Object.values(scenes)) {
scene.recompileMaterials();
}
}
public get colonyStatsData(): ColonyStatsData {
return this.colonyStats.data;
}
static convertNumberToFloatString(n: number): string {
return n.toFixed(8);
}
}