/*
* Copyright 2016 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.logic.particles;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.terasology.utilities.Assets;
import org.terasology.config.Config;
import org.terasology.entitySystem.entity.EntityManager;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.entity.lifecycleEvents.BeforeDeactivateComponent;
import org.terasology.entitySystem.entity.lifecycleEvents.OnActivatedComponent;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.systems.BaseComponentSystem;
import org.terasology.entitySystem.systems.RegisterMode;
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.entitySystem.systems.RenderSystem;
import org.terasology.entitySystem.systems.UpdateSubscriberSystem;
import org.terasology.logic.location.LocationComponent;
import org.terasology.logic.particles.BlockParticleEffectComponent.Particle;
import org.terasology.math.geom.Vector2f;
import org.terasology.math.geom.Vector3f;
import org.terasology.math.geom.Vector3i;
import org.terasology.math.geom.Vector4f;
import org.terasology.registry.In;
import org.terasology.rendering.assets.material.Material;
import org.terasology.rendering.assets.texture.Texture;
import org.terasology.rendering.logic.NearestSortingList;
import org.terasology.rendering.opengl.OpenGLUtils;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.utilities.random.FastRandom;
import org.terasology.utilities.random.Random;
import org.terasology.world.WorldProvider;
import org.terasology.world.biomes.Biome;
import org.terasology.world.block.Block;
import org.terasology.world.block.BlockManager;
import org.terasology.world.block.BlockPart;
import org.terasology.world.block.tiles.WorldAtlas;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Iterator;
import static org.lwjgl.opengl.GL11.GL_ONE;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glCallList;
import static org.lwjgl.opengl.GL11.glDeleteLists;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glEndList;
import static org.lwjgl.opengl.GL11.glGenLists;
import static org.lwjgl.opengl.GL11.glNewList;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glScalef;
import static org.lwjgl.opengl.GL11.glTranslated;
import static org.lwjgl.opengl.GL11.glTranslatef;
/**
*/
// TODO: Generalise for non-block particles
// TODO: Dispose display lists
@RegisterSystem(RegisterMode.CLIENT)
public class BlockParticleEmitterSystem extends BaseComponentSystem implements UpdateSubscriberSystem, RenderSystem {
private static final int PARTICLES_PER_UPDATE = 32;
@In
private EntityManager entityManager;
@In
private WorldProvider worldProvider;
@In
private WorldAtlas worldAtlas;
// TODO: lose dependency on worldRenderer?
@In
private WorldRenderer worldRenderer;
@In
private Config config;
@In
private BlockManager blockManager;
private Random random = new FastRandom();
private NearestSortingList sorter = new NearestSortingList();
private int displayList;
@Override
public void initialise() {
if (displayList == 0) {
displayList = glGenLists(1);
glNewList(displayList, GL11.GL_COMPILE);
drawParticle();
glEndList();
}
sorter.initialise(worldRenderer.getActiveCamera());
}
@Override
public void shutdown() {
glDeleteLists(displayList, 1);
sorter.stop();
}
@Override
public void update(float delta) {
for (EntityRef entity : entityManager.getEntitiesWith(BlockParticleEffectComponent.class, LocationComponent.class)) {
BlockParticleEffectComponent particleEffect = entity.getComponent(BlockParticleEffectComponent.class);
Iterator<Particle> iterator = particleEffect.particles.iterator();
while (iterator.hasNext()) {
BlockParticleEffectComponent.Particle p = iterator.next();
p.lifeRemaining -= delta;
if (p.lifeRemaining <= 0) {
iterator.remove();
} else {
updateVelocity(entity, particleEffect, p, delta);
updatePosition(p, delta);
}
}
for (int i = 0; particleEffect.spawnCount > 0 && i < PARTICLES_PER_UPDATE; ++i) {
spawnParticle(particleEffect);
}
if (particleEffect.particles.size() == 0 && particleEffect.destroyEntityOnCompletion) {
entity.destroy();
}
}
}
@ReceiveEvent(components = {BlockParticleEffectComponent.class, LocationComponent.class})
public void onActivated(OnActivatedComponent event, EntityRef entity) {
sorter.add(entity);
}
@ReceiveEvent(components = {BlockParticleEffectComponent.class, LocationComponent.class})
public void onDeactivated(BeforeDeactivateComponent event, EntityRef entity) {
sorter.remove(entity);
}
private void spawnParticle(BlockParticleEffectComponent particleEffect) {
Particle p = new Particle();
p.lifeRemaining = random.nextFloat() * (particleEffect.maxLifespan - particleEffect.minLifespan) + particleEffect.minLifespan;
p.velocity = random.nextVector3f();
p.size = random.nextFloat() * (particleEffect.maxSize - particleEffect.minSize) + particleEffect.minSize;
p.position.set(
random.nextFloat(-particleEffect.spawnRange.x, particleEffect.spawnRange.x),
random.nextFloat(-particleEffect.spawnRange.y, particleEffect.spawnRange.y),
random.nextFloat(-particleEffect.spawnRange.z, particleEffect.spawnRange.z));
p.color = particleEffect.color;
if (particleEffect.blockType != null) {
final float tileSize = worldAtlas.getRelativeTileSize();
p.texSize.set(tileSize, tileSize);
Block b = blockManager.getBlock(particleEffect.blockType).getBlockFamily().getArchetypeBlock();
p.texOffset.set(b.getPrimaryAppearance().getTextureAtlasPos(BlockPart.FRONT));
if (particleEffect.randBlockTexDisplacement) {
final float relTileSize = worldAtlas.getRelativeTileSize();
Vector2f particleTexSize = new Vector2f(
relTileSize * particleEffect.randBlockTexDisplacementScale.y,
relTileSize * particleEffect.randBlockTexDisplacementScale.y);
p.texSize.x *= particleEffect.randBlockTexDisplacementScale.x;
p.texSize.y *= particleEffect.randBlockTexDisplacementScale.y;
p.texOffset.set(
p.texOffset.x + random.nextFloat() * (tileSize - particleTexSize.x),
p.texOffset.y + random.nextFloat() * (tileSize - particleTexSize.y));
}
}
//p.texSize.set(TEX_SIZE,TEX_SIZE);
particleEffect.particles.add(p);
particleEffect.spawnCount--;
}
protected void updateVelocity(EntityRef entity, BlockParticleEffectComponent particleEffect, Particle particle, float delta) {
Vector3f diff = new Vector3f(particleEffect.targetVelocity);
diff.sub(particle.velocity);
diff.x *= particleEffect.acceleration.x * delta;
diff.y *= particleEffect.acceleration.y * delta;
diff.z *= particleEffect.acceleration.z * delta;
particle.velocity.add(diff);
if (particleEffect.collideWithBlocks) {
LocationComponent location = entity.getComponent(LocationComponent.class);
Vector3f pos = location.getWorldPosition();
pos.add(particle.position);
if (worldProvider.getBlock(new Vector3f(pos.x, pos.y + 2 * Math.signum(particle.velocity.y) * particle.size, pos.z)).getId() != 0x0) {
particle.velocity.y = 0;
}
}
}
protected void updatePosition(Particle particle, float delta) {
particle.position.x += particle.velocity.x * delta;
particle.position.y += particle.velocity.y * delta;
particle.position.z += particle.velocity.z * delta;
}
@Override
public void renderAlphaBlend() {
if (config.getRendering().isRenderNearest()) {
render(Arrays.asList(sorter.getNearest(config.getRendering().getParticleEffectLimit())));
} else {
render(entityManager.getEntitiesWith(BlockParticleEffectComponent.class, LocationComponent.class));
}
}
private void render(Iterable<EntityRef> particleEntities) {
Assets.getMaterial("engine:prog.particle").get().enable();
glDisable(GL11.GL_CULL_FACE);
Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition();
for (EntityRef entity : particleEntities) {
LocationComponent location = entity.getComponent(LocationComponent.class);
if (null == location) {
continue;
}
Vector3f worldPos = location.getWorldPosition();
if (!worldProvider.isBlockRelevant(worldPos)) {
continue;
}
BlockParticleEffectComponent particleEffect = entity.getComponent(BlockParticleEffectComponent.class);
if (particleEffect.texture == null) {
Texture terrainTex = Assets.getTexture("engine:terrain").get();
if (terrainTex == null || !terrainTex.isLoaded()) {
return;
}
GL13.glActiveTexture(GL13.GL_TEXTURE0);
glBindTexture(GL11.GL_TEXTURE_2D, terrainTex.getId());
} else if (particleEffect.texture.isLoaded()) {
GL13.glActiveTexture(GL13.GL_TEXTURE0);
glBindTexture(GL11.GL_TEXTURE_2D, particleEffect.texture.getId());
} else {
return;
}
if (particleEffect.blendMode == BlockParticleEffectComponent.ParticleBlendMode.ADD) {
glBlendFunc(GL_ONE, GL_ONE);
}
if (particleEffect.blockType != null) {
renderBlockParticles(worldPos, cameraPosition, particleEffect);
} else {
renderParticles(worldPos, cameraPosition, particleEffect);
}
if (particleEffect.blendMode == BlockParticleEffectComponent.ParticleBlendMode.ADD) {
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
}
glEnable(GL11.GL_CULL_FACE);
}
private void renderBlockParticles(Vector3f worldPos, Vector3f cameraPosition, BlockParticleEffectComponent particleEffect) {
Vector3i worldPos3i = new Vector3i(worldPos, RoundingMode.HALF_UP);
Biome biome = worldProvider.getBiome(worldPos3i);
glPushMatrix();
glTranslated(worldPos.x - cameraPosition.x, worldPos.y - cameraPosition.y, worldPos.z - cameraPosition.z);
for (Particle particle : particleEffect.particles) {
glPushMatrix();
glTranslatef(particle.position.x, particle.position.y, particle.position.z);
OpenGLUtils.applyBillboardOrientation();
glScalef(particle.size, particle.size, particle.size);
float light = worldRenderer.getRenderingLightIntensityAt(new Vector3f(worldPos.x + particle.position.x,
worldPos.y + particle.position.y, worldPos.z + particle.position.z));
renderParticle(particle, blockManager.getBlock(particleEffect.blockType).getBlockFamily().getArchetypeBlock(), biome, light);
glPopMatrix();
}
glPopMatrix();
}
private void renderParticles(Vector3f worldPos, Vector3f cameraPosition, BlockParticleEffectComponent particleEffect) {
glPushMatrix();
glTranslated(worldPos.x - cameraPosition.x, worldPos.y - cameraPosition.y, worldPos.z - cameraPosition.z);
for (Particle particle : particleEffect.particles) {
glPushMatrix();
glTranslatef(particle.position.x, particle.position.y, particle.position.z);
OpenGLUtils.applyBillboardOrientation();
glScalef(particle.size, particle.size, particle.size);
float light = worldRenderer.getRenderingLightIntensityAt(new Vector3f(worldPos.x + particle.position.x,
worldPos.y + particle.position.y, worldPos.z + particle.position.z));
renderParticle(particle, light);
glPopMatrix();
}
glPopMatrix();
}
protected void renderParticle(Particle particle, float light) {
Material mat = Assets.getMaterial("engine:prog.particle").get();
mat.setFloat4("colorOffset", particle.color.x, particle.color.y, particle.color.z, particle.color.w, true);
mat.setFloat2("texOffset", particle.texOffset.x, particle.texOffset.y, true);
mat.setFloat2("texScale", particle.texSize.x, particle.texSize.y, true);
mat.setFloat("light", light, true);
glCallList(displayList);
}
protected void renderParticle(Particle particle, Block block, Biome biome, float light) {
Material mat = Assets.getMaterial("engine:prog.particle").get();
Vector4f colorMod = block.calcColorOffsetFor(BlockPart.FRONT, biome);
mat.setFloat4("colorOffset", particle.color.x * colorMod.x, particle.color.y * colorMod.y, particle.color.z * colorMod.z, particle.color.w * colorMod.w, true);
mat.setFloat2("texOffset", particle.texOffset.x, particle.texOffset.y, true);
mat.setFloat2("texScale", particle.texSize.x, particle.texSize.y, true);
mat.setFloat("light", light, true);
glCallList(displayList);
}
private void drawParticle() {
glBegin(GL_QUADS);
GL11.glTexCoord2f(0.0f, 0.0f);
GL11.glVertex3f(-0.5f, 0.5f, 0.0f);
GL11.glTexCoord2f(1.0f, 0.0f);
GL11.glVertex3f(0.5f, 0.5f, 0.0f);
GL11.glTexCoord2f(1.0f, 1.0f);
GL11.glVertex3f(0.5f, -0.5f, 0.0f);
GL11.glTexCoord2f(0.0f, 1.0f);
GL11.glVertex3f(-0.5f, -0.5f, 0.0f);
glEnd();
}
@Override
public void renderOpaque() {
}
@Override
public void renderOverlay() {
}
@Override
public void renderFirstPerson() {
}
@Override
public void renderShadows() {
}
}