Skip to content

Commit

Permalink
* add: MathHelper functions
Browse files Browse the repository at this point in the history
* add: new demo (simplex noise)
  • Loading branch information
joafalves committed Nov 6, 2022
1 parent 0573058 commit f8edf02
Show file tree
Hide file tree
Showing 2 changed files with 360 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package org.pixel.demo.concept.terragen;

import static org.lwjgl.opengl.GL11C.GL_NEAREST;
import static org.lwjgl.opengl.GL11C.GL_REPEAT;
import static org.lwjgl.opengl.GL11C.GL_RGBA;
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11C.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11C.glBindTexture;
import static org.lwjgl.opengl.GL11C.glGenTextures;
import static org.lwjgl.opengl.GL11C.glTexImage2D;
import static org.lwjgl.opengl.GL11C.glTexParameteri;
import static org.lwjgl.system.libc.LibCStdlib.free;

import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.lwjgl.BufferUtils;
import org.pixel.commons.DeltaTime;
import org.pixel.commons.logger.Logger;
import org.pixel.commons.logger.LoggerFactory;
import org.pixel.content.Texture;
import org.pixel.core.Camera2D;
import org.pixel.core.PixelWindow;
import org.pixel.core.WindowSettings;
import org.pixel.graphics.Color;
import org.pixel.graphics.render.SpriteBatch;
import org.pixel.input.keyboard.Keyboard;
import org.pixel.input.keyboard.KeyboardKey;
import org.pixel.math.MathHelper;
import org.pixel.math.Rectangle;

public class TerragenAdvancedGame extends PixelWindow {

private final Logger log = LoggerFactory.getLogger(TerragenGame.class);

private final static int SCREEN_WIDTH = 1280;
private final static int SCREEN_HEIGHT = 720;
private final static int COLUMNS = SCREEN_WIDTH;
private final static int ROWS = SCREEN_HEIGHT;

private final ByteBuffer colorData = BufferUtils.createByteBuffer(COLUMNS * ROWS * 4);
private final ByteBuffer heightMapData = BufferUtils.createByteBuffer(COLUMNS * ROWS * 4);

private Texture colorTexture;
private Texture heightMapTexture;
private SpriteBatch spriteBatch;
private Camera2D gameCamera;
private long seed;
private boolean showHeightOnly = false;
private int maxElevation = 32;
private int minElevation = 1;
private double amplitude = maxElevation / 2.0;
private double frequency = 0.003906;
private int octaves = 6;
private double lacunarity = 2.1;
private double persistence = 0.5;
private HashMap<Integer, Color> colorMap;

/**
* Constructor.
*
* @param settings The settings to use.
*/
public TerragenAdvancedGame(WindowSettings settings) {
super(settings);
}

@Override
public void load() {
spriteBatch = new SpriteBatch();
gameCamera = new Camera2D(this);
gameCamera.setOrigin(0);
seed = ThreadLocalRandom.current().nextLong();
colorTexture = new Texture(glGenTextures(), SCREEN_WIDTH, SCREEN_HEIGHT);
heightMapTexture = new Texture(glGenTextures(), SCREEN_WIDTH, SCREEN_HEIGHT);

setBackgroundColor(Color.BLACK);

final var startNanoTimestamp = System.nanoTime();
generateTextures();
final var elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanoTimestamp);
final var totalTiles = COLUMNS * ROWS;
log.info("Noise map initialized in {}ms with seed '{}' (tiles: {}).", elapsed, seed, totalTiles);
}

private void generateColorMap() {
colorMap = new HashMap<>();
for (int i = 1; i <= maxElevation; i++) {
var elevation = i / (float) maxElevation;
Color color;
if (elevation > 0.90f) {
color = new Color(0xCFCFCFff);
} else if (elevation > 0.85f) {
color = new Color(0xC7C7C7ff);
} else if (elevation > 0.80f) {
color = new Color(0xA0A28Fff);
} else if (elevation > 0.70f) {
color = new Color(0x8C8E7Bff);
} else if (elevation > 0.55f) {
color = new Color(0x5A7F32ff);
} else if (elevation > 0.40f) {
color = new Color(0x3C6114ff);
} else if (elevation > 0.30f) {
color = new Color(0x867645ff);
} else if (elevation > 0.20f) {
color = new Color(0xA49463ff);
} else if (elevation > 0.15f) {
color = new Color(0x0952C6ff);
} else if (elevation > 0.07f) {
color = new Color(0x084BB5ff);
} else {
color = new Color(0x003EB2ff);
}
colorMap.put(i, color);
}
}

private double noise(double x, double y) {
return (OpenSimplexNoise.noise2(seed, x, y));
}

private void generateTextures() {
generateColorMap();
heightMapData.clear();
colorData.clear();

var distributionMap = new HashMap<Integer, Long>();
var min = 100f;
var max = -100f;
for (int y = 0; y < SCREEN_HEIGHT; y++) {
for (int x = 0; x < SCREEN_WIDTH; x++) {
var elevation = amplitude;
var tmpFrequency = frequency;
var tmpAmplitude = amplitude;
for (int o = 0; o < octaves; o++) {
double nx = x * tmpFrequency; // + offsetX
double ny = y * tmpFrequency; // + offsetY
elevation += noise(nx, ny) * tmpAmplitude;
tmpFrequency *= lacunarity;
tmpAmplitude *= persistence;
}
if (elevation > max) {
max = (float) elevation;
}
if (elevation < min) {
min = (float) elevation;
}
elevation = MathHelper.clamp(MathHelper.round(elevation), minElevation, maxElevation);
distributionMap.putIfAbsent((int) elevation, 1L);
distributionMap.computeIfPresent((int) elevation, (key, count) -> count + 1);

var color = colorMap.get((int) elevation);
putColor(colorData, color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
putColor(heightMapData, 1, 1, 1, (float) (elevation / maxElevation));
}
}

log.info("Distribution:");
distributionMap.forEach((key, count) ->
log.info("{} -> {}", key, count));
log.info("Elevation (min: {}, max {})", new BigDecimal(min).toPlainString(),
new BigDecimal(max).toPlainString());

assignBufferToTexture(colorData, colorTexture);
assignBufferToTexture(heightMapData, heightMapTexture);
updateStatus();
}

private void generateColor(ByteBuffer buffer, double elevation) {
if (elevation <= 4) {
putColor(buffer, 0, 0, MathHelper.linearInterpolation(0.0f, 1f, (float) (elevation / 4f)), 1);

} else if (elevation <= 16) {
putColor(buffer, 0, MathHelper.linearInterpolation(0.0f, 1f, (float) (elevation - 4 / 12f)), 0, 1);

} else {
putColor(buffer, 1, 1, 1, (float) (elevation / maxElevation));
}
}

private void putColor(ByteBuffer buffer, float r, float g, float b, float a) {
buffer.put((byte) (r * 255));
buffer.put((byte) (g * 255));
buffer.put((byte) (b * 255));
buffer.put((byte) (a * 255));
}

private void assignBufferToTexture(ByteBuffer buffer, Texture texture) {
glBindTexture(GL_TEXTURE_2D, texture.getId());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, COLUMNS, ROWS, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer.flip());
glBindTexture(GL_TEXTURE_2D, 0);
}

@Override
public void update(DeltaTime delta) {
if (Keyboard.isKeyPressed(KeyboardKey.R)) {
seed = ThreadLocalRandom.current().nextLong();
generateTextures();
} else if (Keyboard.isKeyPressed(KeyboardKey.H)) {
showHeightOnly = !showHeightOnly;
}

if (Keyboard.isKeyDown(KeyboardKey.F)) {
if (Keyboard.isKeyPressed(KeyboardKey.KP_ADD)) {
frequency *= 2.0;
generateTextures();
} else if (Keyboard.isKeyPressed(KeyboardKey.KP_SUBTRACT)) {
frequency /= 2.0;
generateTextures();
}
} else if (Keyboard.isKeyDown(KeyboardKey.O)) {
if (Keyboard.isKeyPressed(KeyboardKey.KP_ADD)) {
octaves += 1;
generateTextures();
} else if (Keyboard.isKeyPressed(KeyboardKey.KP_SUBTRACT)) {
octaves -= 1;
generateTextures();
}
} else if (Keyboard.isKeyDown(KeyboardKey.L)) {
if (Keyboard.isKeyPressed(KeyboardKey.KP_ADD)) {
lacunarity += .1;
generateTextures();
} else if (Keyboard.isKeyPressed(KeyboardKey.KP_SUBTRACT)) {
lacunarity -= .1;
generateTextures();
}
} else if (Keyboard.isKeyDown(KeyboardKey.P)) {
if (Keyboard.isKeyPressed(KeyboardKey.KP_ADD)) {
persistence += .1;
generateTextures();
} else if (Keyboard.isKeyPressed(KeyboardKey.KP_SUBTRACT)) {
persistence -= .1;
generateTextures();
}
} else if (Keyboard.isKeyDown(KeyboardKey.E)) {
if (Keyboard.isKeyPressed(KeyboardKey.KP_ADD)) {
maxElevation *= 2;
amplitude = maxElevation / 2.0;
generateTextures();
} else if (Keyboard.isKeyPressed(KeyboardKey.KP_SUBTRACT)) {
maxElevation /= 2;
amplitude = maxElevation / 2.0;
generateTextures();
}
}
}

private void updateStatus() {
setWindowTitle(
String.format(
"Max [E]lev: %d | Amp: %f | [F]requency: %f | [O]ctaves: %d | [L]acunarity: %f | [P]ersistence: %f",
maxElevation, amplitude, frequency, octaves, lacunarity, persistence));
}

@Override
public void draw(DeltaTime delta) {
spriteBatch.begin(gameCamera.getViewMatrix());

if (showHeightOnly) {
spriteBatch.draw(heightMapTexture, new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT));
} else {
spriteBatch.draw(colorTexture, new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT));
}

spriteBatch.end();

if (Keyboard.isKeyPressed(KeyboardKey.P)) {
screenshot("./out_" + seed + ".png", false);
}
}

@Override
public void dispose() {
spriteBatch.dispose();
colorTexture.dispose();
heightMapTexture.dispose();
free(colorData);
free(heightMapData);
}

public static void main(String[] args) {
WindowSettings settings = new WindowSettings(SCREEN_WIDTH, SCREEN_HEIGHT);
settings.setWindowResizable(true);
settings.setMultisampling(2);
settings.setVsync(true);
settings.setDebugMode(false);
settings.setWindowWidth(SCREEN_WIDTH);
settings.setWindowHeight(SCREEN_HEIGHT);
settings.setIdleThrottle(false);

PixelWindow window = new TerragenAdvancedGame(settings);
window.start();
}
}
58 changes: 56 additions & 2 deletions modules/math/src/main/java/org/pixel/math/MathHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static float sqrt(float value) {
}

/**
* Returns the largest (closest to positive infinity) double value that is less than or equal to the argument and is
* Returns the largest (closest to positive infinity) float value that is less than or equal to the argument and is
* equal to a mathematical integer. Special cases:
*
* @param value The value.
Expand All @@ -58,7 +58,18 @@ public static float floor(float value) {
}

/**
* Returns the smallest (closest to negative infinity) double value that is greater than or equal to the argument
* Returns the largest (closest to positive infinity) double value that is less than or equal to the argument and is
* equal to a mathematical integer. Special cases:
*
* @param value The value.
* @return The largest integer less than or equal to the argument.
*/
public static double floor(double value) {
return StrictMath.floor(value);
}

/**
* Returns the smallest (closest to negative infinity) float value that is greater than or equal to the argument
* and is equal to a mathematical integer. Special cases:
*
* @param value The value.
Expand All @@ -68,6 +79,37 @@ public static float ceil(float value) {
return (float) StrictMath.ceil(value);
}

/**
* Returns the smallest (closest to negative infinity) double value that is greater than or equal to the argument
* and is equal to a mathematical integer. Special cases:
*
* @param value The value.
* @return The smallest integer greater than or equal to the argument.
*/
public static double ceil(double value) {
return StrictMath.ceil(value);
}

/**
* Returns the round float value of the given argument.
*
* @param value The value.
* @return The round float value of the given argument.
*/
public static float round(float value) {
return (float) StrictMath.round(value);
}

/**
* Returns the round double value of the given argument.
*
* @param value The value.
* @return The round float value of the given argument.
*/
public static double round(double value) {
return StrictMath.round(value);
}

/**
* Tangent function.
*
Expand Down Expand Up @@ -267,6 +309,18 @@ public static float clamp(float value, float min, float max) {
return value < min ? min : value > max ? max : value;
}

/**
* Clamps the given value between the given min and max values.
*
* @param value The value to clamp.
* @param min The minimum value.
* @param max The maximum value.
* @return The clamped value.
*/
public static double clamp(double value, double min, double max) {
return value < min ? min : value > max ? max : value;
}

/**
* Clamps the given value between the given min and max values.
*
Expand Down

0 comments on commit f8edf02

Please sign in to comment.