This commit is contained in:
Edouard DUPIN 2022-02-21 18:17:18 +01:00
parent 976afae490
commit 4d73d8ac48
42 changed files with 3372 additions and 3 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**/*.java__|**/__*.java" kind="src" path="src">
<classpathentry excluding="**/*.java__|**/__*.java|org/atriasoft/arkon/|org/atriasoft/eagle/" kind="src" path="src">
<attributes>
<attribute name="optional" value="true"/>
</attributes>

View File

@ -24,6 +24,9 @@
},
"trunk_2":{
"Kd":"0.057805 0.039546 0.013702"
},
"grass_1":{
"Kd":"0.057805 1.0 0.013702"
}
}
}

View File

@ -3,6 +3,7 @@
* @author Edouard DUPIN */
open module sample.atriasoft.ege {
exports sample.atriasoft.ege.mapFactory;
exports sample.atriasoft.ege.collisiontest;
exports sample.atriasoft.ege.lowPoly;
exports sample.atriasoft.ege.loxelEngine;
@ -10,4 +11,5 @@ open module sample.atriasoft.ege {
exports sample.atriasoft.ege.s1_texturedCube;
requires org.atriasoft.ege;
requires org.atriasoft.ewol; // for map factory
}

View File

@ -0,0 +1,69 @@
package sample.atriasoft.ege.mapFactory;
import org.atriasoft.etk.Configs;
import org.atriasoft.etk.math.Vector2f;
import org.atriasoft.ewol.context.EwolApplication;
import org.atriasoft.ewol.context.EwolContext;
public class Appl implements EwolApplication {
private void localCreate(final EwolContext context) {
// parse all the argument of the application
for (int iii = 0; iii < context.getCmd().size(); iii++) {
String tmpppp = context.getCmd().get(iii);
if (tmpppp == "-h" || tmpppp == "--help") {
Log.print(" -h/--help display this help");
System.exit(0);
}
}
context.setSize(new Vector2f(800, 600));
Configs.getConfigFonts().set("FreeSherif", 12);
// Create the windows
MainWindows basicWindows = new MainWindows();
// configure the ewol context to use the new windows
context.setWindows(basicWindows);
}
@Override
public void onCreate(final EwolContext context) {
Log.info("Application onCreate: [BEGIN]");
localCreate(context);
Log.info("Application onCreate: [ END ]");
}
@Override
public void onDestroy(final EwolContext context) {
Log.info("Application onDestroy: [BEGIN]");
Log.info("Application onDestroy: [ END ]");
}
@Override
public void onPause(final EwolContext context) {
Log.info("Application onPause: [BEGIN]");
Log.info("Application onPause: [ END ]");
}
@Override
public void onResume(final EwolContext context) {
Log.info("Application onResume: [BEGIN]");
Log.info("Application onResume: [ END ]");
}
@Override
public void onStart(final EwolContext context) {
Log.info("Application onStart: [BEGIN]");
Log.info("Application onStart: [ END ]");
}
@Override
public void onStop(final EwolContext context) {
Log.info("Application onStop: [BEGIN]");
Log.info("Application onStop: [ END ]");
}
}

View File

@ -0,0 +1,122 @@
package sample.atriasoft.ege.mapFactory;
import org.atriasoft.ege.Entity;
import org.atriasoft.ege.components.ComponentMesh;
import org.atriasoft.ege.components.ComponentPosition;
import org.atriasoft.ege.components.ComponentRenderMeshPalette;
import org.atriasoft.ege.components.ComponentTexturePalette;
import org.atriasoft.ege.engines.EngineLight;
import org.atriasoft.etk.Uri;
import org.atriasoft.etk.math.Transform3D;
import org.atriasoft.etk.math.Vector3f;
public class ApplScene extends EgeScene {
Ground ground = new Ground();
/**
* Constructor
*/
public ApplScene() {
addGenericGird();
// test entity
Entity groundEntity = new Entity(this.env);
ComponentPosition objectPosition = new ComponentPosition(new Transform3D(new Vector3f(0, 0, 0)));
groundEntity.addComponent(objectPosition);
//this.materialCube = new Material();
//basicTree.addComponent(new ComponentMaterial(this.materialCube));
//groundEntity.addComponent(new ComponentMesh(new Uri("DATA", "tree1.emf", "plop")));
groundEntity.addComponent(new ComponentMesh(this.ground.createMesh()));
groundEntity.addComponent(new ComponentTexturePalette(new Uri("DATA", "palette_1.json")));
//basicTree.addComponent(new ComponentRenderTexturedStaticMesh(new Uri("DATA", "basic.vert", "loxelEngine"), new Uri("DATA", "basic.frag", "loxelEngine")));
groundEntity
.addComponent(new ComponentRenderMeshPalette(new Uri("DATA", "basicPalette.vert"), new Uri("DATA", "basicPalette.frag"), (EngineLight) this.env.getEngine(EngineLight.ENGINE_NAME)));
this.env.addEntity(groundEntity);
}
}
/*
private float angleLight = 0;
private Quaternion basicRotation = Quaternion.IDENTITY;
private Quaternion basicRotation2 = Quaternion.IDENTITY;
private ComponentPosition lightPosition;
private Material materialCube;
private ComponentPosition objectPosition;
private ControlCameraSimple simpleControl;
public LowPolyApplication() {}
@Override
public void onCreate(final GaleContext context) {
// simple sun to have a global light ...
final Entity sun = new Entity(this.env);
sun.addComponent(new ComponentPosition(new Transform3D(new Vector3f(1000, 1000, 1000))));
sun.addComponent(new ComponentLightSun(new Light(new Color(1.0f, 1.0f, 1.0f), new Vector3f(0, 0, 0), new Vector3f(1.0f, 0, 0))));
this.env.addEntity(sun);
// add a cube to show where in the light ...
final Entity localLight = new Entity(this.env);
this.lightPosition = new ComponentPosition(new Transform3D(new Vector3f(-10, -10, 1)));
localLight.addComponent(this.lightPosition);
localLight.addComponent(new ComponentStaticMesh(new Uri("DATA", "cube-one.obj")));
localLight.addComponent(new ComponentTexture(new Uri("DATA", "grass.png")));
localLight.addComponent(new ComponentLight(new Light(new Color(0.0f, 0.0f, 2.0f), new Vector3f(0, 0, 0), new Vector3f(0.8f, 0.01f, 0.002f))));
localLight.addComponent(new ComponentRenderTexturedStaticMesh(new Uri("DATA", "basic.vert", "loxelEngine"), new Uri("DATA", "basic.frag", "loxelEngine")));
this.env.addEntity(localLight);
// Simple Gird
final Entity gird = new Entity(this.env);
gird.addComponent(new ComponentPosition(new Transform3D(new Vector3f(0, 0, 0))));
gird.addComponent(new ComponentStaticMesh(MeshGenerator.createGrid(5)));
gird.addComponent(new ComponentRenderColoredStaticMesh(new Uri("DATA", "wireColor.vert", "ege"), new Uri("DATA", "wireColor.frag", "ege")));
this.env.addEntity(gird);
// test entity
Entity basicTree = new Entity(this.env);
this.objectPosition = new ComponentPosition(new Transform3D(new Vector3f(0, 0, 0)));
basicTree.addComponent(this.objectPosition);
//this.materialCube = new Material();
//basicTree.addComponent(new ComponentMaterial(this.materialCube));
basicTree.addComponent(new ComponentMesh(new Uri("DATA", "tree1.emf")));
basicTree.addComponent(new ComponentTexturePalette(new Uri("DATA", "palette_1.json")));
//basicTree.addComponent(new ComponentRenderTexturedStaticMesh(new Uri("DATA", "basic.vert", "loxelEngine"), new Uri("DATA", "basic.frag", "loxelEngine")));
basicTree.addComponent(new ComponentRenderMeshPalette(new Uri("DATA", "basicPalette.vert"), new Uri("DATA", "basicPalette.frag"),
(EngineLight) this.env.getEngine(EngineLight.ENGINE_NAME)));
this.env.addEntity(basicTree);
basicTree = new Entity(this.env);
this.objectPosition = new ComponentPosition(new Transform3D(new Vector3f(3, 2, 0)));
basicTree.addComponent(this.objectPosition);
//this.materialCube = new Material();
//basicTree.addComponent(new ComponentMaterial(this.materialCube));
basicTree.addComponent(new ComponentMesh(new Uri("DATA", "tree2.emf")));
basicTree.addComponent(new ComponentTexturePalette(new Uri("DATA", "palette_1.json")));
//basicTree.addComponent(new ComponentRenderTexturedStaticMesh(new Uri("DATA", "basic.vert", "loxelEngine"), new Uri("DATA", "basic.frag", "loxelEngine")));
basicTree.addComponent(new ComponentRenderMeshPalette(new Uri("DATA", "basicPalette.vert"), new Uri("DATA", "basicPalette.frag"),
(EngineLight) this.env.getEngine(EngineLight.ENGINE_NAME)));
this.env.addEntity(basicTree);
this.simpleControl = new ControlCameraSimple(mainView);
this.env.addControlInterface(this.simpleControl);
// start the engine.
this.env.setPropertyStatus(GameStatus.gameStart);
this.basicRotation = Quaternion.fromEulerAngles(new Vector3f(0.005f, 0.005f, 0.01f));
this.basicRotation2 = Quaternion.fromEulerAngles(new Vector3f(0.003f, 0.01f, 0.001f));
// ready to let Gale & Ege manage the display
Log.info("==> Init APPL (END)");
}
@Override
public void onKeyboard(final KeySpecial special, final KeyKeyboard type, final Character value, final KeyStatus state) {
this.env.onKeyboard(special, type, value, state);
}
@Override
public void onPointer(final KeySpecial special, final KeyType type, final int pointerID, final Vector2f pos, final KeyStatus state) {
this.env.onPointer(special, type, pointerID, pos, state);
}
*/

View File

@ -0,0 +1,125 @@
package sample.atriasoft.ege.mapFactory;
import org.atriasoft.ege.Entity;
import org.atriasoft.ege.Environement;
import org.atriasoft.ege.camera.Camera;
import org.atriasoft.ege.components.ComponentPosition;
import org.atriasoft.ege.components.ComponentRenderColoredStaticMesh;
import org.atriasoft.ege.components.ComponentStaticMesh;
import org.atriasoft.ege.tools.MeshGenerator;
import org.atriasoft.esignal.Connection;
import org.atriasoft.etk.Uri;
import org.atriasoft.etk.math.Matrix4f;
import org.atriasoft.etk.math.Transform3D;
import org.atriasoft.etk.math.Vector2f;
import org.atriasoft.etk.math.Vector3f;
import org.atriasoft.ewol.event.EventInput;
import org.atriasoft.ewol.event.EventTime;
import org.atriasoft.ewol.widget.Widget;
import org.atriasoft.gale.backend3d.OpenGL;
import org.atriasoft.gale.backend3d.OpenGL.Flag;
public class EgeScene extends Widget {
/**
* Periodic call to update grapgic display
* @param _event Time generic event
*/
protected static void periodicCall(final EgeScene self, final EventTime event) {
Log.verbose("Periodic call on Entry(" + event + ")");
/*
if (!self.shape.periodicCall(event)) {
//Log.error("end periodic call");
self.periodicConnectionHanble.close();
}
*/
self.markToRedraw();
}
protected Environement env;
/// Periodic call handle to remove it when needed
protected Connection periodicConnectionHanble = new Connection();
/**
* Constructor
*/
public EgeScene() {
this.propertyCanFocus = true;
markToRedraw();
// can not support multiple click...
setMouseLimit(2);
this.env = new Environement();
// default camera....
final Camera mainView = new Camera();
this.env.addCamera("default", mainView);
mainView.setPitch((float) Math.PI * -0.25f);
mainView.setPosition(new Vector3f(0, -5, 5));
}
public void addGenericGird() {
// Simple Gird
final Entity gird = new Entity(this.env);
gird.addComponent(new ComponentPosition(new Transform3D(new Vector3f(0, 0, 0))));
gird.addComponent(new ComponentStaticMesh(MeshGenerator.createGrid(5)));
gird.addComponent(new ComponentRenderColoredStaticMesh(new Uri("DATA", "wireColor.vert", "ege"), new Uri("DATA", "wireColor.frag", "ege")));
this.env.addEntity(gird);
}
@Override
public void calculateMinMaxSize() {
// call main class
super.calculateMinMaxSize();
this.minSize = Vector2f.VALUE_128;
// verify the min max of the min size ...
checkMinSize();
Log.error("min size = " + this.minSize);
}
private float getAspectRatio() {
return this.size.x() / this.size.y();
}
@Override
protected void onDraw() {
//Log.info("==> appl Draw ...");
final Vector2f size = getSize();
// Store openGl context.
OpenGL.push();
// set projection matrix:
final Matrix4f tmpProjection = Matrix4f.createMatrixPerspective(3.14f * 0.5f, getAspectRatio(), 0.1f, 50000);
OpenGL.setMatrix(tmpProjection);
// set the basic openGL view port: (Draw in all the windows...)
OpenGL.setViewPort(new Vector2f(0, 0), size);
// clear background
//final Color bgColor = new Color(0.0f, 1.0f, 0.0f, 1.0f);
//OpenGL.clearColor(bgColor);
// real clear request:
//OpenGL.clear(OpenGL.ClearFlag.clearFlag_colorBuffer);
OpenGL.clear(OpenGL.ClearFlag.clearFlag_depthBuffer);
OpenGL.enable(Flag.flag_depthTest);
this.env.render(20, "default");
OpenGL.disable(Flag.flag_depthTest);
OpenGL.clear(OpenGL.ClearFlag.clearFlag_depthBuffer);
// Restore context of matrix
OpenGL.pop();
}
@Override
public boolean onEventInput(final EventInput event) {
Vector2f relPos = relativePosition(event.pos());
Log.warning("Event on Input ... " + event + " relPos = " + relPos);
return false;
}
@Override
public void onRegenerateDisplay() {
this.env.periodicCall();
markToRedraw();
}
}

View File

@ -0,0 +1,33 @@
package sample.atriasoft.ege.mapFactory;
import org.atriasoft.etk.math.Vector4f;
import org.atriasoft.loader3d.model.Material;
import org.atriasoft.loader3d.resources.ResourceMeshHeightMap;
public class Ground {
int width = 512;
int length = 512;
float[][] heightMap = new float[512][512];
String[][] colorMap = new String[512][512 * 2];
ResourceMeshHeightMap mesh = new ResourceMeshHeightMap();
public Ground() {
for (int yyy = 0; yyy < this.length; yyy++) {
for (int xxx = 0; xxx < this.width; xxx++) {
this.heightMap[yyy][xxx] = 0.0f;
this.colorMap[yyy][xxx] = "grass_1";
}
}
}
public ResourceMeshHeightMap createMesh() {
return this.mesh;
}
public void updateMesh() {
Material mat = new Material();
this.mesh.addMaterial("grass_1", mat);
mat.setAmbientFactor(new Vector4f(1.0f, 0, 0, 1.0f));
}
}

View File

@ -0,0 +1,39 @@
package sample.atriasoft.ege.mapFactory;
public class Log {
private static final String LIBNAME = "mapFactory";
public static void critical(final String data) {
System.out.println("[C] " + Log.LIBNAME + " | " + data);
}
public static void debug(final String data) {
System.out.println("[D] " + Log.LIBNAME + " | " + data);
}
public static void error(final String data) {
System.out.println("[E] " + Log.LIBNAME + " | " + data);
}
public static void info(final String data) {
System.out.println("[I] " + Log.LIBNAME + " | " + data);
}
public static void print(final String data) {
System.out.println(data);
}
public static void todo(final String data) {
System.out.println("[TODO] " + Log.LIBNAME + " | " + data);
}
public static void verbose(final String data) {
System.out.println("[V] " + Log.LIBNAME + " | " + data);
}
public static void warning(final String data) {
System.out.println("[W] " + Log.LIBNAME + " | " + data);
}
private Log() {}
}

View File

@ -0,0 +1,51 @@
package sample.atriasoft.ege.mapFactory;
import org.atriasoft.etk.math.Vector2b;
import org.atriasoft.ewol.widget.Button;
import org.atriasoft.ewol.widget.Sizer;
import org.atriasoft.ewol.widget.Sizer.DisplayMode;
import org.atriasoft.ewol.widget.Windows;
public class MainWindows extends Windows {
public static void eventButtonIncrease(final MainWindows self) {
//Vector2b state = self.testWidget.getPropertyFill();
//self.testWidget.setPropertyFill(state.withY(!state.y()));
if (self.heightButton.getPropertyValue() == "Increase") {
self.heightButton.setPropertyValue("Decrease");
} else {
self.heightButton.setPropertyValue("Increase");
}
}
Button heightButton;
ApplScene scene;
public MainWindows() {
setPropertyTitle("Map Factory (create your dream world)");
Sizer sizerHoryMain = new Sizer(DisplayMode.modeHori);
sizerHoryMain.setPropertyExpand(Vector2b.TRUE_TRUE);
sizerHoryMain.setPropertyFill(Vector2b.TRUE_TRUE);
setSubWidget(sizerHoryMain);
this.scene = new ApplScene();
this.scene.setPropertyExpand(Vector2b.TRUE_TRUE);
this.scene.setPropertyFill(Vector2b.TRUE_TRUE);
sizerHoryMain.subWidgetAdd(this.scene);
Sizer sizerMenu = new Sizer(DisplayMode.modeVert);
sizerMenu.setPropertyExpand(Vector2b.FALSE_TRUE);
sizerMenu.setPropertyLockExpand(Vector2b.TRUE_TRUE);
sizerMenu.setPropertyFill(Vector2b.TRUE_TRUE);
sizerHoryMain.subWidgetAdd(sizerMenu);
this.heightButton = new Button();
this.heightButton.setPropertyValue("Increase");
this.heightButton.setPropertyExpand(Vector2b.TRUE_FALSE);
this.heightButton.setPropertyFill(Vector2b.TRUE_TRUE);
sizerMenu.subWidgetAdd(this.heightButton);
this.heightButton.signalClick.connectAuto(this, MainWindows::eventButtonIncrease);
}
}

View File

@ -0,0 +1,22 @@
package sample.atriasoft.ege.mapFactory;
import org.atriasoft.ege.Ege;
import org.atriasoft.etk.Uri;
import org.atriasoft.ewol.Ewol;
import org.atriasoft.gale.Gale;
public class MapFactoryMain {
public static void main(final String[] args) {
Gale.init();
Ewol.init();
Ege.init();
Uri.setGroup("DATA", "data/");
Uri.setGroup("RES", "res");
//Uri.addLibrary("loxelEngine", MainCollisionTest.class, "testDataLoxelEngine/");
//Uri.addLibrary("plop", Appl.class, "resources/mapFactory/");
Uri.setApplication(Appl.class, "lowPoly");//, "resources/mapFactory/");
Ewol.run(new Appl(), args);
}
private MapFactoryMain() {}
}

View File

@ -0,0 +1,53 @@
package org.atriasoft.arkon;
/*
* From http://www.redblobgames.com/x/1742-webgl-mapgen2/
* Copyright 2017 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*/
/* Generate the biome colormap indexed by elevation -1:+1 and rainfall 0:1 */
class ColorMap {
public static int width = 64;
public static int height = 64;
public int[] colormap() {
int[] pixels = new int[width * height * 4];
for (int y = 0, p = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int e = 2 * x / width - 1, m = y / height;
int r, g, b;
if (x == width / 2 - 1) {
r = 48;
g = 120;
b = 160;
} else if (x == width / 2 - 2) {
r = 48;
g = 100;
b = 150;
} else if (x == width / 2 - 3) {
r = 48;
g = 80;
b = 140;
} else if (e < 0.0) {
r = 48 + 48 * e;
g = 64 + 64 * e;
b = 127 + 127 * e;
} else { // adapted from terrain-from-noise article
m = m * (1 - e); // higher elevation holds less moisture; TODO: should be based on slope, not elevation
r = 210 - 100 * m;
g = 185 - 45 * m;
b = 139 - 45 * m;
r = 255 * e + r * (1 - e);
g = 255 * e + g * (1 - e);
b = 255 * e + b * (1 - e);
}
pixels[p++] = r;
pixels[p++] = g;
pixels[p++] = b;
pixels[p++] = 255;
}
}
return pixels;
}
}

View File

@ -0,0 +1,62 @@
package org.atriasoft.arkon;
/*
* From https://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* Configuration parameters shared by the point precomputation and the
* map generator. Some of these objects are empty because they will be
* filled in by the map generator.
*/
class Config {
public class ConfinElement {
public float initialValue;
public float min;
public float max;
public ConfinElement(float initialValue, float min, float max) {
this.initialValue = initialValue;
this.min = min;
this.max = max;
}
}
public int spacing = 5;
public int mountainSpacing = 35;
public int mountainDensity = 1500;
public ConfinElement mesh_seed = new ConfinElement(187, 1, 1 << 30);
public ConfinElement mesh_island = new ConfinElement(0.5f, 0, 1);
public ConfinElement mesh_noisy_coastlines = new ConfinElement(0.01f, 0, 0.1f);
public ConfinElement mesh_hill_height = new ConfinElement(0.02f, 0, 0.1f);
public ConfinElement mesh_mountain_jagged = new ConfinElement(0, 0, 1);
public ConfinElement mesh_mountain_sharpness = new ConfinElement(10, 9.5f, 12.5f);
public ConfinElement mesh_ocean_depth = new ConfinElement(1.5f, 1, 3);
public ConfinElement biomes_wind_angle_deg = new ConfinElement(0, 0, 360);
public ConfinElement biomes_raininess = new ConfinElement(0.9f, 0, 2);
public ConfinElement biomes_rain_shadow = new ConfinElement(0.5f, 0.1f, 2);
public ConfinElement biomes_evaporation = new ConfinElement(0.5f, 0, 1);
public ConfinElement rivers_lg_min_flow = new ConfinElement(2.7f, -5, 5);
public ConfinElement rivers_lg_river_width = new ConfinElement(-2.7f, -5, 5);
public ConfinElement rivers_flow = new ConfinElement(0.2f, 0, 1);
public ConfinElement render_zoom = new ConfinElement(100 / 480, 100 / 1000, 100 / 50);
public ConfinElement render_x = new ConfinElement(500, 0, 1000);
public ConfinElement render_y = new ConfinElement(500, 0, 1000);
public ConfinElement render_light_angle_deg = new ConfinElement(80, 0, 360);
public ConfinElement render_slope = new ConfinElement(2, 0, 5);
public ConfinElement render_flat = new ConfinElement(2.5f, 0, 5);
public ConfinElement render_ambient = new ConfinElement(0.25f, 0, 1);
public ConfinElement render_overhead = new ConfinElement(30, 0, 60);
public ConfinElement render_tilt_deg = new ConfinElement(0, 0, 90);
public ConfinElement render_rotate_deg = new ConfinElement(0, -180, 180);
public ConfinElement render_mountain_height = new ConfinElement(50, 0, 250);
public ConfinElement render_outline_depth = new ConfinElement(1, 0, 2);
public ConfinElement render_outline_strength = new ConfinElement(15, 0, 30);
public ConfinElement render_outline_threshold = new ConfinElement(0, 0, 100);
public ConfinElement render_outline_coast = new ConfinElement(0, 0, 1);
public ConfinElement render_outline_water = new ConfinElement(10.0f, 0, 20); // things start going wrong when this is high
public ConfinElement render_biome_colors = new ConfinElement(1, 0, 1);
}

View File

@ -0,0 +1,229 @@
package org.atriasoft.arkon;
public class Create {
let Delaunator = require('delaunator'); // ISC licensed
let TriangleMesh = require('./');
function s_next_s(s) { return (s % 3 == 2) ? s-2 : s+1; }
function checkPointInequality({_r_vertex, _triangles, _halfedges}) {
// TODO: check for collinear vertices. Around each red point P if
// there's a point Q and R both connected to it, and the angle PQ and
// the angle PR are 180° apart, then there's collinearity. This would
// indicate an issue with point selection.
}
function checkTriangleInequality({_r_vertex, _triangles, _halfedges}) {
// check for skinny triangles
const badAngleLimit = 30;
let summary = new Array(badAngleLimit).fill(0);
let count = 0;
for (let s = 0; s < _triangles.length; s++) {
let r0 = _triangles[s],
r1 = _triangles[s_next_s(s)],
r2 = _triangles[s_next_s(s_next_s(s))];
let p0 = _r_vertex[r0],
p1 = _r_vertex[r1],
p2 = _r_vertex[r2];
let d0 = [p0[0]-p1[0], p0[1]-p1[1]];
let d2 = [p2[0]-p1[0], p2[1]-p1[1]];
let dotProduct = d0[0] * d2[0] + d0[1] + d2[1];
let angleDegrees = 180 / Math.PI * Math.acos(dotProduct);
if (angleDegrees < badAngleLimit) {
summary[angleDegrees|0]++;
count++;
}
}
// NOTE: a much faster test would be the ratio of the inradius to
// the circumradius, but as I'm generating these offline, I'm not
// worried about speed right now
// TODO: consider adding circumcenters of skinny triangles to the point set
if (count > 0) {
console.log(' bad angles:', summary.join(" "));
}
}
function checkMeshConnectivity({_r_vertex, _triangles, _halfedges}) {
// 1. make sure each side's opposite is back to itself
// 2. make sure region-circulating starting from each side works
let ghost_r = _r_vertex.length - 1, out_s = [];
for (let s0 = 0; s0 < _triangles.length; s0++) {
if (_halfedges[_halfedges[s0]] !== s0) {
console.log(`FAIL _halfedges[_halfedges[${s0}]] !== ${s0}`);
}
let s = s0, count = 0;
out_s.length = 0;
do {
count++; out_s.push(s);
s = s_next_s(_halfedges[s]);
if (count > 100 && _triangles[s0] !== ghost_r) {
console.log(`FAIL to circulate around region with start side=${s0} from region ${_triangles[s0]} to ${_triangles[s_next_s(s0)]}, out_s=${out_s}`);
break;
}
} while (s !== s0);
}
}
/*
* Add vertices evenly along the boundary of the mesh;
* use a slight curve so that the Delaunay triangulation
* doesn't make long thing triangles along the boundary.
* These points also prevent the Poisson disc generator
* from making uneven points near the boundary.
*/
function addBoundaryPoints(spacing, size) {
let N = Math.ceil(size/spacing);
let points = [];
for (let i = 0; i <= N; i++) {
let t = (i + 0.5) / (N + 1);
let w = size * t;
let offset = Math.pow(t - 0.5, 2);
points.push([offset, w], [size-offset, w]);
points.push([w, offset], [w, size-offset]);
}
return points;
}
function addGhostStructure({_r_vertex, _triangles, _halfedges}) {
const numSolidSides = _triangles.length;
const ghost_r = _r_vertex.length;
let numUnpairedSides = 0, firstUnpairedEdge = -1;
let r_unpaired_s = []; // seed to side
for (let s = 0; s < numSolidSides; s++) {
if (_halfedges[s] === -1) {
numUnpairedSides++;
r_unpaired_s[_triangles[s]] = s;
firstUnpairedEdge = s;
}
}
let r_newvertex = _r_vertex.concat([[500, 500]]);
let s_newstart_r = new Int32Array(numSolidSides + 3 * numUnpairedSides);
s_newstart_r.set(_triangles);
let s_newopposite_s = new Int32Array(numSolidSides + 3 * numUnpairedSides);
s_newopposite_s.set(_halfedges);
for (let i = 0, s = firstUnpairedEdge;
i < numUnpairedSides;
i++, s = r_unpaired_s[s_newstart_r[s_next_s(s)]]) {
// Construct a ghost side for s
let ghost_s = numSolidSides + 3 * i;
s_newopposite_s[s] = ghost_s;
s_newopposite_s[ghost_s] = s;
s_newstart_r[ghost_s] = s_newstart_r[s_next_s(s)];
// Construct the rest of the ghost triangle
s_newstart_r[ghost_s + 1] = s_newstart_r[s];
s_newstart_r[ghost_s + 2] = ghost_r;
let k = numSolidSides + (3 * i + 4) % (3 * numUnpairedSides);
s_newopposite_s[ghost_s + 2] = k;
s_newopposite_s[k] = ghost_s + 2;
}
return {
numSolidSides,
_r_vertex: r_newvertex,
_triangles: s_newstart_r,
_halfedges: s_newopposite_s
};
}
/**
* Build a dual mesh from points, with ghost triangles around the exterior.
*
* The builder assumes 0 x < 1000, 0 y < 1000
*
* Options:
* - To have equally spaced points added around the 1000x1000 boundary,
* pass in boundarySpacing > 0 with the spacing value. If using Poisson
* disc points, I recommend 1.5 times the spacing used for Poisson disc.
*
* Phases:
* - Your own set of points
* - Poisson disc points
*
* The mesh generator runs some sanity checks but does not correct the
* generated points.
*
* Examples:
*
* Build a mesh with poisson disc points and a boundary:
*
* new MeshBuilder({boundarySpacing: 150})
* .addPoisson(Poisson, 100)
* .create()
*/
class MeshBuilder {
/** If boundarySpacing > 0 there will be a boundary added around the 1000x1000 area */
public MeshBuilder (float boundarySpacing) {
let boundaryPoints = boundarySpacing > 0 ? addBoundaryPoints(boundarySpacing, 1000) : [];
this.points = boundaryPoints;
this.numBoundaryRegions = boundaryPoints.length;
}
/** Points should be [x, y] */
addPoints(newPoints) {
for (let p of newPoints) {
this.points.push(p);
}
return this;
}
/** Points will be [x, y] */
getNonBoundaryPoints() {
return this.points.slice(this.numBoundaryRegions);
}
/** (used for more advanced mixing of different mesh types) */
clearNonBoundaryPoints() {
this.points.splice(this.numBoundaryRegions, this.points.length);
return this;
}
/** Pass in the constructor from the poisson-disk-sampling module */
addPoisson(Poisson, spacing, random=Math.random) {
let generator = new Poisson({
shape: [1000, 1000],
minDistance: spacing,
}, random);
this.points.forEach(p => generator.addPoint(p));
this.points = generator.fill();
return this;
}
/** Build and return a TriangleMesh */
create(runChecks=false) {
// TODO: use Float32Array instead of this, so that we can
// construct directly from points read in from a file
let delaunator = Delaunator.from(this.points);
let graph = {
_r_vertex: this.points,
_triangles: delaunator.triangles,
_halfedges: delaunator.halfedges
};
if (runChecks) {
checkPointInequality(graph);
checkTriangleInequality(graph);
}
graph = addGhostStructure(graph);
graph.numBoundaryRegions = this.numBoundaryRegions;
if (runChecks) {
checkMeshConnectivity(graph);
}
return new TriangleMesh(graph);
}
}
}

View File

@ -0,0 +1,369 @@
package org.atriasoft.arkon;
// @ts-check
/*
* From https://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* This module has the procedural map generation algorithms (elevations, rivers)
*/
import SimplexNoise from'simplex-noise';
import FlatQueue from'flatqueue';import{makeRandFloat}from'@redblobgames/prng';
const mountain={slope:20,density:1500,};
/**
* @typedef { import("./types").Mesh } Mesh
*/
class Map {
Mesh mesh;
float[] peaks_t;
long seed = -1;
int spacing;
float mountainJaggedness = Float.NEGATIVE_INFINITY;
float windAngleDeg = Float.POSITIVE_INFINITY;
float[] t_elevation;
float[] r_elevation;
float[] r_humidity;
float[] t_moisture;
float[] r_rainfall;
int[] t_downslope_s;
int[] order_t;
float[] t_flow;
float[] s_flow;
int[] wind_order_r;
float[] r_wind_sort;
float[] t_mountain_distance;
/**
* @param {Mesh} mesh
* @param {number[]} peaks_t - array of triangle indices for mountain peaks
* @param {any} param - global parameters
*/
public Map(Mesh mesh, float[] peaks_t, Config param) {
this.mesh = mesh;
this.peaks_t = peaks_t;
this.seed = -1;
this.spacing = param.spacing;
this.t_elevation = new float[mesh.numTriangles];
this.r_elevation = new float[mesh.numRegions];
this.r_humidity = new float[mesh.numRegions];
this.t_moisture = new float[mesh.numTriangles];
this.r_rainfall = new float[mesh.numRegions];
this.t_downslope_s = new int[mesh.numTriangles];
this.order_t = new int[mesh.numTriangles];
this.t_flow = new float[mesh.numTriangles];
this.s_flow = new float[mesh.numSides];
this.wind_order_r = new int[mesh.numRegions];
this.r_wind_sort = new float[mesh.numRegions];
this.t_mountain_distance = new float[mesh.numTriangles];
}
/**
* Mountains are peaks surrounded by steep dropoffs. In the point
* selection process (mesh.js) we pick the mountain peak locations.
* Here we calculate a distance field from peaks to all other points.
*
* We'll use breadth first search for this because it's simple and
* fast. Dijkstra's Algorithm would produce a more accurate distance
* field, but we only need an approximation. For increased
* interestingness, we add some randomness to the distance field.
*
* @param {Mesh} mesh
* @param {number[]} seeds_t - a list of triangles with mountain peaks
* @param {number} spacing - the global param.spacing value
* @param {number} jaggedness - how much randomness to mix into the distances
* @param {function(): number} randFloat - random number generator
* @param {Float32Array} t_distance - the distance field indexed by t, OUTPUT
*/
public static void calculateMountainDistance(mesh,seeds_t,spacing,jaggedness,randFloat,t_distance){t_distance.fill(-1);let queue_t=seeds_t.concat([]);for(let i=0;i<queue_t.length;i++){let current_t=queue_t[i];for(let j=0;j<3;j++){let s=3*current_t+j;let neighbor_t=mesh.s_outer_t(s);if(t_distance[neighbor_t]==-1){let increment=spacing*(1+jaggedness*(randFloat()-randFloat()));t_distance[neighbor_t]=t_distance[current_t]+increment;queue_t.push(neighbor_t);}}}}
/**
* Save noise values in arrays.
*
* @param {function(): number} randFloat - random number generator
* @param {Mesh} mesh
*/
public static void precalculateNoise(randFloat,mesh){const noise=new SimplexNoise(randFloat);let{numTriangles}=mesh;let t_noise0=new float[numTriangles),t_noise1=new float[numTriangles),t_noise2=new float[numTriangles),t_noise3=new float[numTriangles),t_noise4=new float[numTriangles),t_noise5=new float[numTriangles),t_noise6=new float[numTriangles);for(let t=0;t<numTriangles;t++){let nx=(mesh.t_x(t)-500)/500,ny=(mesh.t_y(t)-500)/500;t_noise0[t]=noise.noise2D(nx,ny);t_noise1[t]=noise.noise2D(2*nx+5,2*ny+5);t_noise2[t]=noise.noise2D(4*nx+7,4*ny+7);t_noise3[t]=noise.noise2D(8*nx+9,8*ny+9);t_noise4[t]=noise.noise2D(16*nx+15,16*ny+15);t_noise5[t]=noise.noise2D(32*nx+31,32*ny+31);t_noise6[t]=noise.noise2D(64*nx+67,64*ny+67);}return{t_noise0,t_noise1,t_noise2,t_noise3,t_noise4,t_noise5,t_noise6};}
void assignTriangleElevation(elevationParam, constraints) {
let {mesh, t_elevation, t_mountain_distance, precomputed} = this;
let {numTriangles, numSolidTriangles} = mesh;
}
// Assign elevations to triangles TODO: separate message,
// store the interpolated values in an array, or maybe for
// each painted cell store which triangle elevations have to
// be updated, so that we don't have to recalculate the entire
// map's interpolated values each time (involves copying 50k
// floats instead of 16k floats), or maybe send a message with
// the bounding box of the painted area
float constraintAt(float x, float y) {
// https://en.wikipedia.org/wiki/Bilinear_interpolation
const C = constraints.constraints, size = constraints.size;
x *= size; y *= size;
let xInt = Math.floor(x),
yInt = Math.floor(y),
xFrac = x - xInt,
yFrac = y - yInt;
if (0 <= xInt && xInt+1 < size && 0 <= yInt && yInt+1 < size) {
let p = size * yInt + xInt;
let e00 = C[p],
e01 = C[p + 1],
e10 = C[p + size],
e11 = C[p + size + 1];
return ((e00 * (1 - xFrac) + e01 * xFrac) * (1 - yFrac)
+ (e10 * (1 - xFrac) + e11 * xFrac) * yFrac);
} else {
return -1.0;
}
}
for (let t = 0; t < numSolidTriangles; t++) {
let e = constraintAt(mesh.t_x(t)/1000, mesh.t_y(t)/1000);
// TODO: e*e*e*e seems too steep for this, as I want this
// to apply mostly at the original coastlines and not
// elsewhere
t_elevation[t] = e + elevationParam.noisy_coastlines * (1 - e*e*e*e) * (precomputed.t_noise4[t] + precomputed.t_noise5[t]/2 + precomputed.t_noise6[t]/4);
}
// For land triangles, mix hill and mountain terrain together
const mountain_slope = mountain.slope,
mountain_sharpness = Math.pow(2, elevationParam.mountain_sharpness),
{t_noise0, t_noise1, t_noise2, t_noise4} = precomputed;
for (let t = 0; t < numTriangles; t++) {
let e = t_elevation[t];
if (e > 0) {
/* Mix two sources of elevation:
*
* 1. eh: Hills are formed using simplex noise. These
* are very low amplitude, and the main purpose is
* to make the rivers meander. The amplitude
* doesn't make much difference in the river
* meandering. These hills shouldn't be
* particularly visible so I've kept the amplitude
* low.
*
* 2. em: Mountains are formed using something similar to
* worley noise. These form distinct peaks, with
* varying distance between them.
*/
// TODO: precompute eh, em per triangle
let noisiness = 1.0 - 0.5 * (1 + t_noise0[t]);
let eh = (1 + noisiness * t_noise4[t] + (1 - noisiness) * t_noise2[t]) * elevationParam.hill_height;
if (eh < 0.01) { eh = 0.01; }
let em = 1 - mountain_slope/mountain_sharpness * t_mountain_distance[t];
if (em < 0.01) { em = 0.01; }
let weight = e * e;
e = (1-weight) * eh + weight * em;
} else {
/* Add noise to make it more interesting. */
e *= elevationParam.ocean_depth + t_noise1[t];
}
if (e < -1.0) { e = -1.0; }
if (e > +1.0) { e = +1.0; }
t_elevation[t] = e;
}
}
void assignRegionElevation(elevationParam, constraints) {
let {mesh, t_elevation, r_elevation} = this;
let {numRegions, _r_in_s, _halfedges} = mesh;
for (let r = 0; r < numRegions; r++) {
let count = 0, e = 0, water = false;
const s0 = _r_in_s[r];
let incoming = s0;
do {
let t = (incoming/3) | 0;
e += t_elevation[t];
water = water || t_elevation[t] < 0.0;
let outgoing = mesh.s_next_s(incoming);
incoming = _halfedges[outgoing];
count++;
} while (incoming !== s0);
e /= count;
if (water && e >= 0) { e = -0.001; }
r_elevation[r] = e;
}
}
void assignElevation(elevationParam, constraints) {
if (this.seed !== elevationParam.seed || this.mountainJaggedness !== elevationParam.mountain_jagged) {
this.mountainJaggedness = elevationParam.mountain_jagged;
calculateMountainDistance(
this.mesh, this.peaks_t, this.spacing,
this.mountainJaggedness, makeRandFloat(elevationParam.seed),
this.t_mountain_distance
);
}
if (this.seed !== elevationParam.seed) {
// TODO: function should reuse existing arrays
this.seed = elevationParam.seed;
this.precomputed = precalculateNoise(makeRandFloat(elevationParam.seed), this.mesh);
}
this.assignTriangleElevation(elevationParam, constraints);
this.assignRegionElevation(elevationParam);
}
void assignRainfall(biomesParam) {
const {mesh, wind_order_r, r_wind_sort, r_humidity, r_rainfall, r_elevation} = this;
const {numRegions, _r_in_s, _halfedges} = mesh;
if (biomesParam.wind_angle_deg != this.windAngleDeg) {
this.windAngleDeg = biomesParam.wind_angle_deg;
const windAngleRad = Math.PI / 180 * this.windAngleDeg;
const windAngleVec = [Math.cos(windAngleRad), Math.sin(windAngleRad)];
for (let r = 0; r < numRegions; r++) {
wind_order_r[r] = r;
r_wind_sort[r] = mesh.r_x(r) * windAngleVec[0] + mesh.r_y(r) * windAngleVec[1];
}
wind_order_r.sort((r1, r2) => r_wind_sort[r1] - r_wind_sort[r2]);
}
for (let r of wind_order_r) {
let count = 0, sum = 0.0;
let s0 = _r_in_s[r], incoming = s0;
do {
let neighbor_r = mesh.s_begin_r(incoming);
if (r_wind_sort[neighbor_r] < r_wind_sort[r]) {
count++;
sum += r_humidity[neighbor_r];
}
let outgoing = mesh.s_next_s(incoming);
incoming = _halfedges[outgoing];
} while (incoming !== s0);
let humidity = 0.0, rainfall = 0.0;
if (count > 0) {
humidity = sum / count;
rainfall += biomesParam.raininess * humidity;
}
if (mesh.r_boundary(r)) {
humidity = 1.0;
}
if (r_elevation[r] < 0.0) {
let evaporation = biomesParam.evaporation * -r_elevation[r];
humidity += evaporation;
}
if (humidity > 1.0 - r_elevation[r]) {
let orographicRainfall = biomesParam.rain_shadow * (humidity - (1.0 - r_elevation[r]));
rainfall += biomesParam.raininess * orographicRainfall;
humidity -= orographicRainfall;
}
r_rainfall[r] = rainfall;
r_humidity[r] = humidity;
}
}
void assignRivers(riversParam) {
let {mesh, t_moisture, r_rainfall, t_elevation, t_downslope_s, order_t, t_flow, s_flow} = this;
assignDownslope(mesh, t_elevation, t_downslope_s, order_t);
assignMoisture(mesh, r_rainfall, t_moisture);
assignFlow(mesh, riversParam, order_t, t_elevation, t_moisture, t_downslope_s, t_flow, s_flow);
}
}
/**
* Use prioritized graph exploration to assign river flow direction
*
* @param {Mesh} mesh
* @param {Float32Array} t_elevation - elevation per triangle
* @param {Int32Array} t_downslope_s - OUT parameter - the side each triangle flows out of
* @param {Int32Array} order_t - OUT parameter - pre-order in which the graph was traversed,
* so roots of the tree always get visited before leaves; use reverse to visit leaves before roots
*/
let queue = new FlatQueue();
void assignDownslope(mesh, t_elevation, /* out */ t_downslope_s, /* out */ order_t) {
/* Use a priority queue, starting with the ocean triangles and
* moving upwards using elevation as the priority, to visit all
* the land triangles */
let {numTriangles} = mesh,
queue_in = 0;
t_downslope_s.fill(-999);
/* Part 1: non-shallow ocean triangles get downslope assigned to the lowest neighbor */
for (let t = 0; t < numTriangles; t++) {
if (t_elevation[t] < -0.1) {
let best_s = -1, best_e = t_elevation[t];
for (let j = 0; j < 3; j++) {
let s = 3 * t + j,
e = t_elevation[mesh.s_outer_t(s)];
if (e < best_e) {
best_e = e;
best_s = s;
}
}
order_t[queue_in++] = t;
t_downslope_s[t] = best_s;
queue.push(t, t_elevation[t]);
}
}
/* Part 2: land triangles get visited in elevation priority */
for (let queue_out = 0; queue_out < numTriangles; queue_out++) {
let current_t = queue.pop();
for (let j = 0; j < 3; j++) {
let s = 3 * current_t + j;
let neighbor_t = mesh.s_outer_t(s); // uphill from current_t
if (t_downslope_s[neighbor_t] === -999) {
t_downslope_s[neighbor_t] = mesh.s_opposite_s(s);
order_t[queue_in++] = neighbor_t;
queue.push(neighbor_t, t_elevation[neighbor_t]);
}
}
}
}
/**
* @param {Mesh} mesh
* @param {Float32Array} r_rainfall - per region
* @param {Float32Array} t_moisture - OUT parameter - per triangle
*/
void assignMoisture(mesh, r_rainfall, /* out */ t_moisture) {
const {numTriangles} = mesh;
for (let t = 0; t < numTriangles; t++) {
let moisture = 0.0;
for (let i = 0; i < 3; i++) {
let s = 3 * t + i,
r = mesh.s_begin_r(s);
moisture += r_rainfall[r] / 3;
}
t_moisture[t] = moisture;
}
}
/**
* @param {Int32Array} order_t
* @param {any} riversParam
* @param {Float32Array} t_elevation
* @param {Float32Array} t_moisture
* @param {Int32Array} t_downslope_s
* @param {Float32Array} t_flow
*/
void assignFlow(mesh, riversParam, order_t, t_elevation, t_moisture, t_downslope_s, /* out */ t_flow, /* out */ s_flow) {
let {numTriangles, _halfedges} = mesh;
s_flow.fill(0);
for (let t = 0; t < numTriangles; t++) {
if (t_elevation[t] >= 0.0) {
t_flow[t] = riversParam.flow * t_moisture[t] * t_moisture[t];
} else {
t_flow[t] = 0;
}
}
for (let i = order_t.length-1; i >= 0; i--) {
let tributary_t = order_t[i];
let flow_s = t_downslope_s[tributary_t];
let trunk_t = (_halfedges[flow_s] / 3) | 0;
if (flow_s >= 0) {
t_flow[trunk_t] += t_flow[tributary_t];
s_flow[flow_s] += t_flow[tributary_t]; // TODO: s_flow[t_downslope_s[t]] === t_flow[t]; redundant?
if (t_elevation[trunk_t] > t_elevation[tributary_t] && t_elevation[tributary_t] >= 0.0) {
t_elevation[trunk_t] = t_elevation[tributary_t];
}
}
}
}

View File

@ -0,0 +1,9 @@
package org.atriasoft.arkon;
/* I use the TriangleMesh from my dual mesh library, but I add fields to it,
* so I'm declaring that here for type checking purposes. */
//#import TriangleMesh from'@redblobgames/dual-mesh';
class Mesh extends TriangleMesh {
float[] s_length; /* indexed on s */
}

View File

@ -0,0 +1,553 @@
package org.atriasoft.arkon;
/*
* From http://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* This module uses webgl+regl to render the generated maps
*/
import colormap from './colormap';
import Geometry from './geometry';
import createREGL from 'regl';
const regl = createREGL({
canvas: "#mapgen4",
extensions: ['OES_element_index_uint']
});
const river_texturemap = regl.texture({data: Geometry.createRiverBitmap(), mipmap: 'nice', min: 'mipmap', mag: 'linear', premultiplyAlpha: true});
const fbo_texture_size = 2048;
const fbo_land_texture = regl.texture({width: fbo_texture_size, height: fbo_texture_size});
const fbo_land = regl.framebuffer({color: [fbo_land_texture]});
const fbo_depth_texture = regl.texture({width: fbo_texture_size, height: fbo_texture_size});
const fbo_z = regl.framebuffer({color: [fbo_depth_texture]});
const fbo_river_texture = regl.texture({width: fbo_texture_size, height: fbo_texture_size});
const fbo_river = regl.framebuffer({color: [fbo_river_texture]});
const fbo_final_texture = regl.texture({width: fbo_texture_size, height: fbo_texture_size, min: 'linear', mag: 'linear'});
const fbo_final = regl.framebuffer({color: [fbo_final_texture]});
/* draw rivers to a texture, which will be draped on the map surface */
const drawRivers = regl({
frag: `
precision mediump float;
uniform sampler2D u_rivertexturemap;
varying vec2 v_uv;
const vec3 blue = vec3(0.2, 0.5, 0.7);
void main() {
vec4 color = texture2D(u_rivertexturemap, v_uv);
gl_FragColor = vec4(blue * color.a, color.a);
// gl_FragColor = color;
}`,
vert: `
precision highp float;
uniform mat4 u_projection;
attribute vec4 a_xyuv;
varying vec2 v_uv;
void main() {
v_uv = a_xyuv.ba;
gl_Position = vec4(u_projection * vec4(a_xyuv.xy, 0, 1));
}`,
uniforms: {
u_projection: regl.prop('u_projection'),
u_rivertexturemap: river_texturemap,
},
framebuffer: fbo_river,
blend: {
enable: true,
func: {src:'one', dst:'one minus src alpha'},
equation: {
rgb: 'add',
alpha: 'add'
},
color: [0, 0, 0, 0]
},
depth: {
enable: false,
},
count: regl.prop('count'),
attributes: {
a_xyuv: regl.prop('a_xyuv'),
},
});
/* write 16-bit elevation to a texture's G,R channels; the B,A channels are empty */
const drawLand = regl({
frag: `
precision highp float;
uniform sampler2D u_water;
uniform float u_outline_water;
varying float v_e;
varying vec2 v_xy;
void main() {
float e = 0.5 * (1.0 + v_e);
float river = texture2D(u_water, v_xy).a;
if (e >= 0.5) {
float bump = u_outline_water / 256.0;
float L1 = e + bump;
float L2 = (e - 0.5) * (bump * 100.0) + 0.5;
// TODO: simplify equation
e = min(L1, mix(L1, L2, river));
}
gl_FragColor = vec4(fract(256.0*e), e, 0, 1);
// NOTE: it should be using the floor instead of rounding, but
// rounding produces a nice looking artifact, so I'll keep that
// until I can produce the artifact properly (e.g. bug feature).
// Using linear filtering on the texture also smooths out the artifacts.
// gl_FragColor = vec4(fract(256.0*e), floor(256.0*e)/256.0, 0, 1);
// NOTE: need to use GL_NEAREST filtering for this texture because
// blending R,G channels independently isn't going to give the right answer
}`,
vert: `
precision highp float;
uniform mat4 u_projection;
attribute vec2 a_xy;
attribute vec2 a_em; // NOTE: moisture channel unused
varying float v_e;
varying vec2 v_xy;
void main() {
vec4 pos = vec4(u_projection * vec4(a_xy, 0, 1));
v_xy = (1.0 + pos.xy) * 0.5;
v_e = a_em.x;
gl_Position = pos;
}`,
uniforms: {
u_projection: regl.prop('u_projection'),
u_water: regl.prop('u_water'),
u_outline_water: regl.prop('u_outline_water'),
u_m: regl.prop('u_m'),
},
framebuffer: fbo_land,
depth: {
enable: false,
},
elements: regl.prop('elements'),
attributes: {
a_xy: regl.prop('a_xy'),
a_em: regl.prop('a_em'),
},
});
/* using the same perspective as the final output, write the depth
to a texture, G,R channels; used for outline shader */
const drawDepth = regl({
frag: `
precision highp float;
varying float v_z;
void main() {
gl_FragColor = vec4(fract(256.0*v_z), floor(256.0*v_z)/256.0, 0, 1);
}`,
vert: `
precision highp float;
uniform mat4 u_projection;
attribute vec2 a_xy;
attribute vec2 a_em;
varying float v_z;
void main() {
vec4 pos = vec4(u_projection * vec4(a_xy, max(0.0, a_em.x), 1));
v_z = a_em.x;
gl_Position = pos;
}`,
framebuffer: fbo_z,
elements: regl.prop('elements'),
attributes: {
a_xy: regl.prop('a_xy'),
a_em: regl.prop('a_em'),
},
uniforms: {
u_projection: regl.prop('u_projection'),
},
});
/* draw the final image by draping the biome colors over the geometry;
note that u_depth and u_mapdata are both encoded with G,R channels
for 16 bits */
const drawDrape = regl({
frag: `
precision highp float;
uniform sampler2D u_colormap;
uniform sampler2D u_mapdata;
uniform sampler2D u_water;
uniform sampler2D u_depth;
uniform vec2 u_light_angle;
uniform float u_inverse_texture_size,
u_slope, u_flat,
u_ambient, u_overhead,
u_outline_strength, u_outline_coast, u_outline_water,
u_outline_depth, u_outline_threshold,
u_biome_colors;
varying vec2 v_uv, v_xy, v_em;
const vec2 _decipher = vec2(1.0/256.0, 1);
float decipher(vec4 v) {
return dot(_decipher, v.xy);
}
const vec3 neutral_land_biome = vec3(0.9, 0.8, 0.7);
const vec3 neutral_water_biome = 0.8 * neutral_land_biome;
void main() {
vec2 sample_offset = vec2(0.5*u_inverse_texture_size, 0.5*u_inverse_texture_size);
vec2 pos = v_uv + sample_offset;
vec2 dx = vec2(u_inverse_texture_size, 0),
dy = vec2(0, u_inverse_texture_size);
float zE = decipher(texture2D(u_mapdata, pos + dx));
float zN = decipher(texture2D(u_mapdata, pos - dy));
float zW = decipher(texture2D(u_mapdata, pos - dx));
float zS = decipher(texture2D(u_mapdata, pos + dy));
vec3 slope_vector = normalize(vec3(zS-zN, zE-zW, u_overhead*2.0*u_inverse_texture_size));
vec3 light_vector = normalize(vec3(u_light_angle, mix(u_slope, u_flat, slope_vector.z)));
float light = u_ambient + max(0.0, dot(light_vector, slope_vector));
vec2 em = texture2D(u_mapdata, pos).yz;
em.y = v_em.y;
vec3 neutral_biome_color = neutral_land_biome;
vec4 water_color = texture2D(u_water, pos);
if (em.x >= 0.5) { em.x -= u_outline_water / 256.0 * (1.0 - water_color.a); }
vec3 biome_color = texture2D(u_colormap, em).rgb;
if (em.x < 0.5) { water_color.a = 0.0; neutral_biome_color = neutral_water_biome; } // don't draw rivers in the ocean
water_color = mix(vec4(neutral_water_biome * (1.2 - water_color.a), water_color.a), water_color, u_biome_colors);
biome_color = mix(neutral_biome_color, biome_color, u_biome_colors);
// if (fract(em.x * 10.0) < 10.0 * fwidth(em.x)) { biome_color = vec3(0,0,0); } // contour lines
// TODO: add noise texture based on biome
// TODO: once I remove the elevation rounding artifact I can simplify
// this by taking the max first and then deciphering
float depth0 = decipher(texture2D(u_depth, v_xy)),
depth1 = max(max(decipher(texture2D(u_depth, v_xy + u_outline_depth*(-dy-dx))),
decipher(texture2D(u_depth, v_xy + u_outline_depth*(-dy+dx)))),
decipher(texture2D(u_depth, v_xy + u_outline_depth*(-dy)))),
depth2 = max(max(decipher(texture2D(u_depth, v_xy + u_outline_depth*(dy-dx))),
decipher(texture2D(u_depth, v_xy + u_outline_depth*(dy+dx)))),
decipher(texture2D(u_depth, v_xy + u_outline_depth*(dy))));
float outline = 1.0 + u_outline_strength * (max(u_outline_threshold, depth1-depth0) - u_outline_threshold);
// Add coast outline, but avoid it if there's a river nearby
float neighboring_river = max(
max(
texture2D(u_water, pos + u_outline_depth * dx).a,
texture2D(u_water, pos - u_outline_depth * dx).a
),
max(
texture2D(u_water, pos + u_outline_depth * dy).a,
texture2D(u_water, pos - u_outline_depth * dy).a
)
);
if (em.x <= 0.5 && max(depth1, depth2) > 1.0/256.0 && neighboring_river <= 0.2) { outline += u_outline_coast * 256.0 * (max(depth1, depth2) - 2.0*(em.x - 0.5)); }
gl_FragColor = vec4(mix(biome_color, water_color.rgb, water_color.a) * light / outline, 1);
}`,
vert: `
precision highp float;
uniform mat4 u_projection;
attribute vec2 a_xy;
attribute vec2 a_em;
varying vec2 v_em, v_uv, v_xy;
varying float v_e, v_m;
void main() {
vec4 pos = vec4(u_projection * vec4(a_xy, max(0.0, a_em.x), 1));
v_uv = a_xy / 1000.0;
v_em = a_em;
v_xy = (1.0 + pos.xy) * 0.5;
gl_Position = pos;
}`,
framebuffer: fbo_final,
elements: regl.prop('elements'),
attributes: {
a_xy: regl.prop('a_xy'),
a_em: regl.prop('a_em'),
},
uniforms: {
u_projection: regl.prop('u_projection'),
u_depth: regl.prop('u_depth'),
u_colormap: regl.texture({width: colormap.width, height: colormap.height, data: colormap.data, wrapS: 'clamp', wrapT: 'clamp'}),
u_mapdata: () => fbo_land_texture,
u_water: regl.prop('u_water'),
u_inverse_texture_size: 1.5 / fbo_texture_size,
u_light_angle: regl.prop('u_light_angle'),
u_slope: regl.prop('u_slope'),
u_flat: regl.prop('u_flat'),
u_ambient: regl.prop('u_ambient'),
u_overhead: regl.prop('u_overhead'),
u_outline_depth: regl.prop('u_outline_depth'),
u_outline_coast: regl.prop('u_outline_coast'),
u_outline_water: regl.prop('u_outline_water'),
u_outline_strength: regl.prop('u_outline_strength'),
u_outline_threshold: regl.prop('u_outline_threshold'),
u_biome_colors: regl.prop('u_biome_colors'),
},
});
/* draw the high resolution final output to the screen, smoothed and resized */
const drawFinal = regl({
frag: `
precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_offset;
varying vec2 v_uv;
void main() {
gl_FragColor = texture2D(u_texture, v_uv + u_offset);
}`,
vert: `
precision highp float;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(2.0 * v_uv - 1.0, 0.0, 1.0);
}`,
uniforms: {
u_texture: fbo_final_texture,
u_offset: regl.prop('u_offset'),
},
depth: {
enable: false,
},
count: 3,
attributes: {
a_uv: [-2, 0, 0, -2, 2, 2]
},
});
class Renderer {
constructor (mesh) {
this.resizeCanvas();
this.topdown = mat4.create();
mat4.translate(this.topdown, this.topdown, [-1, -1, 0, 0]);
mat4.scale(this.topdown, this.topdown, [1/500, 1/500, 1, 1]);
this.projection = mat4.create();
this.inverse_projection = mat4.create();
this.a_quad_xy = new Float32Array(2 * (mesh.numRegions + mesh.numTriangles));
this.a_quad_em = new Float32Array(2 * (mesh.numRegions + mesh.numTriangles));
this.quad_elements = new Int32Array(3 * mesh.numSolidSides);
/* NOTE: The maximum number of river triangles will be when
* there's a single binary tree that has every node filled.
* Each of the N/2 leaves will produce 1 output triangle and
* each of the N/2 nodes will produce 2 triangles. On average
* there will be 1.5 output triangles per input triangle. */
this.a_river_xyuv = new Float32Array(1.5 * 3 * 4 * mesh.numSolidTriangles);
this.numRiverTriangles = 0;
Geometry.setMeshGeometry(mesh, this.a_quad_xy);
this.buffer_quad_xy = regl.buffer({
usage: 'static',
type: 'float',
data: this.a_quad_xy,
});
this.buffer_quad_em = regl.buffer({
usage: 'dynamic',
type: 'float',
length: 4 * this.a_quad_em.length,
});
this.buffer_quad_elements = regl.elements({
primitive: 'triangles',
usage: 'dynamic',
type: 'uint32',
length: 4 * this.quad_elements.length,
count: this.quad_elements.length,
});
this.buffer_river_xyuv = regl.buffer({
usage: 'dynamic',
type: 'float',
length: 4 * this.a_river_xyuv.length,
});
this.screenshotCanvas = document.createElement('canvas');
this.screenshotCanvas.width = fbo_texture_size;
this.screenshotCanvas.height = fbo_texture_size;
this.screenshotCallback = null;
this.renderParam = undefined;
this.startDrawingLoop();
}
/**
* @param {[number, number]} coords - screen coordinates 0 x 1, 0 y 1
* @returns {[number, number]} - world coords 0 x 1000, 0 y 1000
*/
screenToWorld(coords) {
/* convert from screen 2d (inverted y) to 4d for matrix multiply */
let glCoords = vec4.fromValues(
coords[0] * 2 - 1,
1 - coords[1] * 2,
/* TODO: z should be 0 only when tilt_deg is 0;
* need to figure out the proper z value here */
0,
1
);
/* it returns vec4 but we only need vec2; they're compatible */
return vec4.transformMat4([], glCoords, this.inverse_projection);
}
/* Update the buffers with the latest map data */
updateMap() {
this.buffer_quad_em.subdata(this.a_quad_em);
this.buffer_quad_elements.subdata(this.quad_elements);
this.buffer_river_xyuv.subdata(this.a_river_xyuv.subarray(0, 4 * 3 * this.numRiverTriangles));
}
/* Allow drawing at a different resolution than the internal texture size */
resizeCanvas() {
let canvas = /** @type{HTMLCanvasElement} */(document.getElementById('mapgen4'));
let size = canvas.clientWidth;
size = 2048; /* could be smaller to increase performance */
if (canvas.width !== size || canvas.height !== size) {
console.log(`Resizing canvas from ${canvas.width}x${canvas.height} to ${size}x${size}`);
canvas.width = canvas.height = size;
regl.poll();
}
}
startDrawingLoop() {
/* Only draw when render parameters have been passed in;
* otherwise skip the render and wait for the next tick */
regl.frame(context => {
const renderParam = this.renderParam;
if (!renderParam) { return; }
this.renderParam = undefined;
if (this.numRiverTriangles > 0) {
drawRivers({
count: 3 * this.numRiverTriangles,
a_xyuv: this.buffer_river_xyuv,
u_projection: this.topdown,
});
}
drawLand({
elements: this.buffer_quad_elements,
a_xy: this.buffer_quad_xy,
a_em: this.buffer_quad_em,
u_projection: this.topdown,
u_water: fbo_river_texture,
u_outline_water: renderParam.outline_water,
});
/* Standard rotation for orthographic view */
mat4.identity(this.projection);
mat4.rotateX(this.projection, this.projection, (180 + renderParam.tilt_deg) * Math.PI/180);
mat4.rotateZ(this.projection, this.projection, renderParam.rotate_deg * Math.PI/180);
/* Top-down oblique copies column 2 (y input) to row 3 (z
* output). Typical matrix libraries such as glm's mat4 or
* Unity's Matrix4x4 or Unreal's FMatrix don't have this
* this.projection built-in. For mapgen4 I merge orthographic
* (which will *move* part of y-input to z-output) and
* top-down oblique (which will *copy* y-input to z-output).
* <https://en.wikipedia.org/wiki/Oblique_projection> */
this.projection[9] = 1;
/* Scale and translate works on the hybrid this.projection */
mat4.scale(this.projection, this.projection, [renderParam.zoom/100, renderParam.zoom/100, renderParam.mountain_height * renderParam.zoom/100, 1]);
mat4.translate(this.projection, this.projection, [-renderParam.x, -renderParam.y, 0, 0]);
/* Keep track of the inverse matrix for mapping mouse to world coordinates */
mat4.invert(this.inverse_projection, this.projection);
if (renderParam.outline_depth > 0) {
drawDepth({
elements: this.buffer_quad_elements,
a_xy: this.buffer_quad_xy,
a_em: this.buffer_quad_em,
u_projection: this.projection
});
}
drawDrape({
elements: this.buffer_quad_elements,
a_xy: this.buffer_quad_xy,
a_em: this.buffer_quad_em,
u_water: fbo_river_texture,
u_depth: fbo_depth_texture,
u_projection: this.projection,
u_light_angle: [
Math.cos(Math.PI/180 * (renderParam.light_angle_deg + renderParam.rotate_deg)),
Math.sin(Math.PI/180 * (renderParam.light_angle_deg + renderParam.rotate_deg)),
],
u_slope: renderParam.slope,
u_flat: renderParam.flat,
u_ambient: renderParam.ambient,
u_overhead: renderParam.overhead,
u_outline_depth: renderParam.outline_depth * 5 * renderParam.zoom,
u_outline_coast: renderParam.outline_coast,
u_outline_water: renderParam.outline_water,
u_outline_strength: renderParam.outline_strength,
u_outline_threshold: renderParam.outline_threshold / 1000,
u_biome_colors: renderParam.biome_colors,
});
drawFinal({
u_offset: [0.5 / fbo_texture_size, 0.5 / fbo_texture_size],
});
if (this.screenshotCallback) {
// TODO: regl says I need to use preserveDrawingBuffer
const gl = regl._gl;
const ctx = this.screenshotCanvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, fbo_texture_size, fbo_texture_size);
const bytesPerRow = 4 * fbo_texture_size;
const buffer = new Uint8Array(bytesPerRow * fbo_texture_size);
gl.readPixels(0, 0, fbo_texture_size, fbo_texture_size, gl.RGBA, gl.UNSIGNED_BYTE, buffer);
// Flip row order from WebGL to Canvas
for (let y = 0; y < fbo_texture_size; y++) {
const rowBuffer = new Uint8Array(buffer.buffer, y * bytesPerRow, bytesPerRow);
imageData.data.set(rowBuffer, (fbo_texture_size-y-1) * bytesPerRow);
}
ctx.putImageData(imageData, 0, 0);
this.screenshotCallback();
this.screenshotCallback = null;
}
// I don't have to clear fbo_em because it doesn't have depth
// and will be redrawn every frame. I do have to clear
// fbo_river because even though it doesn't have depth, it
// doesn't draw all triangles.
fbo_river.use(() => {
regl.clear({color: [0, 0, 0, 0]});
});
fbo_z.use(() => {
regl.clear({color: [0, 0, 0, 1], depth: 1});
});
fbo_final.use(() => {
regl.clear({color: [0.2, 0.3, 0.5, 1], depth: 1});
});
});
}
updateView(renderParam) {
this.renderParam = renderParam;
}
}
export default Renderer;

View File

@ -0,0 +1,5 @@
package org.atriasoft.arkon;
public class TriangleMesh {
}

View File

@ -0,0 +1,127 @@
/*
* From https://www.redblobgames.com/x/1845-draggable/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*/
'use strict';
class Draggable {
/** Props should be an object:
*
* el: HTMLElement - required - the element where the drag handlers are attached
* reference: HTMLElement - defaults to el - the element where positions are calculated
*
* The reference element should not move during the drag operation.
*
* start(event) - optional - called when drag operation starts
* drag(event) - optional - called each time mouse/finger moves
* end(event) - optional - called when the drag operation ends
*
* event.raw will have the raw (native) event
*
* TODO: document (coords, uninstall, mouse_button, touch_identifier)
*/
constructor(props) {
this.reference = props.el;
Object.assign(this, props);
let mouse_cleanup = () => null;
const mouseDown = (event) => {
if (event.button != 0) { return; /* don't trap right click */ }
mouse_cleanup(); // in case a drag is already in progress
const rect = this.reference.getBoundingClientRect();
let operation = Object.create(this);
operation.mouse_button = event.button;
operation.raw = event;
operation.start(operation.coords(rect, event));
function mouseMove(event) {
operation.raw = event;
operation.drag(operation.coords(rect, event));
event.preventDefault();
event.stopPropagation();
}
function mouseUp(event) {
operation.raw = event;
operation.end(operation.coords(rect, event));
mouse_cleanup();
event.preventDefault();
event.stopPropagation();
}
mouse_cleanup = () => {
window.removeEventListener('mousemove', mouseMove);
window.removeEventListener('mouseup', mouseUp);
mouse_cleanup = () => null;
};
window.addEventListener('mousemove', mouseMove);
window.addEventListener('mouseup', mouseUp);
event.preventDefault();
event.stopPropagation();
};
let touch_begin = [];
const touchEvent = (event) => {
const rect = this.reference.getBoundingClientRect();
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i];
let current = this.coords(rect, touch);
current.raw = touch;
switch (event.type) {
case 'touchstart':
touch_begin[touch.identifier] = Object.create(this);
touch_begin[touch.identifier].touch_identifier = touch.identifier;
touch_begin[touch.identifier].start(current);
break;
case 'touchmove':
touch_begin[touch.identifier].drag(current);
break;
case 'touchend':
touch_begin[touch.identifier].end(current);
touch_begin[touch.identifier] = null;
break;
}
}
event.preventDefault();
event.stopPropagation();
};
this.el.style.touchAction = 'none';
this.el.addEventListener('mousedown', mouseDown);
this.el.addEventListener('touchstart', touchEvent);
this.el.addEventListener('touchmove', touchEvent);
this.el.addEventListener('touchend', touchEvent);
this.uninstall = function() {
this.el.style.touchAction = '';
this.el.removeEventListener('mousedown', mouseDown);
this.el.removeEventListener('touchstart', touchEvent);
this.el.removeEventListener('touchmove', touchEvent);
this.el.removeEventListener('touchend', touchEvent);
mouse_cleanup();
};
}
// NOTE: this doesn't take into account css transforms
// <https://bugzilla.mozilla.org/show_bug.cgi?id=972041>
coords(rect, event) {
let coords = {x: event.clientX - rect.left, y: event.clientY - rect.top};
const svg = this.reference instanceof SVGSVGElement? this.reference : this.reference.ownerSVGElement;
if (svg) {
// NOTE: svg.getScreenCTM already factors in the bounding rect
// so there's no need to subtract rect, or even call getBoundingClientRect
let point = svg.createSVGPoint();
point.x = event.clientX;
point.y = event.clientY;
coords = point.matrixTransform(svg.getScreenCTM().inverse());
}
return coords;
}
start(_event) {}
drag(_event) {}
end(_event) {}
}

View File

@ -0,0 +1,95 @@
<div style="width:auto">
<style>
html, body { margin: 0; padding: 0; }
button { text-align: center; padding: 0; background: hsl(60,20%,90%); border-radius: 4px; cursor: pointer; }
button svg { width: 100%; height: 100%; }
button text { text-anchor: middle; font-size: 24px; font-family: var(--sans-serif, Arial); }
button.current-control { background: hsl(60,100%,80%); outline: 2px solid hsl(220,50%,50%); }
button:focus { outline: none; }
input[type="range"] { cursor: ew-resize; margin-left: 0; margin-right: 0; }
button:disabled, input:disabled { cursor: unset; opacity: 0.25; }
#ui {
display: grid;
min-height: calc(100vmin - 130px);
grid-template-rows: repeat(3, 40px) calc(100vmin - 130px - 120px - 40px);
grid-template-columns: 1fr calc(100vmin - 130px) repeat(6, 20px) 1fr;
grid-row-gap: 10px;
grid-column-gap: 10px;
}
#map { grid-area: 1 / 2 / span 4 / span 1; width:100%; height:100%; }
#small { grid-area: 1 / 3 / span 1 / span 2; }
#medium { grid-area: 1 / 5 / span 1 / span 2; }
#large { grid-area: 1 / 7 / span 1 / span 2; }
#ocean { grid-area: 2 / 3 / span 1 / span 3; }
#shallow { grid-area: 2 / 6 / span 1 / span 3; }
#valley { grid-area: 3 / 3 / span 1 / span 3; }
#mountain { grid-area: 3 / 6 / span 1 / span 3; }
#sliders { grid-area: 4 / 3 / span 1 / span 6; overflow-y: scroll; overflow-x: clip; line-height: 1.1; }
@media (orientation: portrait) {
/* Put the buttons on bottom instead of on the right */
#ui {
grid-template-rows: calc(100vmin) repeat(6, 20px) 1fr;
grid-template-columns: 50px repeat(2, 70px) calc(100vmin - 50px - 140px - 40px);
}
#map { grid-area: 1 / 1 / span 1 / span 6; }
#small { grid-area: 2 / 1 / span 2 / span 1; }
#medium { grid-area: 4 / 1 / span 2 / span 1; }
#large { grid-area: 6 / 1 / span 2 / span 1; }
#ocean { grid-area: 2 / 2 / span 3 / span 1; }
#shallow { grid-area: 2 / 3 / span 3 / span 1; }
#valley { grid-area: 5 / 2 / span 3 / span 1; }
#mountain { grid-area: 5 / 3 / span 3 / span 1; }
#sliders { grid-area: 2 / 4 / span 7 / span 1; justify-self: end; width: 100%; column-width: 20ch; }
}
#mapgen4 { width: 100%; height: 100%; cursor: crosshair; }
#sliders h3 {
background-color: hsl(60,20%,90%);
margin: 0.5em 0;
padding: 0.5em;
font-family: var(--sans-serif, sans-serif);
font-size: 66%;
}
#sliders label {
display: block;
font-size: 66%;
}
#sliders label > * {
max-width: 99%;
}
#sliders label span {
font-family: var(--monospace, monospace);
display: inline-block;
width: 17ch;
}
#button-reset {
display: inline-block;
margin-left: auto;
margin-right: auto;
width: 100%;
height: 2em;
}
</style>
<div id="ui">
<button id="small"><svg viewBox="-50 -50 100 100"><circle r="20"/></svg></button>
<button id="medium"><svg viewBox="-50 -50 100 100"><circle r="35"/></svg></button>
<button id="large"><svg viewBox="-50 -50 100 100"><circle r="50"/></svg></button>
<button id="ocean" title="Ocean"><svg viewBox="-50 -50 100 100"><text y="45">Ocean</text><path d="M -50,-30 q 25,-20 50,0 q 25,20 50,0 l 0,50 l -100,0 z" fill="hsl(240,50%,40%)"/></svg></button>
<button id="shallow" title="Water"><svg viewBox="-50 -50 100 100"><text y="45">Water</text><path d="M -50,-20 q 15,20 30,0 q 20,20 40,0 q 15,20 30,0 l 0,40 l -100,0 z" fill="hsl(200,50%,70%)"/></svg></button>
<button id="valley" title="Valley"><svg viewBox="-50 -50 100 100"><text y="45">Valley</text><path d="M -50,-20 c 20,10 80,10 100,0 l 0,40 l -100,0 z" fill="hsl(100,40%,60%)"/></svg></button>
<button id="mountain" title="Mountain"><svg viewBox="-50 -50 100 100"><text y="45">Mountains</text><g fill="hsl(60,50%,40%)" stroke="white" stroke-width="2"><path d="M -30,20 l 30,-60 l 30,60 z"/><path d="M -50,20 l 20,-40 l 20,40 z"/><path d="M 10,20 l 15,-30 l 15,30 z"/></g></svg></button>
<div id="sliders">
<button id="button-reset">Reset</button>
</div>
<div id="map"><canvas id="mapgen4" width="2048" height="2048"/></div>
</div>
<script defer="defer" src="draggable.v2.js"></script>
<script defer="defer" src="build/_bundle.js"></script>
</div>

View File

@ -0,0 +1,56 @@
/*
* From https://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* Generate the points used for the mountains peaks and the mesh.
*
* This step is slow and it doesn't vary from one run to the next so
* it makes sense to precompute the results and save them to a file.
*
* File format: Int16Array, where first element is the number of
* mountain peaks M, then the next M elements are the mountainIndices into the
* mesh that have mountain peaks, then the rest are X,Y
*/
'use strict';
const fs = require('fs');
const {makeRandFloat} = require('@redblobgames/prng');
const Poisson = require('poisson-disk-sampling');
const {mesh, spacing, mountainSpacing} = require('./config');
const filename = `build/points-${spacing}.data`;
/* First generate the mountain points */
let mountainPoints = new Poisson([1000, 1000], mountainSpacing, undefined, undefined, makeRandFloat(mesh.seed)).fill();
/* Generate the rest of the mesh points with the mountain points as constraints */
let generator = new Poisson([1000, 1000], spacing, undefined, undefined, makeRandFloat(mesh.seed));
for (let p of mountainPoints) { generator.addPoint(p); }
let meshPoints = generator.fill();
/* For better compression, I want to sort the points. However, that
* means the mountain points are no longer at the beginning of the
* array, so I need some way to find them. Solution: keep track of the
* original position of the points, then write out the new positions
* of the mountain points. */
meshPoints = meshPoints.map((p, i) => [p[0] | 0, p[1] | 0, i]);
meshPoints.sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]);
/* File format described at top */
let mountainIndices = [];
for (let i = 0; i < meshPoints.length; i++) {
if (meshPoints[i][2] < mountainPoints.length) {
mountainIndices.push(i);
}
}
let flat = [mountainPoints.length].concat(mountainIndices);
for (let p of meshPoints) {
flat.push(p[0], p[1]);
}
fs.writeFileSync(filename, Uint16Array.from(flat));
/* For debugging, write an ascii version: */
// fs.writeFileSync(filename, JSON.stringify(flat));

View File

@ -0,0 +1,268 @@
/*
* From http://www.redblobgames.com/maps/magpen4/
* Copyright 2017 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*/
import {vec2} from 'gl-matrix';
import Map from './map';
/**
* @typedef { import("./types").Mesh } Mesh
*/
/**
* Fill a buffer with data from the mesh.
*
* @param {Mesh} mesh
* @param {Float32Array} P - x,y for each region, then for each triangle
*/
function setMeshGeometry(mesh, P) {
let {numRegions, numTriangles} = mesh;
if (P.length !== 2 * (numRegions + numTriangles)) { throw "wrong size"; }
let p = 0;
for (let r = 0; r < numRegions; r++) {
P[p++] = mesh.r_x(r);
P[p++] = mesh.r_y(r);
}
for (let t = 0; t < numTriangles; t++) {
P[p++] = mesh.t_x(t);
P[p++] = mesh.t_y(t);
}
};
/**
* Fill an indexed buffer with data from the map.
*
* @param {Map} map
* @param {Int32Array} I - indices into the data array
* @param {Float32Array} P - elevation, rainfall data
*/
function setMapGeometry(map, I, P) {
// TODO: V should probably depend on the slope, or elevation, or maybe it should be 0.95 in mountainous areas and 0.99 elsewhere
const V = 0.95; // reduce elevation in valleys
let {mesh, s_flow, r_elevation, t_elevation, r_rainfall} = map;
let {numSolidSides, numRegions, numTriangles} = mesh;
if (I.length !== 3 * numSolidSides) { throw "wrong size"; }
if (P.length !== 2 * (numRegions + numTriangles)) { throw "wrong size"; }
let p = 0;
for (let r = 0; r < numRegions; r++) {
P[p++] = r_elevation[r];
P[p++] = r_rainfall[r];
}
for (let t = 0; t < numTriangles; t++) {
P[p++] = V * t_elevation[t];
let s0 = 3*t;
let r1 = mesh.s_begin_r(s0),
r2 = mesh.s_begin_r(s0+1),
r3 = mesh.s_begin_r(s0+2);
P[p++] = 1/3 * (r_rainfall[r1] + r_rainfall[r2] + r_rainfall[r3]);
}
// TODO: split this into its own function; it can be updated separately, and maybe not as often
let i = 0;
for (let s = 0; s < numSolidSides; s++) {
let opposite_s = mesh.s_opposite_s(s),
r1 = mesh.s_begin_r(s),
r2 = mesh.s_begin_r(opposite_s),
t1 = mesh.s_inner_t(s),
t2 = mesh.s_inner_t(opposite_s);
// Each quadrilateral is turned into two triangles, so each
// half-edge gets turned into one. There are two ways to fold
// a quadrilateral. This is usually a nuisance but in this
// case it's a feature. See the explanation here
// https://www.redblobgames.com/x/1725-procedural-elevation/#rendering
let coast = r_elevation[r1] < 0.0 || r_elevation[r2] < 0.0;
if (coast || s_flow[s] > 0 || s_flow[opposite_s] > 0) {
// It's a coastal or river edge, forming a valley
I[i++] = r1; I[i++] = numRegions+t2; I[i++] = numRegions+t1;
} else {
// It's a ridge
I[i++] = r1; I[i++] = r2; I[i++] = numRegions+t1;
}
}
if (I.length !== i) { throw "wrong size"; }
if (P.length !== p) { throw "wrong size"; }
};
/**
* Create a bitmap that will be used for texture mapping
* BEND textures will be ordered: {blank side, input side, output side}
* FORK textures will be ordered: {passive input side, active input side, output side}
*
* Cols will be the input flow rate
* Rows will be the output flow rate
*/
function assignTextureCoordinates(spacing, numSizes, textureSize) {
/* create (numSizes+1)^2 size combinations, each with two triangles */
function UV(x, y) {
return {xy: [x, y], uv: [(x+0.5)/textureSize, (y+0.5)/textureSize]};
}
let triangles = [[]];
let width = Math.floor((textureSize - 2*spacing) / (2*numSizes+3)) - spacing,
height = Math.floor((textureSize - 2*spacing) / (numSizes+1)) - spacing;
for (let row = 0; row <= numSizes; row++) {
triangles[row] = [];
for (let col = 0; col <= numSizes; col++) {
let baseX = spacing + (2 * spacing + 2 * width) * col,
baseY = spacing + (spacing + height) * row;
triangles[row][col] = [
[UV(baseX + width, baseY),
UV(baseX, baseY + height),
UV(baseX + 2*width, baseY + height)],
[UV(baseX + 2*width + spacing, baseY + height),
UV(baseX + 3*width + spacing, baseY),
UV(baseX + width + spacing, baseY)]
];
}
}
return triangles;
}
// TODO: turn this into an object :-/
const riverTextureSpacing = 40; // TODO: should depend on river size
const numRiverSizes = 24; // NOTE: too high and rivers are low quality; too low and there's not enough variation
const riverTextureSize = 4096;
const riverMaximumFractionOfWidth = 0.5;
const riverTexturePositions = assignTextureCoordinates(riverTextureSpacing, numRiverSizes, riverTextureSize);
function createRiverBitmap() {
let canvas = document.createElement('canvas');
canvas.width = canvas.height = riverTextureSize;
let ctx = canvas.getContext('2d');
function lineWidth(i) {
const spriteSize = riverTexturePositions[0][1][0][0].xy[0] - riverTexturePositions[0][0][0][0].xy[0];
return i / numRiverSizes * spriteSize * riverMaximumFractionOfWidth;
}
ctx.lineCap = "round";
for (let row = 0; row <= numRiverSizes; row++) {
for (let col = 0; col <= numRiverSizes; col++) {
for (let type = 0; type < 2; type++) {
let pos = riverTexturePositions[row][col][type];
ctx.save();
ctx.beginPath();
ctx.rect(pos[1].xy[0] - riverTextureSpacing/2, pos[0].xy[1] - riverTextureSpacing/2,
pos[2].xy[0] - pos[1].xy[0] + riverTextureSpacing, pos[2].xy[1] - pos[0].xy[1] + riverTextureSpacing);
// ctx.clip(); // TODO: to make this work right, the spacing needs to vary based on the river size, I think
let center = [(pos[0].xy[0] + pos[1].xy[0] + pos[2].xy[0]) / 3,
(pos[0].xy[1] + pos[1].xy[1] + pos[2].xy[1]) / 3];
let midpoint12 = vec2.lerp([], pos[1].xy, pos[2].xy, 0.5);
let midpoint20 = vec2.lerp([], pos[2].xy, pos[0].xy, 0.5);
ctx.strokeStyle = "hsl(200,50%,35%)";
if (type === 1) {
// TODO: river delta/fork sprite
} else {
const w = 1; /* TODO: draw a path and fill it; that will allow variable width */
let c = vec2.lerp([], pos[1].xy, pos[2].xy, 0.5 - w),
d = vec2.lerp([], pos[1].xy, pos[2].xy, 0.5 + w),
a = vec2.lerp([], pos[0].xy, pos[1].xy, 0.5 - w),
f = vec2.lerp([], pos[0].xy, pos[1].xy, 0.5 + w),
b = null /* TODO: intersect lines */,
e = null /* TODO: intersect lines */;
if (col > 0) {
ctx.lineWidth = Math.min(lineWidth(col), lineWidth(row));
ctx.beginPath();
ctx.moveTo(midpoint12[0], midpoint12[1]);
ctx.quadraticCurveTo(center[0], center[1], midpoint20[0], midpoint20[1]);
ctx.stroke();
} else {
ctx.lineWidth = lineWidth(row);
ctx.beginPath();
ctx.moveTo(center[0], center[1]);
ctx.lineTo(midpoint20[0], midpoint20[1]);
ctx.stroke();
}
}
ctx.restore();
}
}
}
return canvas;
};
function clamp(x, lo, hi) {
if (x < lo) { x = lo; }
if (x > hi) { x = hi; }
return x;
}
/**
* Fill a buffer with river geometry
*
* @param {Map} map
* @param {number} spacing - global param.spacing value
* @param {any} riversParam - global param.rivers
* @param {Float32Array} P - array of x,y,u,v triples for the river triangles
* @returns {number} - how many triangles were needed (at most numSolidTriangles)
*/
function setRiverTextures(map, spacing, riversParam, P) {
const MIN_FLOW = Math.exp(riversParam.lg_min_flow);
const RIVER_WIDTH = Math.exp(riversParam.lg_river_width);
let {mesh, t_downslope_s, s_flow} = map;
let {numSolidTriangles, s_length} = mesh;
function riverSize(s, flow) {
// TODO: performance: build a table of flow to width
if (s < 0) { return 1; }
let width = Math.sqrt(flow - MIN_FLOW) * spacing * RIVER_WIDTH;
let size = Math.ceil(width * numRiverSizes / s_length[s]);
return clamp(size, 1, numRiverSizes);
}
let p = 0, uv = [0, 0, 0, 0, 0, 0];
for (let t = 0; t < numSolidTriangles; t++) {
let out_s = t_downslope_s[t];
let out_flow = s_flow[out_s];
if (out_s < 0 || out_flow < MIN_FLOW) continue;
let r1 = mesh.s_begin_r(3*t ),
r2 = mesh.s_begin_r(3*t + 1),
r3 = mesh.s_begin_r(3*t + 2);
let in1_s = mesh.s_next_s(out_s);
let in2_s = mesh.s_next_s(in1_s);
let in1_flow = s_flow[mesh.s_opposite_s(in1_s)];
let in2_flow = s_flow[mesh.s_opposite_s(in2_s)];
let textureRow = riverSize(out_s, out_flow);
function add(r, c, i, j, k) {
const T = riverTexturePositions[r][c][0];
P[p ] = mesh.r_x(r1);
P[p + 1] = mesh.r_y(r1);
P[p + 4] = mesh.r_x(r2);
P[p + 5] = mesh.r_y(r2);
P[p + 8] = mesh.r_x(r3);
P[p + 9] = mesh.r_y(r3);
P[p + 4*(out_s - 3*t) + 2] = T[i].uv[0];
P[p + 4*(out_s - 3*t) + 3] = T[i].uv[1];
P[p + 4*(in1_s - 3*t) + 2] = T[j].uv[0];
P[p + 4*(in1_s - 3*t) + 3] = T[j].uv[1];
P[p + 4*(in2_s - 3*t) + 2] = T[k].uv[0];
P[p + 4*(in2_s - 3*t) + 3] = T[k].uv[1];
p += 12;
}
if (in1_flow >= MIN_FLOW) {
add(textureRow, riverSize(in1_s, in1_flow), 0, 2, 1);
}
if (in2_flow >= MIN_FLOW) {
add(textureRow, riverSize(in2_s, in2_flow), 2, 1, 0);
}
}
return p / 12;
};
export default {setMeshGeometry, createRiverBitmap, setMapGeometry, setRiverTextures};

View File

@ -0,0 +1,186 @@
/*
* From http://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
*
* 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.
*/
'use strict';
import param from './config'; // param in the Config
import {makeMesh} from './mesh';
import Painting from './painting';
import Renderer from './render';
Config initialParams = new Config();
/** @typedef { import("./types").Mesh } Mesh */
/**
* Starts the UI, once the mesh has been loaded in.
*
* @param {{mesh: Mesh, peaks_t: number[]}} _
*/
function main(Mesh mesh, float[] peaks_t}) {
Renderer render = new Renderer(mesh);
/* set initial parameters */
for (let phase of ['elevation', 'biomes', 'rivers', 'render']) {
const container = document.createElement('div');
const header = document.createElement('h3');
header.appendChild(document.createTextNode(phase));
container.appendChild(header);
document.getElementById('sliders').appendChild(container);
for (let [name, initialValue, min, max] of initialParams[phase]) {
const step = name === 'seed'? 1 : 0.001;
param[phase][name] = initialValue;
let span = document.createElement('span');
span.appendChild(document.createTextNode(name));
let slider = document.createElement('input');
slider.setAttribute('type', name === 'seed'? 'number' : 'range');
slider.setAttribute('min', min);
slider.setAttribute('max', max);
slider.setAttribute('step', step);
slider.addEventListener('input', event => {
param[phase][name] = slider.valueAsNumber;
requestAnimationFrame(() => {
if (phase == 'render') { redraw(); }
else { generate(); }
});
});
/* improve slider behavior on iOS */
function handleTouch(e) {
let rect = slider.getBoundingClientRect();
let value = (e.changedTouches[0].clientX - rect.left) / rect.width;
value = min + value * (max - min);
value = Math.round(value / step) * step;
if (value < min) { value = min; }
if (value > max) { value = max; }
slider.value = value.toString();
slider.dispatchEvent(new Event('input'));
e.preventDefault();
e.stopPropagation();
};
slider.addEventListener('touchmove', handleTouch);
slider.addEventListener('touchstart', handleTouch);
let label = document.createElement('label');
label.setAttribute('id', `slider-${name}`);
label.appendChild(span);
label.appendChild(slider);
container.appendChild(label);
slider.value = initialValue;
}
}
function redraw() {
render.updateView(param.render);
}
/* Ask render module to copy WebGL into Canvas */
function download() {
render.screenshotCallback = () => {
let a = document.createElement('a');
render.screenshotCanvas.toBlob(blob => {
// TODO: Firefox doesn't seem to allow a.click() to
// download; is it everyone or just my setup?
a.href = URL.createObjectURL(blob);
a.setAttribute('download', `mapgen4-${param.elevation.seed}.png`);
a.click();
});
};
render.updateView(param.render);
}
Painting.screenToWorldCoords = (coords) => {
let out = render.screenToWorld(coords);
return [out[0] / 1000, out[1] / 1000];
};
Painting.onUpdate = () => {
generate();
};
const worker = new window.Worker("build/_worker.js");
let working = false;
let workRequested = false;
let elapsedTimeHistory = [];
worker.addEventListener('messageerror', event => {
console.log("WORKER ERROR", event);
});
worker.addEventListener('message', event => {
working = false;
let {elapsed, numRiverTriangles, quad_elements_buffer, a_quad_em_buffer, a_river_xyuv_buffer} = event.data;
elapsedTimeHistory.push(elapsed | 0);
if (elapsedTimeHistory.length > 10) { elapsedTimeHistory.splice(0, 1); }
const timingDiv = document.getElementById('timing');
if (timingDiv) { timingDiv.innerText = `${elapsedTimeHistory.join(' ')} milliseconds`; }
render.quad_elements = new Int32Array(quad_elements_buffer);
render.a_quad_em = new Float32Array(a_quad_em_buffer);
render.a_river_xyuv = new Float32Array(a_river_xyuv_buffer);
render.numRiverTriangles = numRiverTriangles;
render.updateMap();
redraw();
if (workRequested) {
requestAnimationFrame(() => {
workRequested = false;
generate();
});
}
});
function updateUI() {
let userHasPainted = Painting.userHasPainted();
document.querySelector("#slider-seed input").disabled = userHasPainted;
document.querySelector("#slider-island input").disabled = userHasPainted;
document.querySelector("#button-reset").disabled = !userHasPainted;
}
function generate() {
if (!working) {
working = true;
Painting.setElevationParam(param.elevation);
updateUI();
worker.postMessage({
param,
constraints: {
size: Painting.size,
constraints: Painting.constraints,
},
quad_elements_buffer: render.quad_elements.buffer,
a_quad_em_buffer: render.a_quad_em.buffer,
a_river_xyuv_buffer: render.a_river_xyuv.buffer,
}, [
render.quad_elements.buffer,
render.a_quad_em.buffer,
render.a_river_xyuv.buffer,
]
);
} else {
workRequested = true;
}
}
worker.postMessage({mesh, peaks_t, param});
generate();
const downloadButton = document.getElementById('button-download');
if (downloadButton) downloadButton.addEventListener('click', download);
}
makeMesh().then(main);

View File

@ -0,0 +1,172 @@
package org.atriasoft.arkon;
/*
* From https://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* Point selection (blue noise or jittered grid), mountain peak
* selection, and mesh building.
*
* Points are regions (r), and either come from a jittered hexagonal
* grid or a precomputed blue noise set. Mountain peaks are triangles
* (t), and either come from a random subset of triangles or from a
* non-random subset of the blue noise points. However, since the blue
* noise points are regions and mountain peaks are triangles, I
* arbitrarily pick one triangle from each region.
*
* The precomputed points are read from the network, so the module
* uses async functions that build the mesh only after the points are
* read in.
*/
//import param from './config';
import Config;
//import MeshBuilder from '@redblobgames/dual-mesh/create';
import Create.MeshBuilder;
import {makeRandFloat} from '@redblobgames/prng';
/**
* @typedef { import("./types").Mesh } Mesh
*/
/**
* Apply random circular jitter to a set of points.
*
* @param {number[][]} points
* @param {number} dr
* @param {function(): number} randFloat
*/
function applyJitter(points, dr, randFloat) {
let newPoints = [];
for (let p of points) {
let r = dr * Math.sqrt(Math.abs(randFloat()));
let a = Math.PI * randFloat();
let dx = r * Math.cos(a);
let dy = r * Math.sin(a);
newPoints.push([p[0] + dx, p[1] + dy]);
}
return newPoints;
}
/**
* Generate a hexagonal grid with a given spacing. This is used when NOT
* reading points from a file.
*
* @param {number} spacing - horizontal spacing between adjacent hexagons
* @returns {[number, number][]} - list of [x, y] points
*/
function hexagonGrid(spacing) {
let points = /** @type{[number, number][]} */([]);
let offset = 0;
for (let y = spacing/2; y < 1000-spacing/2; y += spacing * 3/4) {
offset = (offset === 0)? spacing/2 : 0;
for (let x = offset + spacing/2; x < 1000-spacing/2; x += spacing) {
points.push([x, y]);
}
}
return points;
}
/**
* Choose a random set of regions for mountain peaks. This is used
* when NOT reading points from a file.
*
* @param {number} numPoints
* @param {number} spacing - param.spacing parameter, used to calculate density
* @param {function(): number} randFloat - random number generator (0-1)
* @returns {number[]} - array of point indices
*/
function chooseMountainPeaks(numPoints, spacing, randFloat) {
const fractionOfPeaks = spacing*spacing / param.mountainDensity;
let peaks_r = [];
for (let r = 0; r < numPoints; r++) {
if (randFloat() < fractionOfPeaks) {
peaks_r.push(r);
}
}
return peaks_r;
}
/**
* Read mesh and mountain peak points from a file saved by generate-points.js
*
* The points are [x,y]; the peaks in the index are an index into the
* points[] array, *not* region ids. The mesh creation process can
* insert new regions before and after this array, so these indices
* have to be adjusted later.
*
* @param {ArrayBuffer} buffer - data read from the mesh file
* @returns {{points: number[][], peaks_index: number[]}}
*/
function extractPoints(buffer) {
/* See file format in generate-points.js */
const pointData = new Uint16Array(buffer);
const numMountainPeaks = pointData[0];
let peaks_index = Array.from(pointData.slice(1, 1 + numMountainPeaks));
const numRegions = (pointData.length - numMountainPeaks - 1) / 2;
let points = [];
for (let i = 0; i < numRegions; i++) {
let j = 1 + numMountainPeaks + 2*i;
points.push([pointData[j], pointData[j+1]]);
}
return {points, peaks_index};
}
/**
* Either read mesh and mountain peak points, or generate locally.
*
* TODO: This hard-codes the spacing of 5; it should be a parameter
*/
async function choosePoints() {
let points = undefined, peaks_index = undefined;
const jitter = 0.5;
if (param.spacing === 5) {
let buffer = await fetch("build/points-5.data").then(response => response.arrayBuffer());
let extraction = extractPoints(buffer);
points = applyJitter(extraction.points, param.spacing * jitter * 0.5, makeRandFloat(param.mesh.seed));
peaks_index = extraction.peaks_index;
} else {
points = applyJitter(hexagonGrid(1.5 * param.spacing), param.spacing * jitter, makeRandFloat(param.mesh.seed));
peaks_index = chooseMountainPeaks(points.length, param.spacing, makeRandFloat(param.mesh.seed));
};
return {points, peaks_index};
}
export async function makeMesh() {
let {points, peaks_index} = await choosePoints();
let builder = new MeshBuilder({boundarySpacing: param.spacing * 1.5})
.addPoints(points);
let mesh = /** @type {Mesh} */(builder.create());
console.log(`triangles = ${mesh.numTriangles} regions = ${mesh.numRegions}`);
mesh.s_length = new Float32Array(mesh.numSides);
for (let s = 0; s < mesh.numSides; s++) {
let r1 = mesh.s_begin_r(s),
r2 = mesh.s_end_r(s);
let dx = mesh.r_x(r1) - mesh.r_x(r2),
dy = mesh.r_y(r1) - mesh.r_y(r2);
mesh.s_length[s] = Math.sqrt(dx*dx + dy*dy);
}
/* The input points get assigned to different positions in the
* output mesh. The peaks_index has indices into the original
* array. This test makes sure that the logic for mapping input
* indices to output indices hasn't changed. */
if (points[200][0] !== mesh.r_x(200 + mesh.numBoundaryRegions)
|| points[200][1] !== mesh.r_y(200 + mesh.numBoundaryRegions)) {
throw "Mapping from input points to output points has changed";
}
let peaks_r = peaks_index.map(i => i + mesh.numBoundaryRegions);
let peaks_t = [];
for (let r of peaks_r) {
peaks_t.push(mesh.s_inner_t(mesh._r_in_s[r]));
}
return {mesh, peaks_t};
}

View File

@ -0,0 +1,223 @@
/*
* From https://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* This module allows the user to paint constraints for the map generator
*/
'use strict';
/* global Draggable */
/*
* The painting interface uses a square array of elevations. As you
* drag the mouse it will paint filled circles into the elevation map,
* then send the elevation map to the generator to produce the output.
*/
import SimplexNoise from 'simplex-noise';
import {makeRandFloat} from '@redblobgames/prng';
const CANVAS_SIZE = 128;
const currentStroke = {
/* elevation before the current paint stroke began */
previousElevation: new Float32Array(CANVAS_SIZE * CANVAS_SIZE),
/* how long, in milliseconds, was spent painting */
time: new Float32Array(CANVAS_SIZE * CANVAS_SIZE),
/* maximum strength applied */
strength: new Float32Array(CANVAS_SIZE * CANVAS_SIZE),
};
/* The elevation is -1.0 to 0.0 → water, 0.0 to +1.0 → land */
class Generator {
constructor () {
this.userHasPainted = false;
this.elevation = new Float32Array(CANVAS_SIZE * CANVAS_SIZE);
}
setElevationParam(elevationParam) {
if ( elevationParam.seed !== this.seed
|| elevationParam.island !== this.island) {
this.seed = elevationParam.seed;
this.island = elevationParam.island;
this.generate();
}
}
/** Use a noise function to determine the shape */
generate() {
const {elevation, island} = this;
const noise = new SimplexNoise(makeRandFloat(this.seed));
const persistence = 1/2;
const amplitudes = Array.from({length: 5}, (_, octave) => Math.pow(persistence, octave));
function fbm_noise(nx, ny) {
let sum = 0, sumOfAmplitudes = 0;
for (let octave = 0; octave < amplitudes.length; octave++) {
let frequency = 1 << octave;
sum += amplitudes[octave] * noise.noise2D(nx * frequency, ny * frequency);
sumOfAmplitudes += amplitudes[octave];
}
return sum / sumOfAmplitudes;
}
for (let y = 0; y < CANVAS_SIZE; y++) {
for (let x = 0; x < CANVAS_SIZE; x++) {
let p = y * CANVAS_SIZE + x;
let nx = 2 * x/CANVAS_SIZE - 1,
ny = 2 * y/CANVAS_SIZE - 1;
let distance = Math.max(Math.abs(nx), Math.abs(ny));
let e = 0.5 * (fbm_noise(nx, ny) + island * (0.75 - 2 * distance * distance));
if (e < -1.0) { e = -1.0; }
if (e > +1.0) { e = +1.0; }
elevation[p] = e;
if (e > 0.0) {
let m = (0.5 * noise.noise2D(nx + 30, ny + 50)
+ 0.5 * noise.noise2D(2*nx + 33, 2*ny + 55));
// TODO: make some of these into parameters
let mountain = Math.min(1.0, e * 5.0) * (1 - Math.abs(m) / 0.5);
if (mountain > 0.0) {
elevation[p] = Math.max(e, Math.min(e * 3, mountain));
}
}
}
}
this.userHasPainted = false;
}
/**
* Paint a circular region
*
* @param {{elevation: number}} tool
* @param {number} x0 - should be 0 to 1
* @param {number} y0 - should be 0 to 1
* @param {{innerRadius: number, outerRadius: number, rate: number}} size
* @param {number} deltaTimeInMs
*/
paintAt(tool, x0, y0, size, deltaTimeInMs) {
let {elevation} = this;
/* This has two effects: first time you click the mouse it has a
* strong effect, and it also limits the amount in case you
* pause */
deltaTimeInMs = Math.min(100, deltaTimeInMs);
let newElevation = tool.elevation;
let {innerRadius, outerRadius, rate} = size;
let xc = (x0 * CANVAS_SIZE) | 0, yc = (y0 * CANVAS_SIZE) | 0;
let top = Math.max(0, yc - outerRadius),
bottom = Math.min(CANVAS_SIZE-1, yc + outerRadius);
for (let y = top; y <= bottom; y++) {
let s = Math.sqrt(outerRadius * outerRadius - (y - yc) * (y - yc)) | 0;
let left = Math.max(0, xc - s),
right = Math.min(CANVAS_SIZE-1, xc + s);
for (let x = left; x <= right; x++) {
let p = y * CANVAS_SIZE + x;
let distance = Math.sqrt((x - xc) * (x - xc) + (y - yc) * (y - yc));
let strength = 1.0 - Math.min(1, Math.max(0, (distance - innerRadius) / (outerRadius - innerRadius)));
let factor = rate/1000 * deltaTimeInMs;
currentStroke.time[p] += strength * factor;
if (strength > currentStroke.strength[p]) {
currentStroke.strength[p] = (1 - factor) * currentStroke.strength[p] + factor * strength;
}
let mix = currentStroke.strength[p] * Math.min(1, currentStroke.time[p]);
elevation[p] = (1 - mix) * currentStroke.previousElevation[p] + mix * newElevation;
}
}
this.userHasPainted = true;
}
}
let heightMap = new Generator();
let exported = {
size: CANVAS_SIZE,
onUpdate: () => {},
screenToWorldCoords: coords => coords,
constraints: heightMap.elevation,
setElevationParam: elevationParam => heightMap.setElevationParam(elevationParam),
userHasPainted: () => heightMap.userHasPainted,
};
document.getElementById('button-reset').addEventListener('click', () => {
heightMap.generate();
exported.onUpdate();
});
const SIZES = {
// rate is effect per second
small: {key: '1', rate: 8, innerRadius: 2, outerRadius: 6},
medium: {key: '2', rate: 5, innerRadius: 5, outerRadius: 10},
large: {key: '3', rate: 3, innerRadius: 10, outerRadius: 16},
};
const TOOLS = {
ocean: {elevation: -0.25},
shallow: {elevation: -0.05},
valley: {elevation: +0.05},
mountain: {elevation: +1.0},
};
let currentTool = 'mountain';
let currentSize = 'small';
function displayCurrentTool() {
const className = 'current-control';
for (let c of document.querySelectorAll("."+className)) {
c.classList.remove(className);
}
document.getElementById(currentTool).classList.add(className);
document.getElementById(currentSize).classList.add(className);
}
/** @type {[string, string, function][]} */
const controls = [
['1', "small", () => { currentSize = 'small'; }],
['2', "medium", () => { currentSize = 'medium'; }],
['3', "large", () => { currentSize = 'large'; }],
['q', "ocean", () => { currentTool = 'ocean'; }],
['w', "shallow", () => { currentTool = 'shallow'; }],
['e', "valley", () => { currentTool = 'valley'; }],
['r', "mountain", () => { currentTool = 'mountain'; }],
];
window.addEventListener('keydown', e => {
for (let control of controls) {
if (e.key === control[0]) { control[2](); displayCurrentTool(); }
}
});
for (let control of controls) {
document.getElementById(control[1]).addEventListener('click', () => { control[2](); displayCurrentTool(); } );
}
displayCurrentTool();
const output = document.getElementById('mapgen4');
new Draggable({
// TODO: replace with pointer events, now that they're widely supported
el: output,
start(event) {
this.timestamp = Date.now();
currentStroke.time.fill(0);
currentStroke.strength.fill(0);
currentStroke.previousElevation.set(heightMap.elevation);
this.drag(event);
},
drag(event) {
const nowMs = Date.now();
let coords = [event.x / output.clientWidth,
event.y / output.clientHeight];
coords = exported.screenToWorldCoords(coords);
heightMap.paintAt(TOOLS[currentTool], coords[0], coords[1], SIZES[currentSize], nowMs - this.timestamp);
this.timestamp = nowMs;
exported.onUpdate();
},
});
export default exported;

View File

@ -0,0 +1,67 @@
/*
* From https://www.redblobgames.com/maps/mapgen4/
* Copyright 2018 Red Blob Games <redblobgames@gmail.com>
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
*
* This module runs the worker thread that calculates the map data.
*/
'use strict';
import DualMesh from '@redblobgames/dual-mesh';
import Map from './map';
import Geometry from './geometry';
/**
* @typedef { import("./types").Mesh } Mesh
*/
// This handler is for the initial message
let handler = event => {
const param = event.data.param;
// NOTE: web worker messages only include the data; to
// reconstruct the full object I call the constructor again
// and then copy the data over
const mesh = /** @type{Mesh} */(new DualMesh(event.data.mesh));
Object.assign(mesh, event.data.mesh);
const map = new Map(mesh, event.data.peaks_t, param);
// TODO: placeholder
const run = {elevation: true, biomes: true, rivers: true};
// This handler is for all subsequent messages
handler = event => {
let {param, constraints, quad_elements_buffer, a_quad_em_buffer, a_river_xyuv_buffer} = event.data;
let numRiverTriangles = 0;
let start_time = performance.now();
if (run.elevation) { map.assignElevation(param.elevation, constraints); }
if (run.biomes) { map.assignRainfall(param.biomes); }
if (run.rivers) { map.assignRivers(param.rivers); }
if (run.elevation || run.rivers) {
Geometry.setMapGeometry(map, new Int32Array(quad_elements_buffer), new Float32Array(a_quad_em_buffer));
}
if (run.rivers) { numRiverTriangles = Geometry.setRiverTextures(map, param.spacing, param.rivers, new Float32Array(a_river_xyuv_buffer)); }
let elapsed = performance.now() - start_time;
self.postMessage(
{elapsed,
numRiverTriangles,
quad_elements_buffer,
a_quad_em_buffer,
a_river_xyuv_buffer,
},
[
quad_elements_buffer,
a_quad_em_buffer,
a_river_xyuv_buffer,
]
);
};
};
onmessage = event => handler(event);

View File

@ -0,0 +1,37 @@
package org.atriasoft.eagle;
import org.atriasoft.etk.math.Vector3i;
public class CirclePointGenerator {
private Vector3i dataList[];
private int maxIndex = 0;
private int currentIndex = -1;
public CirclePointGenerator(Vector3i position, int distance) {
Vector3i startPosition = new Vector3i(position.x() - distance / 2, position.y() - distance / 2, position.z());
this.dataList = new Vector3i[distance * distance + 1];
int square = distance * distance;
for (int xxx = 0; xxx < distance; xxx++) {
for (int yyy = 0; yyy < distance; yyy++) {
if (position.distance2(startPosition.x() + xxx, startPosition.y() + yyy, position.z()) < square) {
this.dataList[this.maxIndex] = new Vector3i(startPosition.x() + xxx, startPosition.y() + yyy, position.z());
this.maxIndex++;
}
}
}
this.maxIndex--;
}
public Vector3i getValue() {
return this.dataList[this.currentIndex];
}
public boolean hasNext() {
if (this.currentIndex < this.maxIndex) {
this.currentIndex++;
return true;
}
return false;
}
}

View File

@ -0,0 +1,18 @@
package org.atriasoft.eagle;
import org.atriasoft.eagle.generator.Land;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.etk.math.Vector3i;
public class SpaceGenerator {
public static MetaMap generateNewMap() {
MetaMap out = new MetaMap(System.currentTimeMillis());
Land generator = new Land();
generator.generate(out, new Vector3i[] { new Vector3i(0, 0, 2) }, 100);
return out;
}
public static void insertMap(MetaMap data) {
}
}

View File

@ -0,0 +1,14 @@
package org.atriasoft.eagle.generator;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.etk.math.Vector3i;
public class Bridge implements LocalGenerator {
@Override
public void generate(MetaMap map, Vector3i[] positions, float distance) {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,27 @@
package org.atriasoft.eagle.generator;
import org.atriasoft.eagle.CirclePointGenerator;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.eagle.model.TypeElement;
import org.atriasoft.eagle.model.VoxelMap;
import org.atriasoft.etk.math.Vector3i;
public class Land implements LocalGenerator {
@Override
public void generate(MetaMap map, Vector3i[] positions, float distance) {
CirclePointGenerator generator = new CirclePointGenerator(positions[0], (int) distance);
while (generator.hasNext()) {
Vector3i pos = generator.getValue();
float height = map.getRand().nextFloat(0, 10);
VoxelMap elem = map.getValueCreate(pos.x(), pos.y());
elem.element = TypeElement.ELEMENT_STONE;
elem.height = height;
}
// need to apply a filter
}
}
////// https://www.redblobgames.com/maps/terrain-from-noise/

View File

@ -0,0 +1,8 @@
package org.atriasoft.eagle.generator;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.etk.math.Vector3i;
public interface LocalGenerator {
void generate(MetaMap map, Vector3i[] positions, float distance);
}

View File

@ -0,0 +1,14 @@
package org.atriasoft.eagle.generator;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.etk.math.Vector3i;
public class Montain implements LocalGenerator {
@Override
public void generate(MetaMap map, Vector3i[] positions, float distance) {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,14 @@
package org.atriasoft.eagle.generator;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.etk.math.Vector3i;
public class River implements LocalGenerator {
@Override
public void generate(MetaMap map, Vector3i[] positions, float distance) {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,14 @@
package org.atriasoft.eagle.generator;
import org.atriasoft.eagle.model.MetaMap;
import org.atriasoft.etk.math.Vector3i;
public class Road implements LocalGenerator {
@Override
public void generate(MetaMap map, Vector3i[] positions, float distance) {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,68 @@
package org.atriasoft.eagle.internal;
import io.scenarium.logger.LogLevel;
import io.scenarium.logger.Logger;
public class Log {
private static final String LIB_NAME = "eagle";
private static final String LIB_NAME_DRAW = Logger.getDrawableName(LIB_NAME);
private static final boolean PRINT_CRITICAL = Logger.getNeedPrint(LIB_NAME, LogLevel.CRITICAL);
private static final boolean PRINT_ERROR = Logger.getNeedPrint(LIB_NAME, LogLevel.ERROR);
private static final boolean PRINT_WARNING = Logger.getNeedPrint(LIB_NAME, LogLevel.WARNING);
private static final boolean PRINT_INFO = Logger.getNeedPrint(LIB_NAME, LogLevel.INFO);
private static final boolean PRINT_DEBUG = Logger.getNeedPrint(LIB_NAME, LogLevel.DEBUG);
private static final boolean PRINT_VERBOSE = Logger.getNeedPrint(LIB_NAME, LogLevel.VERBOSE);
private static final boolean PRINT_TODO = Logger.getNeedPrint(LIB_NAME, LogLevel.TODO);
private static final boolean PRINT_PRINT = Logger.getNeedPrint(LIB_NAME, LogLevel.PRINT);
public static void critical(final String data) {
if (PRINT_CRITICAL) {
Logger.critical(LIB_NAME_DRAW, data);
}
}
public static void debug(final String data) {
if (PRINT_DEBUG) {
Logger.debug(LIB_NAME_DRAW, data);
}
}
public static void error(final String data) {
if (PRINT_ERROR) {
Logger.error(LIB_NAME_DRAW, data);
}
}
public static void info(final String data) {
if (PRINT_INFO) {
Logger.info(LIB_NAME_DRAW, data);
}
}
public static void print(final String data) {
if (PRINT_PRINT) {
Logger.print(LIB_NAME_DRAW, data);
}
}
public static void todo(final String data) {
if (PRINT_TODO) {
Logger.todo(LIB_NAME_DRAW, data);
}
}
public static void verbose(final String data) {
if (PRINT_VERBOSE) {
Logger.verbose(LIB_NAME_DRAW, data);
}
}
public static void warning(final String data) {
if (PRINT_WARNING) {
Logger.warning(LIB_NAME_DRAW, data);
}
}
private Log() {}
}

View File

@ -0,0 +1,49 @@
package org.atriasoft.eagle.model;
import org.atriasoft.etk.math.Vector3i;
public class MapChunk {
public final Vector3i dimention;
public final Vector3i position;
private final VoxelMap[] element;
public MapChunk(Vector3i size, Vector3i pos) {
this.dimention = size;
this.position = pos;
this.element = new VoxelMap[this.dimention.x() * this.dimention.y()];
}
public VoxelMap getValue(int xxx, int yyy) {
return getValueInternal(xxx - this.position.x(), yyy - this.position.y());
}
public VoxelMap getValue(Vector3i position) {
return getValue(position.x(), position.y());
}
public VoxelMap getValueCreate(int xxx, int yyy) {
return getValueCreateInternal(xxx - this.position.x(), yyy - this.position.y());
}
public VoxelMap getValueCreate(Vector3i position) {
return getValueCreate(position.x(), position.y());
}
private VoxelMap getValueCreateInternal(int xxx, int yyy) {
VoxelMap val = this.element[yyy * this.dimention.x() + xxx];
if (val == null) {
val = new VoxelMap();
this.element[this.dimention.y() + yyy * this.dimention.x() + xxx] = val;
}
return val;
}
private VoxelMap getValueInternal(int xxx, int yyy) {
return this.element[yyy * this.dimention.x() + xxx];
}
public boolean isStartPosition(int xxx, int yyy) {
return this.position.x() == xxx & this.position.y() == yyy;
}
}

View File

@ -0,0 +1,60 @@
package org.atriasoft.eagle.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.atriasoft.etk.math.Vector3i;
public class MetaMap {
private final long originalSeed;
private Vector3i size = Vector3i.VALUE_128;
private final Random randGenerator;
private final List<MapChunk> elementChunk = new ArrayList<>();
public MetaMap(long seed) {
this.originalSeed = seed;
this.randGenerator = new Random(seed);
this.elementChunk.add(new MapChunk(this.size, Vector3i.ZERO));
}
public Random getRand() {
return this.randGenerator;
}
public VoxelMap getValue(int xxx, int yyy) {
//find start
int startX = (xxx / this.size.x()) * this.size.x();
int startY = (yyy / this.size.y()) * this.size.y();
for (MapChunk elem : this.elementChunk) {
if (elem.isStartPosition(startX, startY)) {
return elem.getValue(xxx, yyy);
}
}
return null;
}
public VoxelMap getValue(Vector3i pos) {
// TODO Auto-generated method stub
return getValue(pos.x(), pos.y());
}
public VoxelMap getValueCreate(int xxx, int yyy) {
//find start
int startX = (xxx / this.size.x()) * this.size.x();
int startY = (yyy / this.size.y()) * this.size.y();
for (MapChunk elem : this.elementChunk) {
if (elem.isStartPosition(startX, startY)) {
return elem.getValueCreate(xxx, yyy);
}
}
MapChunk newElem = new MapChunk(this.size, new Vector3i(startX, startY, 0));
this.elementChunk.add(newElem);
return newElem.getValueCreate(xxx, yyy);
}
public VoxelMap getValueCreate(Vector3i pos) {
// TODO Auto-generated method stub
return getValue(pos.x(), pos.y());
}
}

View File

@ -0,0 +1,27 @@
package org.atriasoft.eagle.model;
public enum TypeElement {
FLOOR, // Generic floor area
FLOOR_STONE, // Element stone no specified
FLOOR_STONE_GRANIT, // stone with type granit
FLOOR_STONE_GIPSE, // stone with type gipse
FLOOR_STONE_BASALT, // stone with type basalt
FLOOR_DIRT, //
FLOOR_DIRT_BROWN, //
FLOOR_DIRT_DARK, //
FLOOR_DIRT_SAND, //
WATER, // generic water area
WATER_LIQUID, //
WATER_FROZZEN, //
WATER_HOT, //
ELEMENT, // generic Element area
ELEMENT_TREE, //
ELEMENT_TREE_DESERT, //
ELEMENT_TREE_FOREST, //
ELEMENT_TREE_LAND, //
ELEMENT_TREE_MONTAIN, //
ELEMENT_STONE, //
EMPTY, //
EMPTY_SMOKE, //
END
}

View File

@ -0,0 +1,6 @@
package org.atriasoft.eagle.model;
public class VoxelMap {
public TypeElement element;
public float height;
}

View File

@ -3,6 +3,7 @@ package org.atriasoft.ege.components;
import org.atriasoft.ege.Component;
import org.atriasoft.etk.Uri;
import org.atriasoft.loader3d.resources.ResourceMesh;
import org.atriasoft.loader3d.resources.ResourceMeshHeightMap;
import org.atriasoft.loader3d.resources.ResourceStaticMesh;
public class ComponentMesh extends Component {
@ -12,6 +13,10 @@ public class ComponentMesh extends Component {
this.mesh = ResourceMesh.create(objectFileName);
}
public ComponentMesh(ResourceMeshHeightMap externalMesh) {
this.mesh = externalMesh;
}
public void bindForRendering() {
if (this.mesh == null) {
return;

View File

@ -108,7 +108,7 @@ public class EnginePhysics extends Engine {
this.accumulator += deltaMili * 0.0001f;
// While there is enough accumulated time to take one or several physics steps
while (this.accumulator >= TIME_STEP) {
Log.info("update physic ... " + this.accumulator);
Log.verbose("update physic ... " + this.accumulator);
clearPreviousCycle();
applyForces(TIME_STEP);
// update AABB after because in rotation force, the Bounding box change...

View File

@ -0,0 +1,68 @@
package org.atriasoft.garoux.internal;
import io.scenarium.logger.LogLevel;
import io.scenarium.logger.Logger;
public class Log {
private static final String LIB_NAME = "garoux";
private static final String LIB_NAME_DRAW = Logger.getDrawableName(LIB_NAME);
private static final boolean PRINT_CRITICAL = Logger.getNeedPrint(LIB_NAME, LogLevel.CRITICAL);
private static final boolean PRINT_ERROR = Logger.getNeedPrint(LIB_NAME, LogLevel.ERROR);
private static final boolean PRINT_WARNING = Logger.getNeedPrint(LIB_NAME, LogLevel.WARNING);
private static final boolean PRINT_INFO = Logger.getNeedPrint(LIB_NAME, LogLevel.INFO);
private static final boolean PRINT_DEBUG = Logger.getNeedPrint(LIB_NAME, LogLevel.DEBUG);
private static final boolean PRINT_VERBOSE = Logger.getNeedPrint(LIB_NAME, LogLevel.VERBOSE);
private static final boolean PRINT_TODO = Logger.getNeedPrint(LIB_NAME, LogLevel.TODO);
private static final boolean PRINT_PRINT = Logger.getNeedPrint(LIB_NAME, LogLevel.PRINT);
public static void critical(final String data) {
if (PRINT_CRITICAL) {
Logger.critical(LIB_NAME_DRAW, data);
}
}
public static void debug(final String data) {
if (PRINT_DEBUG) {
Logger.debug(LIB_NAME_DRAW, data);
}
}
public static void error(final String data) {
if (PRINT_ERROR) {
Logger.error(LIB_NAME_DRAW, data);
}
}
public static void info(final String data) {
if (PRINT_INFO) {
Logger.info(LIB_NAME_DRAW, data);
}
}
public static void print(final String data) {
if (PRINT_PRINT) {
Logger.print(LIB_NAME_DRAW, data);
}
}
public static void todo(final String data) {
if (PRINT_TODO) {
Logger.todo(LIB_NAME_DRAW, data);
}
}
public static void verbose(final String data) {
if (PRINT_VERBOSE) {
Logger.verbose(LIB_NAME_DRAW, data);
}
}
public static void warning(final String data) {
if (PRINT_WARNING) {
Logger.warning(LIB_NAME_DRAW, data);
}
}
private Log() {}
}

View File

@ -4,7 +4,7 @@ import io.scenarium.logger.LogLevel;
import io.scenarium.logger.Logger;
public class Log {
private static final String LIB_NAME = "ege";
private static final String LIB_NAME = "phyligram";
private static final String LIB_NAME_DRAW = Logger.getDrawableName(LIB_NAME);
private static final boolean PRINT_CRITICAL = Logger.getNeedPrint(LIB_NAME, LogLevel.CRITICAL);
private static final boolean PRINT_ERROR = Logger.getNeedPrint(LIB_NAME, LogLevel.ERROR);