mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2025-01-07 12:56:31 +01:00
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
This commit is contained in:
parent
1fe8297e72
commit
78839fe7eb
9 changed files with 256 additions and 73 deletions
|
@ -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();
|
||||
|
|
|
@ -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<T> {
|
|||
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<T> {
|
|||
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 <I> void distributeWork(TaskEngine taskEngine, List<I> instances, Consumer<I> 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<T> {
|
|||
public void remove(T obj) {
|
||||
if (!Backend.isOn()) return;
|
||||
|
||||
if (canCreateInstance(obj)) {
|
||||
getStorage().remove(obj);
|
||||
}
|
||||
getStorage().remove(obj);
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -18,4 +18,9 @@ public class SerialTaskEngine implements TaskEngine {
|
|||
public void syncPoint() {
|
||||
// noop
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThreadCount() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,4 +9,6 @@ public interface TaskEngine {
|
|||
* Wait for all running jobs to finish.
|
||||
*/
|
||||
void syncPoint();
|
||||
|
||||
int getThreadCount();
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import com.jozufozu.flywheel.backend.instancing.AbstractInstance;
|
|||
|
||||
public interface Effect {
|
||||
|
||||
Collection<? extends AbstractInstance> createInstances(InstancerManager instancerManager);
|
||||
Collection<AbstractInstance> createInstances(InstancerManager instancerManager);
|
||||
}
|
||||
|
|
|
@ -115,17 +115,11 @@ public class EffectInstanceManager extends InstanceManager<Effect> {
|
|||
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) {
|
||||
|
|
|
@ -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<ExampleEffect> 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<Instance> effects;
|
||||
|
||||
public ExampleEffect(Level level, Vec3 targetPoint) {
|
||||
private final List<Boid> 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<? extends AbstractInstance> createInstances(InstancerManager instancerManager) {
|
||||
public Collection<AbstractInstance> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue