diff --git a/package.json b/package.json index d46ee47..30ea841 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "scripts": { "build": "webpack --config ./webpack.config.js --mode=production", "dev": "webpack serve --config ./webpack.config.js --mode=development", - "gh-pages": "push-dir --dir=build --branch=gh-pages" + "gh-pages": "npm run build && push-dir --dir=build --branch=gh-pages" }, "author": "", "license": "MIT" diff --git a/src/App.ts b/src/App.ts index 4864587..cba9a9f 100644 --- a/src/App.ts +++ b/src/App.ts @@ -6,6 +6,7 @@ import AntsDiscretizeScene from "./scenes/AntsDiscretizeScene"; import WorldBlurScene from "./scenes/WorldBlurScene"; import Config from "./Config"; import GUI from "./GUI"; +import DrawScene from "./scenes/DrawScene"; export interface SceneCollection { ants: AntsComputeScene; @@ -13,6 +14,7 @@ export interface SceneCollection { worldBlur: WorldBlurScene; discretize: AntsDiscretizeScene; screen: ScreenScene; + draw: DrawScene; } export default new class App { @@ -22,6 +24,7 @@ export default new class App { private renderLoop = (deltaTime: number): void => this.render(deltaTime); private simInterval: NodeJS.Timer; private simulationStepsPerSecond: number = 0; + private simStarted: boolean = false; constructor() { this.initScenes(); @@ -47,6 +50,8 @@ export default new class App { } this.simulationStep(); + + this.simStarted = true; }, 1000 / this.simulationStepsPerSecond); } @@ -57,12 +62,13 @@ export default new class App { worldBlur: new WorldBlurScene(this.renderer), discretize: new AntsDiscretizeScene(this.renderer), screen: new ScreenScene(this.renderer), + draw: new DrawScene(this.renderer) }; } private resize() { - const width = window.innerWidth; - const height = window.innerHeight; + const width = window.innerWidth * window.devicePixelRatio; + const height = window.innerHeight * window.devicePixelRatio; this.renderer.resizeCanvas(width, height); @@ -82,6 +88,10 @@ export default new class App { private render(deltaTime: number) { requestAnimationFrame(this.renderLoop); + if (!this.simStarted) { + return; + } + this.renderer.renderToScreen(this.scenes); } } \ No newline at end of file diff --git a/src/Config.ts b/src/Config.ts index 5c55aee..86f297a 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -8,5 +8,6 @@ export default { scentMaxStorage: 1e6, scentPerMarker: 1000, antSpeed: 1, - antRotationAngle: Math.PI / 50 + antRotationAngle: Math.PI / 50, + brushRadius: 20, }; \ No newline at end of file diff --git a/src/GUI.ts b/src/GUI.ts index f211709..95ff06c 100644 --- a/src/GUI.ts +++ b/src/GUI.ts @@ -15,6 +15,8 @@ export default class GUI { const controlsFolder = this.gui.addFolder('Controls'); + controlsFolder.add(Config, 'brushRadius', 1, 100); + simFolder.open(); controlsFolder.open(); } diff --git a/src/Renderer.ts b/src/Renderer.ts index 717f424..1462980 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -4,6 +4,7 @@ import Config from "./Config"; interface Resources { worldRenderTarget: THREE.WebGLRenderTarget; + worldRenderTargetCopy: THREE.WebGLRenderTarget; worldBlurredRenderTarget: THREE.WebGLRenderTarget; antsDataRenderTarget0: THREE.WebGLRenderTarget; antsDataRenderTarget1: THREE.WebGLRenderTarget; @@ -31,12 +32,19 @@ export default class Renderer { magFilter: THREE.LinearFilter, minFilter: THREE.LinearFilter, }), + worldRenderTargetCopy: new THREE.WebGLRenderTarget(Config.worldSize, Config.worldSize, { + format: THREE.RGBAFormat, + type: THREE.FloatType, + depthBuffer: false, + magFilter: THREE.NearestFilter, + 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, + magFilter: THREE.NearestFilter, + minFilter: THREE.NearestFilter, }), antsDataRenderTarget0: new THREE.WebGLRenderTarget(antTextureSize, antTextureSize, { format: THREE.RGBAFormat, @@ -65,39 +73,47 @@ export default class Renderer { public renderSimulation(scenes: SceneCollection) { const [antsComputeSource, antsComputeTarget] = scenes.ants.getRenderTargets(); + this.renderer.setViewport(0, 0, scenes.worldBlur.renderWidth, scenes.worldBlur.renderHeight); + + this.renderer.setRenderTarget(this.resources.worldBlurredRenderTarget); + scenes.worldBlur.material.uniforms.tWorld.value = this.resources.worldRenderTarget.texture; + this.renderer.render(scenes.worldBlur, scenes.worldBlur.camera); + 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 = scenes.worldBlur.getRenderTarget().texture; + scenes.ants.material.uniforms.tWorld.value = this.resources.worldBlurredRenderTarget.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()); + this.renderer.setRenderTarget(this.resources.antsDiscreteRenderTarget); 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(scenes.world.getRenderTarget()); - 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.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); - this.renderer.setViewport(0, 0, scenes.worldBlur.renderWidth, scenes.worldBlur.renderHeight); - - this.renderer.setRenderTarget(scenes.worldBlur.getRenderTarget()); - scenes.worldBlur.material.uniforms.tWorld.value = scenes.world.getRenderTarget().texture; - this.renderer.render(scenes.worldBlur, scenes.worldBlur.camera); - scenes.screen.material.uniforms.tData.value = antsComputeTarget.texture; - scenes.screen.groundMaterial.uniforms.map.value = scenes.worldBlur.getRenderTarget().texture; + scenes.screen.groundMaterial.uniforms.map.value = this.resources.worldRenderTargetCopy.texture; } public renderToScreen(scenes: SceneCollection) { + this.renderer.setViewport(0, 0, scenes.draw.renderWidth, scenes.draw.renderHeight); + 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.drawMode; + scenes.draw.material.uniforms.brushRadius.value = Config.brushRadius; + this.renderer.render(scenes.draw, scenes.draw.camera); + this.renderer.copyFramebufferToTexture(new THREE.Vector2(), this.resources.worldRenderTarget.texture); + this.renderer.setViewport(0, 0, scenes.screen.renderWidth, scenes.screen.renderHeight); this.renderer.setRenderTarget(null); this.renderer.render(scenes.screen, scenes.screen.camera); diff --git a/src/scenes/DrawScene.ts b/src/scenes/DrawScene.ts new file mode 100644 index 0000000..b0a9ad2 --- /dev/null +++ b/src/scenes/DrawScene.ts @@ -0,0 +1,49 @@ +import * as THREE from 'three'; +import Renderer from "../Renderer"; +import AbstractScene from "./AbstractScene"; +import FullScreenTriangleGeometry from "../utils/FullScreenTriangleGeometry"; +import fragmentShader from '../shaders/draw.frag'; +import vertexShader from '../shaders/draw.vert'; +import {WebGLRenderTarget} from "three"; + +export default class DrawScene extends AbstractScene { + public readonly camera: THREE.OrthographicCamera = new THREE.OrthographicCamera(); + public readonly material: THREE.RawShaderMaterial; + + constructor(renderer: Renderer) { + super(renderer); + + const geometry = new FullScreenTriangleGeometry(); + const material = new THREE.RawShaderMaterial({ + uniforms: { + tWorld: {value: null}, + pointerPosition: {value: new THREE.Vector2()}, + drawMode: {value: 0}, + brushRadius: {value: 0}, + }, + vertexShader, + fragmentShader, + defines: this.renderer.getCommonMaterialDefines(), + glslVersion: THREE.GLSL3 + }); + const mesh = new THREE.Mesh(geometry, material); + this.add(mesh); + + this.material = material; + + this.renderWidth = this.renderer.resources.worldRenderTarget.width; + this.renderHeight = this.renderer.resources.worldRenderTarget.height; + } + + public getRenderTarget(): WebGLRenderTarget { + return this.renderer.resources.worldRenderTarget; + } + + public resize(width: number, height: number) { + + } + + public update(deltaTime: number) { + + } +} \ No newline at end of file diff --git a/src/scenes/ScreenScene.ts b/src/scenes/ScreenScene.ts index eaffa38..c735c4f 100644 --- a/src/scenes/ScreenScene.ts +++ b/src/scenes/ScreenScene.ts @@ -8,25 +8,29 @@ import fragmentShaderGround from "../shaders/screenWorld.frag"; enum PointerState { None, - LMB, - RMB + Food, + Home, + Obstacle } export default class ScreenScene extends AbstractScene { - public readonly camera: THREE.PerspectiveCamera; + public readonly camera: THREE.OrthographicCamera; public readonly material: THREE.ShaderMaterial; public readonly groundMaterial: THREE.ShaderMaterial; public readonly pointerPosition: THREE.Vector2 = new THREE.Vector2(); - public pointerState: PointerState = PointerState.None; + public drawMode: PointerState = PointerState.None; + private cameraZoomLinear: number = 0; + private isPointerDown: boolean = false; constructor(renderer: Renderer) { super(renderer); const ground = new THREE.Mesh( - new THREE.PlaneBufferGeometry(10, 10), + new THREE.PlaneBufferGeometry(1, 1), new THREE.ShaderMaterial({ uniforms: { map: {value: this.renderer.resources.worldRenderTarget.texture}, + tDiscreteAnts: {value: this.renderer.resources.antsDiscreteRenderTarget.texture}, }, vertexShader: vertexShaderGround, fragmentShader: fragmentShaderGround, @@ -37,8 +41,8 @@ export default class ScreenScene extends AbstractScene { this.groundMaterial = ground.material; - ground.position.x += 5; - ground.position.y += 5; + //ground.position.x = 0.5; + //ground.position.y = 0.5; this.add(ground); @@ -60,28 +64,31 @@ export default class ScreenScene extends AbstractScene { }); const ants = new THREE.InstancedMesh( - new THREE.PlaneBufferGeometry(0.15, 0.15), + new THREE.PlaneBufferGeometry(0.015, 0.015), this.material, this.renderer.resources.antsDataRenderTarget0.width * this.renderer.resources.antsDataRenderTarget0.height ) + ants.position.x = ants.position.y = -0.5; + this.add(ants); - this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 10000); + this.camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5); 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) => { + this.renderer.canvas.addEventListener('contextmenu', e => { e.preventDefault(); }); - this.renderer.canvas.addEventListener('pointerdown', (e) => { + this.renderer.canvas.addEventListener('pointerdown', e => { + this.isPointerDown = true; + raycastVector.x = (e.clientX / window.innerWidth) * 2 - 1; raycastVector.y = -(e.clientY / window.innerHeight) * 2 + 1; @@ -92,45 +99,80 @@ export default class ScreenScene extends AbstractScene { 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 => { + if (this.isPointerDown) { + const dx = e.movementX; + const dy = e.movementY; + + this.camera.position.x -= dx / window.innerHeight / this.camera.zoom; + this.camera.position.y += dy / window.innerHeight / this.camera.zoom; + } + + 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.isPointerDown = false; + }); + + this.renderer.canvas.addEventListener('pointerleave', e => { + this.isPointerDown = false; + }); + + this.renderer.canvas.addEventListener('wheel', e => { + this.cameraZoomLinear -= e.deltaY * 0.001; + + this.updateCameraZoom(); + }); + + window.addEventListener('keydown', e => { + switch (e.code) { + case 'KeyQ': { + this.drawMode = PointerState.Home; + break; + } + case 'KeyW': { + this.drawMode = PointerState.Food; + break; + } + case 'KeyE': { + this.drawMode = PointerState.Obstacle; + break; } } }); - 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; + window.addEventListener('keyup', e => { + this.drawMode = PointerState.None; }); } - public getPointerData(): THREE.Vector4 { - return new THREE.Vector4( - +(this.pointerState === PointerState.LMB), - +(this.pointerState === PointerState.RMB), - this.pointerPosition.x, - this.pointerPosition.y - ); + private updateCameraZoom() { + this.camera.zoom = 2 ** this.cameraZoomLinear; + this.camera.updateProjectionMatrix(); } public resize(width: number, height: number) { - this.camera.aspect = width / height; + const aspect = width / height; + + this.camera.left = -0.5 * aspect; + this.camera.right = 0.5 * aspect; + this.camera.top = 0.5; + this.camera.bottom = -0.5; + this.camera.updateProjectionMatrix(); this.renderWidth = width; diff --git a/src/scenes/WorldComputeScene.ts b/src/scenes/WorldComputeScene.ts index 8ef5e85..4ff9194 100644 --- a/src/scenes/WorldComputeScene.ts +++ b/src/scenes/WorldComputeScene.ts @@ -16,9 +16,8 @@ export default class WorldComputeScene extends AbstractScene { const geometry = new FullScreenTriangleGeometry(); const material = new THREE.RawShaderMaterial({ uniforms: { - tLastState: {value: this.renderer.resources.worldRenderTarget.texture}, - tDiscreteAnts: {value: this.renderer.resources.antsDiscreteRenderTarget.texture}, - pointerData: {value: new THREE.Vector4()}, + tLastState: {value: null}, + tDiscreteAnts: {value: null} }, vertexShader, fragmentShader, diff --git a/src/shaders/ants.vert b/src/shaders/ants.vert index bf4f27b..57fc9f2 100644 --- a/src/shaders/ants.vert +++ b/src/shaders/ants.vert @@ -26,7 +26,7 @@ void main() { vec4 dataSample = texture(tData, vec2(sampleX, sampleY) / dataTextureSize); - vec2 offset = dataSample.xy * 10.; + vec2 offset = dataSample.xy; vec2 rotatedPosition = rotate(position.xy, -dataSample.z + PI * 0.5); vIsCarryingFood = float(int(dataSample.w) & 1); diff --git a/src/shaders/antsCompute.frag b/src/shaders/antsCompute.frag index 2231f47..50ca8b2 100644 --- a/src/shaders/antsCompute.frag +++ b/src/shaders/antsCompute.frag @@ -77,21 +77,6 @@ void main() { storage = 0.; } - if (tryGetFood(pos) && isCarrying == 0.) { - isCarrying = 1.; - angle += PI; - storage = getMaxScentStorage(vUv); - } - - if (tryDropFood(pos)) { - storage = getMaxScentStorage(vUv); - - if (isCarrying == 1.) { - isCarrying = 0.; - angle += PI; - } - } - if (isCarrying == 0.) { if (noise < 0.33) { vec2 offset = vec2(cos(angle), sin(angle)) * sampleDistance; @@ -181,6 +166,21 @@ void main() { angle += PI * (noise - 0.5); } + if (tryGetFood(pos) && isCarrying == 0.) { + isCarrying = 1.; + angle += PI; + storage = getMaxScentStorage(vUv); + } + + if (tryDropFood(pos)) { + storage = getMaxScentStorage(vUv); + + if (isCarrying == 1.) { + isCarrying = 0.; + angle += PI; + } + } + FragColor = vec4( pos.x, pos.y, diff --git a/src/shaders/antsDiscretize.vert b/src/shaders/antsDiscretize.vert index 830271f..6df4eca 100644 --- a/src/shaders/antsDiscretize.vert +++ b/src/shaders/antsDiscretize.vert @@ -38,7 +38,7 @@ void main() { vIsCellCleared = isCellCleared; gl_Position = vec4( - (position.xy * cellSize * 0.1 + floor(offset * WORLD_SIZE) / WORLD_SIZE + cellSize * 0.5) * 2. - 1., + (position.xy * cellSize * 0.01 + floor(offset * WORLD_SIZE) / WORLD_SIZE + cellSize * 0.5) * 2. - 1., 0, 1 ); diff --git a/src/shaders/draw.frag b/src/shaders/draw.frag new file mode 100644 index 0000000..95d193f --- /dev/null +++ b/src/shaders/draw.frag @@ -0,0 +1,28 @@ +precision highp float; +precision highp int; + +in vec2 vUv; + +out vec4 FragColor; + +uniform sampler2D tWorld; +uniform vec2 pointerPosition; +uniform float drawMode; +uniform float brushRadius; + +void main() { + vec4 lastState = texture(tWorld, vUv); + + float isFood = lastState.x; + float isHome = lastState.y; + + if (distance(pointerPosition, vUv) < brushRadius / WORLD_SIZE) { + if (drawMode == 1.) { + isFood = 1.; + } else if (drawMode == 2.) { + isHome = 1.; + } + } + + FragColor = vec4(isFood, isHome, lastState.zw); +} \ No newline at end of file diff --git a/src/shaders/draw.vert b/src/shaders/draw.vert new file mode 100644 index 0000000..1103143 --- /dev/null +++ b/src/shaders/draw.vert @@ -0,0 +1,12 @@ +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); +} \ No newline at end of file diff --git a/src/shaders/screenWorld.frag b/src/shaders/screenWorld.frag index d48b942..86ab9a1 100644 --- a/src/shaders/screenWorld.frag +++ b/src/shaders/screenWorld.frag @@ -6,21 +6,35 @@ in vec2 vUv; out vec4 FragColor; uniform sampler2D map; +uniform sampler2D tDiscreteAnts; void main() { vec4 value = clamp(texture(map, vUv), 0., 1.); - vec3 bg = vec3(0.9); - bg = mix(bg, vec3(0.2, 0.2, 0.8), clamp(value.a, 0., 1.)); - bg = mix(bg, vec3(0.8, 0.2, 0.2), clamp(value.b, 0., 1.)); + float isFood = value.r; + float isHome = value.g; + float toFood = value.b; + float toHome = value.a; - if (value.r == 1.) { - bg = vec3(1, 0.2, 0.2); + // The part below doen't seem right. + // I could figure out a better way to make pheromone colors blend properly on white background :( + + vec3 t = vec3(0.95, 0.2, 0.2) * toFood + vec3(0.2, 0.2, 0.95) * toHome; + float a = clamp(toHome + toFood, 0., 1.); + + t /= a; + + if (a == 0.) t = vec3(0); + + vec3 color = mix(vec3(1, 1, 1), t, a * 0.7); + + if (isFood == 1.) { + color = vec3(1, 0.1, 0.1); } - if (value.g == 1.) { - bg = vec3(0.2, 0.2, 1); + if (isHome == 1.) { + color = vec3(0.1, 0.1, 1); } - FragColor = vec4(bg, 1); + FragColor = vec4(color, 1); } \ No newline at end of file diff --git a/src/shaders/world.frag b/src/shaders/world.frag index 5449268..cbf6876 100644 --- a/src/shaders/world.frag +++ b/src/shaders/world.frag @@ -7,7 +7,6 @@ out vec4 FragColor; uniform sampler2D tLastState; uniform sampler2D tDiscreteAnts; -uniform vec4 pointerData; void main() { vec4 lastState = texture(tLastState, vUv); @@ -15,17 +14,12 @@ void main() { float isFood = lastState.x; float isHome = lastState.y; - float scentToHome = lastState.z + discreteAnts.x * 2.; - float scentToFood = lastState.w + discreteAnts.y * 2.; + float scentToHome = min(10., lastState.z + discreteAnts.x * 2.); + float scentToFood = min(10., lastState.w + discreteAnts.y * 2.); if (discreteAnts.z == 1.) { isFood = 0.; } - if (distance(pointerData.zw, vUv) < 0.02) { - isFood = max(isFood, pointerData.x); - isHome = max(isHome, pointerData.y); - } - FragColor = vec4(isFood, isHome, scentToHome, scentToFood); } \ No newline at end of file diff --git a/src/textures/food.png b/src/textures/food.png index 0fa6808..5cbda32 100644 Binary files a/src/textures/food.png and b/src/textures/food.png differ