From 4d73d8ac4810ccf319be13729f97948fa07663d1 Mon Sep 17 00:00:00 2001 From: Edouard DUPIN Date: Mon, 21 Feb 2022 18:17:18 +0100 Subject: [PATCH] sdcsc --- .classpath | 2 +- samples/resources/lowPoly/data/palette_1.json | 3 + samples/src/module-info.java | 2 + .../sample/atriasoft/ege/mapFactory/Appl.java | 69 +++ .../atriasoft/ege/mapFactory/ApplScene.java | 122 ++++ .../atriasoft/ege/mapFactory/EgeScene.java | 125 ++++ .../atriasoft/ege/mapFactory/Ground.java | 33 ++ .../sample/atriasoft/ege/mapFactory/Log.java | 39 ++ .../atriasoft/ege/mapFactory/MainWindows.java | 51 ++ .../ege/mapFactory/MapFactoryMain.java | 22 + src/org/atriasoft/arkon/ColorMap.java | 53 ++ src/org/atriasoft/arkon/Config.java | 62 ++ src/org/atriasoft/arkon/Create.java | 229 ++++++++ src/org/atriasoft/arkon/Map.java | 369 ++++++++++++ src/org/atriasoft/arkon/Mesh.java | 9 + src/org/atriasoft/arkon/Renderer.java | 553 ++++++++++++++++++ src/org/atriasoft/arkon/TriangleMesh.java | 5 + src/org/atriasoft/arkon/draggable.v2.java | 127 ++++ src/org/atriasoft/arkon/embed.html | 95 +++ src/org/atriasoft/arkon/generate-points.java | 56 ++ src/org/atriasoft/arkon/geometry.java | 268 +++++++++ src/org/atriasoft/arkon/mapgen4.java | 186 ++++++ src/org/atriasoft/arkon/mesh.java | 172 ++++++ src/org/atriasoft/arkon/painting.java | 223 +++++++ src/org/atriasoft/arkon/worker.java | 67 +++ .../atriasoft/eagle/CirclePointGenerator.java | 37 ++ src/org/atriasoft/eagle/SpaceGenerator.java | 18 + src/org/atriasoft/eagle/generator/Bridge.java | 14 + src/org/atriasoft/eagle/generator/Land.java | 27 + .../eagle/generator/LocalGenerator.java | 8 + .../atriasoft/eagle/generator/Montain.java | 14 + src/org/atriasoft/eagle/generator/River.java | 14 + src/org/atriasoft/eagle/generator/Road.java | 14 + src/org/atriasoft/eagle/internal/Log.java | 68 +++ src/org/atriasoft/eagle/model/MapChunk.java | 49 ++ src/org/atriasoft/eagle/model/MetaMap.java | 60 ++ .../atriasoft/eagle/model/TypeElement.java | 27 + src/org/atriasoft/eagle/model/VoxelMap.java | 6 + .../ege/components/ComponentMesh.java | 5 + .../atriasoft/ege/engines/EnginePhysics.java | 2 +- src/org/atriasoft/garoux/internal/Log.java | 68 +++ src/org/atriasoft/phyligram/internal/Log.java | 2 +- 42 files changed, 3372 insertions(+), 3 deletions(-) create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/Appl.java create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/ApplScene.java create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/EgeScene.java create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/Ground.java create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/Log.java create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/MainWindows.java create mode 100644 samples/src/sample/atriasoft/ege/mapFactory/MapFactoryMain.java create mode 100644 src/org/atriasoft/arkon/ColorMap.java create mode 100644 src/org/atriasoft/arkon/Config.java create mode 100644 src/org/atriasoft/arkon/Create.java create mode 100644 src/org/atriasoft/arkon/Map.java create mode 100644 src/org/atriasoft/arkon/Mesh.java create mode 100644 src/org/atriasoft/arkon/Renderer.java create mode 100644 src/org/atriasoft/arkon/TriangleMesh.java create mode 100644 src/org/atriasoft/arkon/draggable.v2.java create mode 100644 src/org/atriasoft/arkon/embed.html create mode 100644 src/org/atriasoft/arkon/generate-points.java create mode 100644 src/org/atriasoft/arkon/geometry.java create mode 100644 src/org/atriasoft/arkon/mapgen4.java create mode 100644 src/org/atriasoft/arkon/mesh.java create mode 100644 src/org/atriasoft/arkon/painting.java create mode 100644 src/org/atriasoft/arkon/worker.java create mode 100644 src/org/atriasoft/eagle/CirclePointGenerator.java create mode 100644 src/org/atriasoft/eagle/SpaceGenerator.java create mode 100644 src/org/atriasoft/eagle/generator/Bridge.java create mode 100644 src/org/atriasoft/eagle/generator/Land.java create mode 100644 src/org/atriasoft/eagle/generator/LocalGenerator.java create mode 100644 src/org/atriasoft/eagle/generator/Montain.java create mode 100644 src/org/atriasoft/eagle/generator/River.java create mode 100644 src/org/atriasoft/eagle/generator/Road.java create mode 100644 src/org/atriasoft/eagle/internal/Log.java create mode 100644 src/org/atriasoft/eagle/model/MapChunk.java create mode 100644 src/org/atriasoft/eagle/model/MetaMap.java create mode 100644 src/org/atriasoft/eagle/model/TypeElement.java create mode 100644 src/org/atriasoft/eagle/model/VoxelMap.java create mode 100644 src/org/atriasoft/garoux/internal/Log.java diff --git a/.classpath b/.classpath index baf231a..fd8a754 100644 --- a/.classpath +++ b/.classpath @@ -1,6 +1,6 @@ - + diff --git a/samples/resources/lowPoly/data/palette_1.json b/samples/resources/lowPoly/data/palette_1.json index 4e937f5..02caeaf 100644 --- a/samples/resources/lowPoly/data/palette_1.json +++ b/samples/resources/lowPoly/data/palette_1.json @@ -24,6 +24,9 @@ }, "trunk_2":{ "Kd":"0.057805 0.039546 0.013702" + }, + "grass_1":{ + "Kd":"0.057805 1.0 0.013702" } } } \ No newline at end of file diff --git a/samples/src/module-info.java b/samples/src/module-info.java index 13a303b..8a80f95 100644 --- a/samples/src/module-info.java +++ b/samples/src/module-info.java @@ -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 } diff --git a/samples/src/sample/atriasoft/ege/mapFactory/Appl.java b/samples/src/sample/atriasoft/ege/mapFactory/Appl.java new file mode 100644 index 0000000..5db4aa9 --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/Appl.java @@ -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 ]"); + } + +} \ No newline at end of file diff --git a/samples/src/sample/atriasoft/ege/mapFactory/ApplScene.java b/samples/src/sample/atriasoft/ege/mapFactory/ApplScene.java new file mode 100644 index 0000000..ab0898f --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/ApplScene.java @@ -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); +} +*/ diff --git a/samples/src/sample/atriasoft/ege/mapFactory/EgeScene.java b/samples/src/sample/atriasoft/ege/mapFactory/EgeScene.java new file mode 100644 index 0000000..cbc5df0 --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/EgeScene.java @@ -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(); + } + +} \ No newline at end of file diff --git a/samples/src/sample/atriasoft/ege/mapFactory/Ground.java b/samples/src/sample/atriasoft/ege/mapFactory/Ground.java new file mode 100644 index 0000000..1ca70b3 --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/Ground.java @@ -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)); + } + +} diff --git a/samples/src/sample/atriasoft/ege/mapFactory/Log.java b/samples/src/sample/atriasoft/ege/mapFactory/Log.java new file mode 100644 index 0000000..768dc2a --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/Log.java @@ -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() {} +} diff --git a/samples/src/sample/atriasoft/ege/mapFactory/MainWindows.java b/samples/src/sample/atriasoft/ege/mapFactory/MainWindows.java new file mode 100644 index 0000000..4bba2ff --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/MainWindows.java @@ -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); + } + +} \ No newline at end of file diff --git a/samples/src/sample/atriasoft/ege/mapFactory/MapFactoryMain.java b/samples/src/sample/atriasoft/ege/mapFactory/MapFactoryMain.java new file mode 100644 index 0000000..3fb7072 --- /dev/null +++ b/samples/src/sample/atriasoft/ege/mapFactory/MapFactoryMain.java @@ -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() {} +} diff --git a/src/org/atriasoft/arkon/ColorMap.java b/src/org/atriasoft/arkon/ColorMap.java new file mode 100644 index 0000000..a704b35 --- /dev/null +++ b/src/org/atriasoft/arkon/ColorMap.java @@ -0,0 +1,53 @@ +package org.atriasoft.arkon; +/* + * From http://www.redblobgames.com/x/1742-webgl-mapgen2/ + * Copyright 2017 Red Blob Games + * License: Apache v2.0 + */ + +/* 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; + } +} diff --git a/src/org/atriasoft/arkon/Config.java b/src/org/atriasoft/arkon/Config.java new file mode 100644 index 0000000..4eafd62 --- /dev/null +++ b/src/org/atriasoft/arkon/Config.java @@ -0,0 +1,62 @@ +package org.atriasoft.arkon; +/* + * From https://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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); + +} diff --git a/src/org/atriasoft/arkon/Create.java b/src/org/atriasoft/arkon/Create.java new file mode 100644 index 0000000..cddcdef --- /dev/null +++ b/src/org/atriasoft/arkon/Create.java @@ -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 P→Q and + // the angle P→R 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); + } + } +} diff --git a/src/org/atriasoft/arkon/Map.java b/src/org/atriasoft/arkon/Map.java new file mode 100644 index 0000000..4d08a07 --- /dev/null +++ b/src/org/atriasoft/arkon/Map.java @@ -0,0 +1,369 @@ +package org.atriasoft.arkon; + +// @ts-check +/* + * From https://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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 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]; + } + } + } +} diff --git a/src/org/atriasoft/arkon/Mesh.java b/src/org/atriasoft/arkon/Mesh.java new file mode 100644 index 0000000..3a9a760 --- /dev/null +++ b/src/org/atriasoft/arkon/Mesh.java @@ -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 */ +} diff --git a/src/org/atriasoft/arkon/Renderer.java b/src/org/atriasoft/arkon/Renderer.java new file mode 100644 index 0000000..d2c2160 --- /dev/null +++ b/src/org/atriasoft/arkon/Renderer.java @@ -0,0 +1,553 @@ +package org.atriasoft.arkon; +/* + * From http://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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). + * */ + 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; diff --git a/src/org/atriasoft/arkon/TriangleMesh.java b/src/org/atriasoft/arkon/TriangleMesh.java new file mode 100644 index 0000000..a0dd86f --- /dev/null +++ b/src/org/atriasoft/arkon/TriangleMesh.java @@ -0,0 +1,5 @@ +package org.atriasoft.arkon; + +public class TriangleMesh { + +} diff --git a/src/org/atriasoft/arkon/draggable.v2.java b/src/org/atriasoft/arkon/draggable.v2.java new file mode 100644 index 0000000..bf15f1c --- /dev/null +++ b/src/org/atriasoft/arkon/draggable.v2.java @@ -0,0 +1,127 @@ +/* + * From https://www.redblobgames.com/x/1845-draggable/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + */ +'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 + // + 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) {} +} diff --git a/src/org/atriasoft/arkon/embed.html b/src/org/atriasoft/arkon/embed.html new file mode 100644 index 0000000..a1bcf00 --- /dev/null +++ b/src/org/atriasoft/arkon/embed.html @@ -0,0 +1,95 @@ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+
+ + +
+ diff --git a/src/org/atriasoft/arkon/generate-points.java b/src/org/atriasoft/arkon/generate-points.java new file mode 100644 index 0000000..952dbd9 --- /dev/null +++ b/src/org/atriasoft/arkon/generate-points.java @@ -0,0 +1,56 @@ +/* + * From https://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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)); diff --git a/src/org/atriasoft/arkon/geometry.java b/src/org/atriasoft/arkon/geometry.java new file mode 100644 index 0000000..d26a6a9 --- /dev/null +++ b/src/org/atriasoft/arkon/geometry.java @@ -0,0 +1,268 @@ +/* + * From http://www.redblobgames.com/maps/magpen4/ + * Copyright 2017 Red Blob Games + * License: Apache v2.0 + */ + +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}; diff --git a/src/org/atriasoft/arkon/mapgen4.java b/src/org/atriasoft/arkon/mapgen4.java new file mode 100644 index 0000000..c93c193 --- /dev/null +++ b/src/org/atriasoft/arkon/mapgen4.java @@ -0,0 +1,186 @@ +/* + * From http://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * + * 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); diff --git a/src/org/atriasoft/arkon/mesh.java b/src/org/atriasoft/arkon/mesh.java new file mode 100644 index 0000000..f4cd9ee --- /dev/null +++ b/src/org/atriasoft/arkon/mesh.java @@ -0,0 +1,172 @@ +package org.atriasoft.arkon; +/* + * From https://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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}; +} diff --git a/src/org/atriasoft/arkon/painting.java b/src/org/atriasoft/arkon/painting.java new file mode 100644 index 0000000..39c9400 --- /dev/null +++ b/src/org/atriasoft/arkon/painting.java @@ -0,0 +1,223 @@ +/* + * From https://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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; diff --git a/src/org/atriasoft/arkon/worker.java b/src/org/atriasoft/arkon/worker.java new file mode 100644 index 0000000..be010ad --- /dev/null +++ b/src/org/atriasoft/arkon/worker.java @@ -0,0 +1,67 @@ +/* + * From https://www.redblobgames.com/maps/mapgen4/ + * Copyright 2018 Red Blob Games + * License: Apache v2.0 + * + * 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); + diff --git a/src/org/atriasoft/eagle/CirclePointGenerator.java b/src/org/atriasoft/eagle/CirclePointGenerator.java new file mode 100644 index 0000000..01e053c --- /dev/null +++ b/src/org/atriasoft/eagle/CirclePointGenerator.java @@ -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; + } + +} diff --git a/src/org/atriasoft/eagle/SpaceGenerator.java b/src/org/atriasoft/eagle/SpaceGenerator.java new file mode 100644 index 0000000..d30eb7d --- /dev/null +++ b/src/org/atriasoft/eagle/SpaceGenerator.java @@ -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) { + + } +} diff --git a/src/org/atriasoft/eagle/generator/Bridge.java b/src/org/atriasoft/eagle/generator/Bridge.java new file mode 100644 index 0000000..f27de35 --- /dev/null +++ b/src/org/atriasoft/eagle/generator/Bridge.java @@ -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 + + } + +} diff --git a/src/org/atriasoft/eagle/generator/Land.java b/src/org/atriasoft/eagle/generator/Land.java new file mode 100644 index 0000000..c3c342b --- /dev/null +++ b/src/org/atriasoft/eagle/generator/Land.java @@ -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/ diff --git a/src/org/atriasoft/eagle/generator/LocalGenerator.java b/src/org/atriasoft/eagle/generator/LocalGenerator.java new file mode 100644 index 0000000..3e9f251 --- /dev/null +++ b/src/org/atriasoft/eagle/generator/LocalGenerator.java @@ -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); +} diff --git a/src/org/atriasoft/eagle/generator/Montain.java b/src/org/atriasoft/eagle/generator/Montain.java new file mode 100644 index 0000000..20f8705 --- /dev/null +++ b/src/org/atriasoft/eagle/generator/Montain.java @@ -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 + + } + +} diff --git a/src/org/atriasoft/eagle/generator/River.java b/src/org/atriasoft/eagle/generator/River.java new file mode 100644 index 0000000..e508a03 --- /dev/null +++ b/src/org/atriasoft/eagle/generator/River.java @@ -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 + + } + +} diff --git a/src/org/atriasoft/eagle/generator/Road.java b/src/org/atriasoft/eagle/generator/Road.java new file mode 100644 index 0000000..5a01aa2 --- /dev/null +++ b/src/org/atriasoft/eagle/generator/Road.java @@ -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 + + } + +} diff --git a/src/org/atriasoft/eagle/internal/Log.java b/src/org/atriasoft/eagle/internal/Log.java new file mode 100644 index 0000000..92c007c --- /dev/null +++ b/src/org/atriasoft/eagle/internal/Log.java @@ -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() {} + +} diff --git a/src/org/atriasoft/eagle/model/MapChunk.java b/src/org/atriasoft/eagle/model/MapChunk.java new file mode 100644 index 0000000..c51f0e0 --- /dev/null +++ b/src/org/atriasoft/eagle/model/MapChunk.java @@ -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; + } + +} diff --git a/src/org/atriasoft/eagle/model/MetaMap.java b/src/org/atriasoft/eagle/model/MetaMap.java new file mode 100644 index 0000000..c93aee9 --- /dev/null +++ b/src/org/atriasoft/eagle/model/MetaMap.java @@ -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 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()); + } +} diff --git a/src/org/atriasoft/eagle/model/TypeElement.java b/src/org/atriasoft/eagle/model/TypeElement.java new file mode 100644 index 0000000..dddd5ea --- /dev/null +++ b/src/org/atriasoft/eagle/model/TypeElement.java @@ -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 +} diff --git a/src/org/atriasoft/eagle/model/VoxelMap.java b/src/org/atriasoft/eagle/model/VoxelMap.java new file mode 100644 index 0000000..5967dbe --- /dev/null +++ b/src/org/atriasoft/eagle/model/VoxelMap.java @@ -0,0 +1,6 @@ +package org.atriasoft.eagle.model; + +public class VoxelMap { + public TypeElement element; + public float height; +} diff --git a/src/org/atriasoft/ege/components/ComponentMesh.java b/src/org/atriasoft/ege/components/ComponentMesh.java index bb845a5..e64e9d0 100644 --- a/src/org/atriasoft/ege/components/ComponentMesh.java +++ b/src/org/atriasoft/ege/components/ComponentMesh.java @@ -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; diff --git a/src/org/atriasoft/ege/engines/EnginePhysics.java b/src/org/atriasoft/ege/engines/EnginePhysics.java index 00a4795..1f29b7a 100644 --- a/src/org/atriasoft/ege/engines/EnginePhysics.java +++ b/src/org/atriasoft/ege/engines/EnginePhysics.java @@ -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... diff --git a/src/org/atriasoft/garoux/internal/Log.java b/src/org/atriasoft/garoux/internal/Log.java new file mode 100644 index 0000000..817fb2f --- /dev/null +++ b/src/org/atriasoft/garoux/internal/Log.java @@ -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() {} + +} diff --git a/src/org/atriasoft/phyligram/internal/Log.java b/src/org/atriasoft/phyligram/internal/Log.java index 43eda0c..b501292 100644 --- a/src/org/atriasoft/phyligram/internal/Log.java +++ b/src/org/atriasoft/phyligram/internal/Log.java @@ -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);