From 78839fe7eb75c44ec6c8b4f34a02735e78baa45b Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Mon, 18 Jul 2022 23:33:00 -0700 Subject: [PATCH] Too much fun with boids - Fix tickable instances ticking while paused - Fix CME on effect origin shift - ExampleEffect is now boids fireflies - Better tick/update load distribution with small instance counts --- .../java/com/jozufozu/flywheel/Flywheel.java | 3 +- .../backend/instancing/InstanceManager.java | 51 ++-- .../backend/instancing/InstanceWorld.java | 3 + .../instancing/ParallelTaskEngine.java | 5 + .../backend/instancing/SerialTaskEngine.java | 5 + .../backend/instancing/TaskEngine.java | 2 + .../backend/instancing/effect/Effect.java | 2 +- .../effect/EffectInstanceManager.java | 14 +- .../vanilla/effect/ExampleEffect.java | 244 +++++++++++++++--- 9 files changed, 256 insertions(+), 73 deletions(-) diff --git a/src/main/java/com/jozufozu/flywheel/Flywheel.java b/src/main/java/com/jozufozu/flywheel/Flywheel.java index 13b8c46f9..1c8c1d90d 100644 --- a/src/main/java/com/jozufozu/flywheel/Flywheel.java +++ b/src/main/java/com/jozufozu/flywheel/Flywheel.java @@ -109,7 +109,8 @@ public class Flywheel { modEventBus.addListener(StitchedSprite::onTextureStitchPre); modEventBus.addListener(StitchedSprite::onTextureStitchPost); - // forgeEventBus.addListener(ExampleEffect::spawn); +// forgeEventBus.addListener(ExampleEffect::tick); +// forgeEventBus.addListener(ExampleEffect::onReload); LayoutShaders.init(); InstanceShaders.init(); diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java index 3a01acd34..7dbe4ca4c 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java @@ -2,7 +2,9 @@ package com.jozufozu.flywheel.backend.instancing; import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.function.Consumer; import com.jozufozu.flywheel.api.instance.DynamicInstance; import com.jozufozu.flywheel.api.instance.TickableInstance; @@ -82,24 +84,10 @@ public abstract class InstanceManager { int cZ = (int) cameraZ; var instances = getStorage().getInstancesForTicking(); - int incr = 500; - int size = instances.size(); - int start = 0; - while (start < size) { - int end = Math.min(start + incr, size); - - var sub = instances.subList(start, end); - taskEngine.submit(() -> { - for (TickableInstance instance : sub) { - tickInstance(cX, cY, cZ, instance); - } - }); - - start += incr; - } + distributeWork(taskEngine, instances, instance -> tickInstance(instance, cX, cY, cZ)); } - protected void tickInstance(int cX, int cY, int cZ, TickableInstance instance) { + protected void tickInstance(TickableInstance instance, int cX, int cY, int cZ) { if (!instance.decreaseTickRateWithDistance()) { instance.tick(); return; @@ -129,20 +117,23 @@ public abstract class InstanceManager { int cZ = (int) camera.getPosition().z; var instances = getStorage().getInstancesForUpdate(); - int incr = 500; - int size = instances.size(); - int start = 0; - while (start < size) { - int end = Math.min(start + incr, size); + distributeWork(taskEngine, instances, instance -> updateInstance(instance, lookX, lookY, lookZ, cX, cY, cZ)); + } - var sub = instances.subList(start, end); - taskEngine.submit(() -> { - for (DynamicInstance dyn : sub) { - updateInstance(dyn, lookX, lookY, lookZ, cX, cY, cZ); - } - }); + private static void distributeWork(TaskEngine taskEngine, List instances, Consumer action) { + final int size = instances.size(); + final int threadCount = taskEngine.getThreadCount(); - start += incr; + if (threadCount == 1) { + taskEngine.submit(() -> instances.forEach(action)); + } else { + final int stride = Math.max(size / (threadCount * 2), 1); + for (int start = 0; start < size; start += stride) { + int end = Math.min(start + stride, size); + + var sub = instances.subList(start, end); + taskEngine.submit(() -> sub.forEach(action)); + } } } @@ -224,9 +215,7 @@ public abstract class InstanceManager { public void remove(T obj) { if (!Backend.isOn()) return; - if (canCreateInstance(obj)) { - getStorage().remove(obj); - } + getStorage().remove(obj); } public void invalidate() { diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceWorld.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceWorld.java index ca55fa1da..78ba4c6c3 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceWorld.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceWorld.java @@ -114,6 +114,9 @@ public class InstanceWorld { */ public void tick() { Minecraft mc = Minecraft.getInstance(); + + if (mc.isPaused()) return; + Entity renderViewEntity = mc.cameraEntity != null ? mc.cameraEntity : mc.player; if (renderViewEntity == null) return; diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/ParallelTaskEngine.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/ParallelTaskEngine.java index e5928d6f2..f929e6336 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/ParallelTaskEngine.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/ParallelTaskEngine.java @@ -44,6 +44,11 @@ public class ParallelTaskEngine implements TaskEngine { threadCount = getOptimalThreadCount(); } + @Override + public int getThreadCount() { + return threadCount; + } + public WorkGroupBuilder group(String name) { return new WorkGroupBuilder(name); } diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/SerialTaskEngine.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/SerialTaskEngine.java index 96361a2d6..1a6c8a9b6 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/SerialTaskEngine.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/SerialTaskEngine.java @@ -18,4 +18,9 @@ public class SerialTaskEngine implements TaskEngine { public void syncPoint() { // noop } + + @Override + public int getThreadCount() { + return 1; + } } diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/TaskEngine.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/TaskEngine.java index 76dbeb9c2..a38f5a5c2 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/TaskEngine.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/TaskEngine.java @@ -9,4 +9,6 @@ public interface TaskEngine { * Wait for all running jobs to finish. */ void syncPoint(); + + int getThreadCount(); } diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/Effect.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/Effect.java index dc0291e2d..e25b7143c 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/Effect.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/Effect.java @@ -7,5 +7,5 @@ import com.jozufozu.flywheel.backend.instancing.AbstractInstance; public interface Effect { - Collection createInstances(InstancerManager instancerManager); + Collection createInstances(InstancerManager instancerManager); } diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/EffectInstanceManager.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/EffectInstanceManager.java index 035875a2a..14f686f1e 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/EffectInstanceManager.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/effect/EffectInstanceManager.java @@ -115,17 +115,11 @@ public class EffectInstanceManager extends InstanceManager { public void recreateAll() { this.dynamicInstances.clear(); this.tickableInstances.clear(); - this.instances.asMap() - .forEach((obj, instances) -> { - instances.forEach(AbstractInstance::remove); - instances.clear(); + this.instances.values().forEach(AbstractInstance::remove); - var newInstances = obj.createInstances(manager); - - newInstances.forEach(this::setup); - - instances.addAll(newInstances); - }); + var backup = new ArrayList<>(this.instances.keySet()); + this.instances.clear(); + backup.forEach(this::create); } private void create(T obj) { diff --git a/src/main/java/com/jozufozu/flywheel/vanilla/effect/ExampleEffect.java b/src/main/java/com/jozufozu/flywheel/vanilla/effect/ExampleEffect.java index 967b5dee2..5ec62b48e 100644 --- a/src/main/java/com/jozufozu/flywheel/vanilla/effect/ExampleEffect.java +++ b/src/main/java/com/jozufozu/flywheel/vanilla/effect/ExampleEffect.java @@ -2,87 +2,255 @@ package com.jozufozu.flywheel.vanilla.effect; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import com.jozufozu.flywheel.api.InstancerManager; import com.jozufozu.flywheel.api.instance.DynamicInstance; +import com.jozufozu.flywheel.api.instance.TickableInstance; import com.jozufozu.flywheel.backend.instancing.AbstractInstance; import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; import com.jozufozu.flywheel.backend.instancing.effect.Effect; import com.jozufozu.flywheel.core.Models; import com.jozufozu.flywheel.core.structs.StructTypes; import com.jozufozu.flywheel.core.structs.model.TransformedPart; +import com.jozufozu.flywheel.event.ReloadRenderersEvent; +import com.jozufozu.flywheel.repack.joml.Vector3f; +import com.jozufozu.flywheel.util.AnimationTickHolder; import com.jozufozu.flywheel.util.box.GridAlignedBB; import com.jozufozu.flywheel.util.box.ImmutableBox; +import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.phys.Vec3; import net.minecraftforge.event.TickEvent; -import net.minecraftforge.fml.LogicalSide; +// http://www.kfish.org/boids/pseudocode.html public class ExampleEffect implements Effect { - private static final int INSTANCE_COUNT = 50; + private static final List ALL_EFFECTS = new ArrayList<>(); + + private static final int INSTANCE_COUNT = 500; + private static final float SPAWN_RADIUS = 8.0f; + private static final float LIMIT_RANGE = 10.0f; + private static final float SPEED_LIMIT = 0.1f; + private static final float RENDER_SCALE = 1 / 16f; + + private static final float SIGHT_RANGE = 5; + + + private static final float COHERENCE = 1f / 60f; + private static final float SEPARATION = 0.05f; + private static final float ALIGNMENT = 1 / 20f; + private static final float TENDENCY = 1 / 1000f; + private static final float AVERSION = 1; + + private static final float GNAT_JITTER = 0.05f; private final Level level; - private final Vec3 targetPoint; + private final Vector3f targetPoint; private final BlockPos blockPos; private final ImmutableBox volume; private final List effects; - public ExampleEffect(Level level, Vec3 targetPoint) { + private final List boids; + + public ExampleEffect(Level level, Vector3f targetPoint) { this.level = level; this.targetPoint = targetPoint; - this.blockPos = new BlockPos(targetPoint); - this.effects = new ArrayList<>(); + this.blockPos = new BlockPos(targetPoint.x, targetPoint.y, targetPoint.z); this.volume = GridAlignedBB.from(this.blockPos); + this.effects = new ArrayList<>(INSTANCE_COUNT); + this.boids = new ArrayList<>(INSTANCE_COUNT); } - public static void spawn(TickEvent.PlayerTickEvent event) { - if (event.side == LogicalSide.SERVER || event.phase == TickEvent.Phase.START) { + public static void tick(TickEvent.ClientTickEvent event) { + if (event.phase == TickEvent.Phase.END || Minecraft.getInstance().isPaused()) { return; } - Player player = event.player; - Level level = player.level; + trySpawnNewEffect(); + } - if (level.random.nextFloat() > 0.01) { + public static void onReload(ReloadRenderersEvent event) { + ALL_EFFECTS.clear(); + } + + private static void trySpawnNewEffect() { + Level level = Minecraft.getInstance().level; + Player player = Minecraft.getInstance().player; + + if (player == null || level == null) { return; } - var effects = InstancedRenderDispatcher.getEffects(level); + if (!ALL_EFFECTS.isEmpty() && level.random.nextFloat() > 0.005f) { + return; + } - effects.add(new ExampleEffect(level, player.position())); + Vec3 playerPos = player.position(); + + var x = (float) (playerPos.x + level.random.nextFloat(-20, 20)); + var y = (float) (playerPos.y + level.random.nextFloat(0, 5)); + var z = (float) (playerPos.z + level.random.nextFloat(-20, 20)); + + ExampleEffect effect = new ExampleEffect(level, new Vector3f(x, y, z)); + ALL_EFFECTS.add(effect); + InstancedRenderDispatcher.getEffects(level) + .queueAdd(effect); } @Override - public Collection createInstances(InstancerManager instancerManager) { + public Collection createInstances(InstancerManager instancerManager) { effects.clear(); + boids.clear(); for (int i = 0; i < INSTANCE_COUNT; i++) { - effects.add(new Instance(instancerManager, level)); + var x = targetPoint.x + level.random.nextFloat(-SPAWN_RADIUS, SPAWN_RADIUS); + var y = targetPoint.y + level.random.nextFloat(-SPAWN_RADIUS, SPAWN_RADIUS); + var z = targetPoint.z + level.random.nextFloat(-SPAWN_RADIUS, SPAWN_RADIUS); + + Boid boid = new Boid(x, y, z); + boids.add(boid); + effects.add(new Instance(instancerManager, level, boid)); } - return effects; + return Collections.unmodifiableList(effects); } - public class Instance extends AbstractInstance implements DynamicInstance { + public class Boid { + final Vector3f lastPosition; + final Vector3f position; + final Vector3f lastVelocity = new Vector3f(0); + final Vector3f velocity = new Vector3f(0); - TransformedPart firefly; + final Vector3f scratch = new Vector3f(0); + final Vector3f coherence = new Vector3f(0); + final Vector3f alignment = new Vector3f(0); - public Instance(InstancerManager instancerManager, Level level) { + public Boid(float x, float y, float z) { + lastPosition = new Vector3f(x, y, z); + position = new Vector3f(x, y, z); + } + + + private void beginTick() { + lastVelocity.set(velocity); + lastPosition.set(position); + } + + public void tick() { + beginTick(); + + int seen = 0; + coherence.set(0); + alignment.set(0); + for (Boid boid : boids) { + if (boid == this) { + continue; + } + + float distance = boid.lastPosition.distance(lastPosition); + + if (distance > SIGHT_RANGE) { + continue; + } + seen++; + + coherence(boid); + separation(boid); + alignment(boid); + } + + if (seen > 0) { + coherencePost(seen); + alignmentPost(seen); + } + //tend(ExampleEffect.this.targetPoint); + + avoidPlayer(); + + position.add(capSpeed(velocity)); + } + + private void avoidPlayer() { + var player = Minecraft.getInstance().player.position(); + scratch.set(player.x, player.y, player.z); + + float dsq = lastPosition.distanceSquared(scratch); + if (dsq > SIGHT_RANGE * SIGHT_RANGE) { + return; + } + + lastPosition.sub(scratch, scratch) + .mul(AVERSION / dsq); + + velocity.add(capSpeed(scratch)); + } + + private void coherence(Boid other) { + this.coherence.add(other.lastPosition); + } + + private void separation(Boid other) { + float dsq = lastPosition.distanceSquared(other.lastPosition); + var push = other.lastPosition.sub(lastPosition, this.scratch) + .mul(SEPARATION / dsq); + + this.velocity.sub(push); + } + + private void alignment(Boid boid) { + this.alignment.add(boid.lastVelocity); + } + + private void coherencePost(int seen) { + this.coherence.div(seen) + .sub(lastPosition) + .mul(COHERENCE); + this.velocity.add(capSpeed(this.coherence)); + } + + private void alignmentPost(int seen) { + this.alignment.div(seen) + .sub(lastVelocity) + .mul(ALIGNMENT); + + this.velocity.add(this.alignment); + } + + private void tend(Vector3f target) { + this.scratch.set(target) + .sub(lastPosition) + .mul(TENDENCY); + this.velocity.add(capSpeed(this.scratch)); + } + + private static Vector3f capSpeed(Vector3f vec) { + return vec.normalize(SPEED_LIMIT); + } + } + + public class Instance extends AbstractInstance implements DynamicInstance, TickableInstance { + + private final Boid self; + TransformedPart instance; + + public Instance(InstancerManager instancerManager, Level level, Boid self) { super(instancerManager, level); + this.self = self; } @Override public void init() { - firefly = instancerManager.factory(StructTypes.TRANSFORMED) + instance = instancerManager.factory(StructTypes.TRANSFORMED) .model(Models.block(Blocks.SHROOMLIGHT.defaultBlockState())) .createInstance(); - firefly.setBlockLight(15) + instance.setBlockLight(15) .setSkyLight(15); } @@ -93,7 +261,7 @@ public class ExampleEffect implements Effect { @Override public void remove() { - firefly.delete(); + instance.delete(); } @Override @@ -102,16 +270,32 @@ public class ExampleEffect implements Effect { } @Override - public void beginFrame() { - var x = level.random.nextFloat() * 3 - 1.5; - var y = level.random.nextFloat() * 3 - 1.5; - var z = level.random.nextFloat() * 3 - 1.5; + public void tick() { + self.tick(); + } - firefly.loadIdentity() - .translate(instancerManager.getOriginCoordinate()) - .translate(targetPoint) + @Override + public void beginFrame() { + float partialTicks = AnimationTickHolder.getPartialTicks(); + + var x = Mth.lerp(partialTicks, self.lastPosition.x, self.position.x); + var y = Mth.lerp(partialTicks, self.lastPosition.y, self.position.y); + var z = Mth.lerp(partialTicks, self.lastPosition.z, self.position.z); + + instance.loadIdentity() + .translateBack(instancerManager.getOriginCoordinate()) .translate(x, y, z) - .scale(1 / 16f); + .scale(RENDER_SCALE); + } + + @Override + public boolean decreaseTickRateWithDistance() { + return false; + } + + @Override + public boolean decreaseFramerateWithDistance() { + return false; } } }