Initial implementation

This commit is contained in:
vHawk 2022-06-26 12:34:16 +03:00
parent 1588105e0b
commit f0c253c7fb
30 changed files with 4325 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
.idea
build

3184
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "ants-simulation",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"dat.gui": "^0.7.9",
"push-dir": "^0.4.1",
"three": "^0.141.0"
},
"devDependencies": {
"@types/dat.gui": "^0.7.7",
"@types/node": "^18.0.0",
"@types/three": "^0.141.0",
"clean-webpack-plugin": "^4.0.0",
"html-webpack-plugin": "^5.5.0",
"raw-loader": "^4.0.2",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.2"
},
"scripts": {
"build": "webpack --config ./webpack.config.js --mode=production",
"dev": "webpack serve --config ./webpack.config.js --mode=development"
},
"author": "",
"license": "MIT"
}

BIN
public/ant.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

BIN
public/food.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

62
src/App.ts Normal file
View file

@ -0,0 +1,62 @@
import AntsComputeScene from "./scenes/AntsComputeScene";
import ScreenScene from "./scenes/ScreenScene";
import Renderer from "./Renderer";
import WorldComputeScene from "./scenes/WorldComputeScene";
import AntsDiscretizeScene from "./scenes/AntsDiscretizeScene";
import WorldBlurScene from "./scenes/WorldBlurScene";
export interface SceneCollection {
ants: AntsComputeScene;
world: WorldComputeScene;
worldBlur: WorldBlurScene;
discretize: AntsDiscretizeScene;
screen: ScreenScene;
}
export default new class App {
private renderer: Renderer = new Renderer(<HTMLCanvasElement>document.getElementById('canvas'));
private scenes: SceneCollection;
private loop = (deltaTime: number): void => this.update(deltaTime);
constructor() {
this.initScenes();
window.addEventListener('resize', () => this.resize());
this.resize();
this.update(0);
}
private initScenes() {
this.scenes = {
ants: new AntsComputeScene(this.renderer),
world: new WorldComputeScene(this.renderer),
worldBlur: new WorldBlurScene(this.renderer),
discretize: new AntsDiscretizeScene(this.renderer),
screen: new ScreenScene(this.renderer),
};
}
private resize() {
const width = window.innerWidth;
const height = window.innerHeight;
this.renderer.resizeCanvas(width, height);
for (const scene of Object.values(this.scenes)) {
scene.resize(width, height);
}
}
private update(deltaTime: number) {
requestAnimationFrame(this.loop);
for (let i = 0; i < 3; i++) {
for (const scene of Object.values(this.scenes)) {
scene.update(deltaTime);
}
this.renderer.renderScenes(this.scenes);
}
}
}

121
src/Renderer.ts Normal file
View file

@ -0,0 +1,121 @@
import * as THREE from 'three';
import {SceneCollection} from "./App";
const Config = {
worldSize: 1024,
antsCount: 64 ** 2
};
interface Resources {
worldRenderTarget0: THREE.WebGLRenderTarget;
worldRenderTarget1: THREE.WebGLRenderTarget;
worldBlurredRenderTarget: THREE.WebGLRenderTarget;
antsDataRenderTarget0: THREE.WebGLRenderTarget;
antsDataRenderTarget1: THREE.WebGLRenderTarget;
antsDiscreteRenderTarget: THREE.WebGLRenderTarget;
}
export default class Renderer {
private renderer: THREE.WebGLRenderer;
public resources: Resources;
constructor(public canvas: HTMLCanvasElement) {
this.renderer = new THREE.WebGLRenderer({canvas})
this.initResources();
}
private initResources() {
const antTextureSize = Math.round(Math.sqrt(Config.antsCount));
this.resources = {
worldRenderTarget0: new THREE.WebGLRenderTarget(Config.worldSize, Config.worldSize, {
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.LinearFilter,
minFilter: THREE.LinearFilter,
}),
worldRenderTarget1: new THREE.WebGLRenderTarget(Config.worldSize, Config.worldSize, {
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.LinearFilter,
minFilter: THREE.LinearFilter,
}),
worldBlurredRenderTarget: new THREE.WebGLRenderTarget(Config.worldSize, Config.worldSize, {
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.LinearFilter,
minFilter: THREE.LinearFilter,
}),
antsDataRenderTarget0: new THREE.WebGLRenderTarget(antTextureSize, antTextureSize, {
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
}),
antsDataRenderTarget1: new THREE.WebGLRenderTarget(antTextureSize, antTextureSize, {
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
}),
antsDiscreteRenderTarget: new THREE.WebGLRenderTarget(Config.worldSize, Config.worldSize, {
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
depthBuffer: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
})
};
}
public renderScenes(scenes: SceneCollection) {
const [antsComputeSource, antsComputeTarget] = scenes.ants.getRenderTargets();
const [worldComputeSource, worldComputeTarget] = scenes.world.getRenderTargets();
this.renderer.setViewport(0, 0, scenes.ants.renderWidth, scenes.ants.renderHeight);
this.renderer.setRenderTarget(antsComputeTarget);
scenes.ants.material.uniforms.tLastState.value = antsComputeSource.texture;
scenes.ants.material.uniforms.tWorld.value = worldComputeSource.texture;
this.renderer.render(scenes.ants, scenes.ants.camera);
this.renderer.setViewport(0, 0, scenes.discretize.renderWidth, scenes.discretize.renderHeight);
this.renderer.setRenderTarget(scenes.discretize.getRenderTarget());
scenes.discretize.material.uniforms.tDataCurrent.value = antsComputeTarget.texture;
scenes.discretize.material.uniforms.tDataLast.value = antsComputeSource.texture;
this.renderer.render(scenes.discretize, scenes.discretize.camera);
this.renderer.setViewport(0, 0, scenes.world.renderWidth, scenes.world.renderHeight);
this.renderer.setRenderTarget(worldComputeTarget);
scenes.world.material.uniforms.tLastState.value = scenes.worldBlur.getRenderTarget().texture;
scenes.world.material.uniforms.tDiscreteAnts.value = scenes.discretize.getRenderTarget().texture;
scenes.world.material.uniforms.pointerData.value = scenes.screen.getPointerData();
this.renderer.render(scenes.world, scenes.world.camera);
this.renderer.setViewport(0, 0, scenes.worldBlur.renderWidth, scenes.worldBlur.renderHeight);
this.renderer.setRenderTarget(scenes.worldBlur.getRenderTarget());
scenes.worldBlur.material.uniforms.tWorld.value = worldComputeTarget.texture;
this.renderer.render(scenes.worldBlur, scenes.worldBlur.camera);
this.renderer.setViewport(0, 0, scenes.screen.renderWidth, scenes.screen.renderHeight);
this.renderer.setRenderTarget(null);
scenes.screen.material.uniforms.tData.value = antsComputeTarget.texture;
scenes.screen.groundMaterial.uniforms.map.value = scenes.worldBlur.getRenderTarget().texture;
this.renderer.render(scenes.screen, scenes.screen.camera);
}
public resizeCanvas(width: number, height: number) {
this.canvas.width = width;
this.canvas.height = height;
}
}

8
src/global.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare module '*.vert' {
const content: string;
export default content;
}
declare module '*.frag' {
const content: string;
export default content;
}

17
src/index.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Ants simulation</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

View file

@ -0,0 +1,19 @@
import * as THREE from 'three';
import Renderer from "../Renderer";
export default abstract class AbstractScene extends THREE.Scene {
protected readonly renderer: Renderer;
public readonly camera: THREE.Camera;
public renderWidth: number = 1;
public renderHeight: number = 1;
protected constructor(renderer: Renderer) {
super();
this.renderer = renderer;
}
public abstract resize(width: number, height: number): void;
public abstract update(deltaTime: number): void;
}

View file

@ -0,0 +1,54 @@
import * as THREE from 'three';
import Renderer from "../Renderer";
import AbstractScene from "./AbstractScene";
import FullScreenTriangleGeometry from "../utils/FullScreenTriangleGeometry";
import fragmentShader from '../shaders/antsCompute.frag';
import vertexShader from '../shaders/antsCompute.vert';
import {WebGLRenderTarget} from "three";
export default class AntsComputeScene extends AbstractScene {
public camera: THREE.OrthographicCamera = new THREE.OrthographicCamera();
public material: THREE.RawShaderMaterial;
private renderTargets: [WebGLRenderTarget, WebGLRenderTarget];
constructor(renderer: Renderer) {
super(renderer);
const geometry = new FullScreenTriangleGeometry();
const material = new THREE.RawShaderMaterial({
uniforms: {
uTime: {value: 0},
tLastState: {value: this.renderer.resources.antsDataRenderTarget0.texture},
tWorld: {value: this.renderer.resources.worldRenderTarget0.texture},
},
vertexShader,
fragmentShader
});
const mesh = new THREE.Mesh(geometry, material);
this.add(mesh);
this.material = material;
this.renderTargets = [
this.renderer.resources.antsDataRenderTarget0,
this.renderer.resources.antsDataRenderTarget1
];
this.renderWidth = this.renderer.resources.antsDataRenderTarget0.width;
this.renderHeight = this.renderer.resources.antsDataRenderTarget0.height;
}
public getRenderTargets(): [WebGLRenderTarget, WebGLRenderTarget] {
this.renderTargets.reverse();
return this.renderTargets;
}
public resize(width: number, height: number) {
}
public update(deltaTime: number) {
this.material.uniforms.uTime.value = performance.now();
}
}

View file

@ -0,0 +1,51 @@
import * as THREE from 'three';
import {WebGLRenderTarget} from 'three';
import Renderer from "../Renderer";
import AbstractScene from "./AbstractScene";
import fragmentShader from '../shaders/antsDiscretize.frag';
import vertexShader from '../shaders/antsDiscretize.vert';
export default class AntsDiscretizeScene extends AbstractScene {
public readonly camera: THREE.OrthographicCamera = new THREE.OrthographicCamera();
public readonly material: THREE.RawShaderMaterial;
private readonly renderTarget: WebGLRenderTarget;
constructor(renderer: Renderer) {
super(renderer);
const geometry = new THREE.BoxBufferGeometry(1, 1, 1);
const material = new THREE.RawShaderMaterial({
uniforms: {
tDataCurrent: {value: null},
tDataLast: {value: null},
},
vertexShader,
fragmentShader
});
const mesh = new THREE.InstancedMesh(
geometry,
material,
this.renderer.resources.antsDataRenderTarget0.width * this.renderer.resources.antsDataRenderTarget0.height
);
this.add(mesh);
this.material = material;
this.renderTarget = this.renderer.resources.antsDiscreteRenderTarget;
this.renderWidth = this.renderer.resources.worldRenderTarget0.width;
this.renderHeight = this.renderer.resources.worldRenderTarget0.height;
}
public getRenderTarget(): WebGLRenderTarget {
return this.renderTarget;
}
public resize(width: number, height: number) {
}
public update(deltaTime: number) {
}
}

141
src/scenes/ScreenScene.ts Normal file
View file

@ -0,0 +1,141 @@
import * as THREE from 'three';
import Renderer from "../Renderer";
import AbstractScene from "./AbstractScene";
import vertexShaderAnts from "../shaders/ants.vert";
import fragmentShaderAnts from "../shaders/ants.frag";
import vertexShaderGround from "../shaders/screenWorld.vert";
import fragmentShaderGround from "../shaders/screenWorld.frag";
enum PointerState {
None,
LMB,
RMB
}
export default class ScreenScene extends AbstractScene {
public readonly camera: THREE.PerspectiveCamera;
public readonly material: THREE.ShaderMaterial;
public readonly groundMaterial: THREE.ShaderMaterial;
public readonly pointerPosition: THREE.Vector2 = new THREE.Vector2();
public pointerState: PointerState = PointerState.None;
constructor(renderer: Renderer) {
super(renderer);
const ground = new THREE.Mesh(
new THREE.PlaneBufferGeometry(10, 10),
new THREE.ShaderMaterial({
uniforms: {
map: {value: this.renderer.resources.worldRenderTarget0.texture},
},
vertexShader: vertexShaderGround,
fragmentShader: fragmentShaderGround
})
);
this.groundMaterial = ground.material;
ground.position.x += 5;
ground.position.y += 5;
this.add(ground);
const antTexture = new THREE.TextureLoader().load('ant.png');
const foodTexture = new THREE.TextureLoader().load('food.png');
antTexture.magFilter = foodTexture.magFilter = THREE.NearestFilter;
antTexture.minFilter = foodTexture.minFilter = THREE.LinearMipMapLinearFilter;
this.material = new THREE.ShaderMaterial({
uniforms: {
tData: {value: this.renderer.resources.antsDataRenderTarget0.texture},
tAnt: {value: antTexture},
tFood: {value: foodTexture}
},
vertexShader: vertexShaderAnts,
fragmentShader: fragmentShaderAnts,
transparent: true
});
const ants = new THREE.InstancedMesh(
new THREE.PlaneBufferGeometry(0.15, 0.15),
this.material,
this.renderer.resources.antsDataRenderTarget0.width * this.renderer.resources.antsDataRenderTarget0.height
)
this.add(ants);
this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 10000);
this.add(this.camera);
this.camera.position.z = 12;
this.camera.position.x = this.camera.position.y = 5;
const raycastVector = new THREE.Vector2(0, 0);
const raycaster = new THREE.Raycaster();
this.renderer.canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
this.renderer.canvas.addEventListener('pointerdown', (e) => {
raycastVector.x = (e.clientX / window.innerWidth) * 2 - 1;
raycastVector.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(raycastVector, this.camera);
const intersects = raycaster.intersectObjects([ground]);
if (intersects.length > 0) {
const uv = intersects[0].uv;
this.pointerPosition.copy(uv);
if (e.button === 0) {
this.pointerState = PointerState.LMB;
} else if (e.button === 2) {
this.pointerState = PointerState.RMB;
}
}
});
this.renderer.canvas.addEventListener('pointermove', (e) => {
raycastVector.x = (e.clientX / window.innerWidth) * 2 - 1;
raycastVector.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(raycastVector, this.camera);
const intersects = raycaster.intersectObjects([ground]);
if (intersects.length > 0) {
const uv = intersects[0].uv;
this.pointerPosition.copy(uv);
}
});
this.renderer.canvas.addEventListener('pointerup', (e) => {
this.pointerState = PointerState.None;
});
}
public getPointerData(): THREE.Vector4 {
return new THREE.Vector4(
+(this.pointerState === PointerState.LMB),
+(this.pointerState === PointerState.RMB),
this.pointerPosition.x,
this.pointerPosition.y
);
}
public resize(width: number, height: number) {
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderWidth = width;
this.renderHeight = height;
}
public update(deltaTime: number) {
}
}

View file

@ -0,0 +1,46 @@
import * as THREE from 'three';
import Renderer from "../Renderer";
import AbstractScene from "./AbstractScene";
import FullScreenTriangleGeometry from "../utils/FullScreenTriangleGeometry";
import fragmentShader from '../shaders/worldBlur.frag';
import vertexShader from '../shaders/worldBlur.vert';
import {WebGLRenderTarget} from "three";
export default class WorldBlurScene extends AbstractScene {
public readonly camera: THREE.OrthographicCamera = new THREE.OrthographicCamera();
public readonly material: THREE.RawShaderMaterial;
private readonly renderTarget: WebGLRenderTarget;
constructor(renderer: Renderer) {
super(renderer);
const geometry = new FullScreenTriangleGeometry();
const material = new THREE.RawShaderMaterial({
uniforms: {
tWorld: {value: this.renderer.resources.worldRenderTarget0.texture},
},
vertexShader,
fragmentShader
});
const mesh = new THREE.Mesh(geometry, material);
this.add(mesh);
this.material = material;
this.renderTarget = this.renderer.resources.worldBlurredRenderTarget;
this.renderWidth = this.renderer.resources.worldRenderTarget0.width;
this.renderHeight = this.renderer.resources.worldRenderTarget0.height;
}
public getRenderTarget(): WebGLRenderTarget {
return this.renderTarget;
}
public resize(width: number, height: number) {
}
public update(deltaTime: number) {
}
}

View file

@ -0,0 +1,54 @@
import * as THREE from 'three';
import Renderer from "../Renderer";
import AbstractScene from "./AbstractScene";
import FullScreenTriangleGeometry from "../utils/FullScreenTriangleGeometry";
import fragmentShader from '../shaders/world.frag';
import vertexShader from '../shaders/world.vert';
import {WebGLRenderTarget} from "three";
export default class WorldComputeScene extends AbstractScene {
public readonly camera: THREE.OrthographicCamera = new THREE.OrthographicCamera();
public readonly material: THREE.RawShaderMaterial;
private readonly renderTargets: [WebGLRenderTarget, WebGLRenderTarget];
constructor(renderer: Renderer) {
super(renderer);
const geometry = new FullScreenTriangleGeometry();
const material = new THREE.RawShaderMaterial({
uniforms: {
tLastState: {value: this.renderer.resources.worldRenderTarget0.texture},
tDiscreteAnts: {value: this.renderer.resources.antsDiscreteRenderTarget.texture},
pointerData: {value: new THREE.Vector4()},
},
vertexShader,
fragmentShader
});
const mesh = new THREE.Mesh(geometry, material);
this.add(mesh);
this.material = material;
this.renderTargets = [
this.renderer.resources.worldRenderTarget0,
this.renderer.resources.worldRenderTarget1
];
this.renderWidth = this.renderer.resources.worldRenderTarget0.width;
this.renderHeight = this.renderer.resources.worldRenderTarget0.height;
}
public getRenderTargets(): [WebGLRenderTarget, WebGLRenderTarget] {
this.renderTargets.reverse();
return this.renderTargets;
}
public resize(width: number, height: number) {
}
public update(deltaTime: number) {
}
}

21
src/shaders/ants.frag Normal file
View file

@ -0,0 +1,21 @@
precision highp float;
precision highp int;
varying vec2 vUv;
varying float vIsCarryingFood;
uniform sampler2D tAnt;
uniform sampler2D tFood;
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec4 antColor = texture2D(tAnt, vUv);
vec4 foodColor = texture2D(tFood, vUv);
gl_FragColor = mix(antColor, foodColor, foodColor.a * vIsCarryingFood);
}

35
src/shaders/ants.vert Normal file
View file

@ -0,0 +1,35 @@
precision highp float;
precision highp int;
#define PI 3.1415926535897932384626433832795
varying vec2 vUv;
varying float vIsCarryingFood;
uniform sampler2D tData;
vec2 rotate(vec2 v, float a) {
float s = sin(a);
float c = cos(a);
mat2 m = mat2(c, -s, s, c);
return m * v;
}
void main() {
vUv = uv;
float dataTextureSize = vec2(textureSize(tData, 0)).x;
float id = float(gl_InstanceID);
float sampleY = floor(id / dataTextureSize);
float sampleX = id - sampleY * dataTextureSize;
vec4 dataSample = texture2D(tData, vec2(sampleX, sampleY) / dataTextureSize);
vec2 offset = dataSample.xy * 10.;
vec2 rotatedPosition = rotate(position.xy, -dataSample.z + PI * 0.5);
vIsCarryingFood = float(int(dataSample.w) & 1);
gl_Position = projectionMatrix * modelViewMatrix * vec4(vec2(rotatedPosition + offset), 0, 1);
}

View file

@ -0,0 +1,200 @@
#version 300 es
precision highp float;
precision highp int;
#define PI 3.1415926535897932384626433832795
#define MAX_STORAGE 1000000.
in vec2 vUv;
out highp vec4 FragColor;
uniform float uTime;
uniform sampler2D tLastState;
uniform sampler2D tWorld;
const float PHI = 1.61803398874989484820459;
const float speed = 1.;
const float sampleDistance = 20.;
const float worldSize = 1024.;
const float cellSize = 1. / worldSize;
float goldNoise(in vec2 xy, in float seed) {
return fract(tan(distance(xy*PHI, xy)*seed)*xy.x);
}
bool tryGetFood(vec2 pos) {
return texture(tWorld, pos).x == 1.;
}
bool tryDropFood(vec2 pos) {
return texture(tWorld, pos).y == 1.;
}
float smell(vec2 pos, float isCarrying) {
vec2 value = texture(tWorld, pos).zw;
if (isCarrying > 0.5) {
return value.y;
}
return value.x;
}
vec2 applyOffsetToPos(vec2 pos, vec2 offsetDirectaion) {
return vec2(
fract(pos.x + offsetDirectaion.x * cellSize),
fract(pos.y + offsetDirectaion.y * cellSize)
);
}
const float rotAngle = PI / 50.;
float constrainAngle(float x){
x = mod(x,360.);
if (x < 0.)
x += 360.;
return x;
}
void main() {
vec4 lastState = texture(tLastState, vUv);
float rand = goldNoise(vUv * 1000., fract(uTime / 1000.));
vec2 pos = lastState.xy;
float angle = lastState.z;
float isCarrying = float(int(lastState.w) & 1);
float storage = float(int(lastState.w) >> 1);
bool movementProcessed = false;
if (pos == vec2(0)) { // init new ant
pos = vec2(0.5);
angle = goldNoise(vUv * 10000., 1.) * 2. * PI;
isCarrying = 0.;
storage = MAX_STORAGE;
}
if (tryDropFood(pos)) {
if (isCarrying == 1.) {
isCarrying = 0.;
angle += PI;
}
storage = MAX_STORAGE;
}
if (isCarrying == 0.) {
if (rand < 0.33) {
vec2 offset = vec2(cos(angle), sin(angle)) * sampleDistance;
vec2 point = applyOffsetToPos(pos, offset);
if (tryGetFood(point)) {
isCarrying = 1.;
storage = MAX_STORAGE;
pos = point;
movementProcessed = true;
}
} else if (rand < 0.66) {
vec2 offset = vec2(cos(angle - rotAngle), sin(angle - rotAngle)) * sampleDistance;
vec2 point = applyOffsetToPos(pos, offset);
if (tryGetFood(point)) {
isCarrying = 1.;
storage = MAX_STORAGE;
pos = point;
movementProcessed = true;
angle -= rotAngle;
}
} else {
vec2 offset = vec2(cos(angle + rotAngle), sin(angle + rotAngle)) * sampleDistance;
vec2 point = applyOffsetToPos(pos, offset);
if (tryGetFood(point)) {
isCarrying = 1.;
storage = MAX_STORAGE;
pos = point;
movementProcessed = true;
angle += rotAngle;
}
}
} else if (isCarrying == 1.) {
if (rand < 0.33) {
vec2 offset = vec2(cos(angle), sin(angle)) * 10.;
vec2 point = applyOffsetToPos(pos, offset);
if (tryDropFood(point)) {
isCarrying = 0.;
storage = MAX_STORAGE;
pos = point;
movementProcessed = true;
}
} else if (rand < 0.66) {
vec2 offset = vec2(cos(angle - rotAngle), sin(angle - rotAngle)) * sampleDistance;
vec2 point = applyOffsetToPos(pos, offset);
if (tryDropFood(point)) {
isCarrying = 0.;
storage = MAX_STORAGE;
pos = point;
movementProcessed = true;
angle -= rotAngle;
}
} else {
vec2 offset = vec2(cos(angle + rotAngle), sin(angle + rotAngle)) * sampleDistance;
vec2 point = applyOffsetToPos(pos, offset);
if (tryDropFood(point)) {
isCarrying = 0.;
storage = MAX_STORAGE;
pos = point;
movementProcessed = true;
angle += rotAngle;
}
}
}
if (!movementProcessed) {
float noise2 = goldNoise(vUv * 10000., uTime / 1000. + 0.2);
float sampleAhead = smell(applyOffsetToPos(pos, vec2(cos(angle), sin(angle)) * sampleDistance), isCarrying);
float sampleLeft = smell(applyOffsetToPos(pos, vec2(cos(angle - rotAngle), sin(angle - rotAngle)) * sampleDistance), isCarrying);
float sampleRight = smell(applyOffsetToPos(pos, vec2(cos(angle + rotAngle), sin(angle + rotAngle)) * sampleDistance), isCarrying);
if (sampleAhead > sampleLeft && sampleAhead > sampleRight) {
// don't change direction
} else if (sampleLeft > sampleAhead && sampleLeft > sampleRight) {
angle -= rotAngle; // steer left
} else if (sampleRight > sampleAhead && sampleRight > sampleLeft) {
angle += rotAngle; // steer right
} else if (rand < 0.33) {
angle += rotAngle; // no smell detected, do random movement
} else if (rand < 0.66) {
angle -= rotAngle;
}
if (noise2 > 0.5) {
angle += PI / 30.;
} else {
angle -= PI / 30.;
}
vec2 offset = vec2(cos(angle), sin(angle));
pos = applyOffsetToPos(pos, offset);
} else {
angle += PI;
}
if (fract(pos.x) == 0. || fract(pos.y) == 0.) {
//angle += PI / 2.;
}
FragColor = vec4(
pos.x,
pos.y,
angle,
float((uint(max(storage - 1000., 0.)) << 1) + uint(isCarrying))
);
}

View file

@ -0,0 +1,14 @@
#version 300 es
precision highp float;
precision highp int;
in vec3 position;
in vec2 uv;
out vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}

View file

@ -0,0 +1,15 @@
#version 300 es
precision highp float;
precision highp int;
in vec2 vUv;
in float vIsCarryingFood;
in float vScentFactor;
in float vIsCellCleared;
out highp vec4 FragColor;
void main() {
FragColor = vec4(vIsCarryingFood * vScentFactor, (1. - vIsCarryingFood) * vScentFactor, vIsCellCleared, 1);
}

View file

@ -0,0 +1,48 @@
#version 300 es
precision highp float;
precision highp int;
in vec3 position;
in vec2 uv;
out vec2 vUv;
out float vIsCarryingFood;
out float vScentFactor;
out float vIsCellCleared;
uniform sampler2D tDataCurrent;
uniform sampler2D tDataLast;
const float worldSize = 1024.;
const float cellSize = 1. / worldSize;
void main() {
vUv = uv;
float dataTextureSize = vec2(textureSize(tDataCurrent, 0)).x;
float id = float(gl_InstanceID);
float sampleY = floor(id / dataTextureSize);
float sampleX = id - sampleY * dataTextureSize;
vec4 dataSampleCurrent = texture(tDataCurrent, vec2(sampleX, sampleY) / dataTextureSize);
vec4 dataSampleLast = texture(tDataLast, vec2(sampleX, sampleY) / dataTextureSize);
vec2 offset = dataSampleCurrent.xy;
float isCarrying = float(int(dataSampleCurrent.w) & 1);
float wasCarrying = float(int(dataSampleLast.w) & 1);
float isCellCleared = float(wasCarrying == 0. && isCarrying == 1.);
float storage = float(int(dataSampleCurrent.w) >> 1);
vIsCarryingFood = isCarrying;
vScentFactor = storage / 1000000.;
vIsCellCleared = isCellCleared;
gl_Position = vec4(
(position.xy * cellSize * 0.5 + floor(offset * worldSize) / worldSize + cellSize * 0.5) * 2. - 1.,
0,
1
);
}

View file

@ -0,0 +1,24 @@
precision highp float;
precision highp int;
varying vec2 vUv;
uniform sampler2D map;
void main() {
vec4 value = clamp(texture2D(map, vUv), 0., 1.);
vec4 bg = vec4(1);
//bg = mix(bg, vec4(0, 0, 1, 1), value.a * 0.7);
//bg = mix(bg, vec4(1, 0, 0, 1), value.b * 0.7);
if (value.r == 1.) {
bg = vec4(1, 0, 0, 1);
}
if (value.g == 1.) {
bg = vec4(0.1, 0.1, 1, 1);
}
gl_FragColor = bg;
}

View file

@ -0,0 +1,10 @@
precision highp float;
precision highp int;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
}

29
src/shaders/world.frag Normal file
View file

@ -0,0 +1,29 @@
precision highp float;
precision highp int;
varying vec2 vUv;
uniform sampler2D tLastState;
uniform sampler2D tDiscreteAnts;
uniform vec4 pointerData;
void main() {
vec4 lastState = texture2D(tLastState, vUv);
vec3 discreteAnts = texture2D(tDiscreteAnts, vUv).xyz;
float isFood = lastState.x;
float isHome = lastState.y;
float scentToHome = lastState.z + discreteAnts.x * 4.;
float scentToFood = lastState.w + discreteAnts.y * 4.;
if (discreteAnts.z == 1.) {
isFood = 0.;
}
if (distance(pointerData.zw, vUv) < 0.02) {
isFood = max(isFood, pointerData.x);
isHome = max(isHome, pointerData.y);
}
gl_FragColor = vec4(isFood, isHome, scentToHome, scentToFood);
}

12
src/shaders/world.vert Normal file
View file

@ -0,0 +1,12 @@
precision highp float;
precision highp int;
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}

View file

@ -0,0 +1,28 @@
#version 300 es
precision highp float;
precision highp int;
in vec2 vUv;
uniform sampler2D tWorld;
out highp vec4 FragColor;
void main() {
float cellSize = 1. / vec2(textureSize(tWorld, 0)).x * 0.2;
vec4 s0 = texture(tWorld, vUv);
vec4 s1 = texture(tWorld, vUv + vec2(1, 1) * cellSize);
vec4 s2 = texture(tWorld, vUv + vec2(-1, -1) * cellSize);
vec4 s3 = texture(tWorld, vUv + vec2(-1, 1) * cellSize);
vec4 s4 = texture(tWorld, vUv + vec2(1, -1) * cellSize);
float scentToHome = (s0.z + s1.z + s2.z + s3.z + s4.z) / 5. * 0.998;
float scentToFood = (s0.w + s1.w + s2.w + s3.w + s4.w) / 5. * 0.998;
float threshold = 0.01;
FragColor = vec4(s0.x, s0.y, scentToHome < threshold ? 0. : scentToHome, scentToFood < threshold ? 0. : scentToFood);
//FragColor = vec4(s0.x, s0.y, s0.z * 0.995, s0.w * 0.995);
}

View file

@ -0,0 +1,14 @@
#version 300 es
precision highp float;
precision highp int;
in vec3 position;
in vec2 uv;
out vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}

View file

@ -0,0 +1,28 @@
import * as THREE from "three";
const positionBuffer = new Float32Array([
-1, 3, 0,
-1, -1, 0,
3, -1, 0,
]);
const uvBuffer = new Float32Array([
0, 2,
0, 0,
2, 0
]);
export default class FullScreenTriangleGeometry extends THREE.BufferGeometry {
constructor() {
super();
this.setAttribute(
'position',
new THREE.BufferAttribute(positionBuffer, 3)
);
this.setAttribute(
'uv',
new THREE.BufferAttribute(uvBuffer, 2)
);
}
}

21
tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"lib": [
"es2019",
"dom"
],
"allowJs": true,
"sourceMap": true,
"moduleResolution": "node",
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
},
"include": ["src"]
}

36
webpack.config.js Normal file
View file

@ -0,0 +1,36 @@
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = [{
entry: './src/App.ts',
output: {
filename: './js/main.js',
path: path.resolve(__dirname, 'build')
},
devtool: 'inline-source-map',
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
minify: false
})
],
module: {
rules: [
{
test: /\.ts|.tsx$/,
loader: 'ts-loader',
options: {configFile: 'tsconfig.json'},
exclude: /node_modules/
}, {
test: /\.(frag|vert)$/i,
use: 'raw-loader'
}
]
},
resolve: {
extensions: ['.ts', '.js']
}
}];