diff --git a/commons/src/main/com/mbrlabs/mundus/commons/utils/MathUtils.java b/commons/src/main/com/mbrlabs/mundus/commons/utils/MathUtils.java index 26142ad2a..281059575 100644 --- a/commons/src/main/com/mbrlabs/mundus/commons/utils/MathUtils.java +++ b/commons/src/main/com/mbrlabs/mundus/commons/utils/MathUtils.java @@ -86,4 +86,40 @@ public static boolean isPowerOfTwo(int number) { return (number & (number - 1)) == 0; } + /** + * Find the nearest point on a line to a given point. + * @param lineStart start of the line + * @param lineEnd end of the line + * @param point the point + * @param out populated with the nearest point on the line + */ + public static void findNearestPointOnLine(Vector2 lineStart, Vector2 lineEnd, Vector2 point, Vector2 out) { + Vector2 lineDirection = Pools.vector2Pool.obtain().set(lineEnd).sub(lineStart); + + // Calculate the length of the line. + float lineLength = lineDirection.len(); + lineDirection.nor(); + + // lineStart to point + Vector2 toPoint = Pools.vector2Pool.obtain().set(point).sub(lineStart); + float projectedLength = lineDirection.dot(toPoint); + + // Calculate the coordinates of the projected point. + Vector2 projectedPoint = new Vector2(lineDirection).scl(toPoint.dot(lineDirection)); + + Pools.vector2Pool.free(lineDirection); + Pools.vector2Pool.free(toPoint); + + if (projectedLength < 0) { + out.set(lineStart); + } + else if (projectedLength > lineLength) { + out.set(lineEnd); + } + else { + // If the projected point lies on the line segment, return the projected point. + out.set(lineStart).add(projectedPoint); + } + } + } diff --git a/commons/src/main/com/mbrlabs/mundus/commons/utils/Pools.java b/commons/src/main/com/mbrlabs/mundus/commons/utils/Pools.java new file mode 100644 index 000000000..938aec937 --- /dev/null +++ b/commons/src/main/com/mbrlabs/mundus/commons/utils/Pools.java @@ -0,0 +1,49 @@ +package com.mbrlabs.mundus.commons.utils; + +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.utils.Pool; + +/** + * Pooling of commonly used objects. + * + * @author JamesTKhan + * @version May 14, 2023 + */ +public class Pools { + + public final static Pool vector2Pool = new Pool() { + @Override + protected Vector2 newObject () { + return new Vector2(); + } + + @Override + protected void reset(Vector2 object) { + object.set(0, 0); + } + }; + + public final static Pool vector3Pool = new Pool() { + @Override + protected Vector3 newObject () { + return new Vector3(); + } + + @Override + protected void reset(Vector3 object) { + object.set(0,0,0); + } + }; + + + /** + * Convenience method, free array of objects + * @param objects objects to free + */ + public static void free(Vector2... objects ) { + for (Vector2 object : objects) { + vector2Pool.free(object); + } + } +} diff --git a/editor/CHANGES b/editor/CHANGES index 1bb397367..a8d14877a 100644 --- a/editor/CHANGES +++ b/editor/CHANGES @@ -8,6 +8,7 @@ - Added args4j for command line parsing - Added command line option to enable GL30 - Added command line option to enable fullscreen +- Added Pools.java class to manage object pools - Update gradle to 7.5.1 - Updated editor to use MSAA by default, disable by command line - Terrain Assets can now be reused in the same scene and other scenes diff --git a/editor/src/main/com/mbrlabs/mundus/editor/tools/brushes/TerrainBrush.java b/editor/src/main/com/mbrlabs/mundus/editor/tools/brushes/TerrainBrush.java index 05bf66676..c67e5b7f0 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/tools/brushes/TerrainBrush.java +++ b/editor/src/main/com/mbrlabs/mundus/editor/tools/brushes/TerrainBrush.java @@ -22,7 +22,6 @@ import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.math.Interpolation; -import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; @@ -32,6 +31,8 @@ import com.mbrlabs.mundus.commons.terrain.SplatMap; import com.mbrlabs.mundus.commons.terrain.SplatTexture; import com.mbrlabs.mundus.commons.terrain.Terrain; +import com.mbrlabs.mundus.commons.utils.MathUtils; +import com.mbrlabs.mundus.commons.utils.Pools; import com.mbrlabs.mundus.editor.Mundus; import com.mbrlabs.mundus.editor.core.project.ProjectManager; import com.mbrlabs.mundus.editor.events.GlobalBrushSettingsChangedEvent; @@ -45,7 +46,7 @@ /** * A Terrain Brush can modify the terrainAsset in various ways (BrushMode). - * + *

* This includes the height of every vertex in the terrainAsset grid & according * splatmap. * @@ -65,7 +66,9 @@ public enum BrushMode { /** Smooths terrain based on average height within radius */ SMOOTH, /** Paints on the splatmap of the terrainAsset. */ - PAINT + PAINT, + /** Create a ramp between two points. */ + RAMP } /** @@ -98,6 +101,7 @@ public ModeNotSupportedException(String message) { } // used for calculations + protected static final Vector3 rampEndPoint = new Vector3(); protected static final Vector2 c = new Vector2(); protected static final Vector2 p = new Vector2(); protected static final Vector2 v = new Vector2(); @@ -110,7 +114,6 @@ public ModeNotSupportedException(String message) { // all brushes share the some common settings private static final GlobalBrushSettingsChangedEvent brushSettingsChangedEvent = new GlobalBrushSettingsChangedEvent(); private static float strength = 0.5f; - private static float smoothingFactor = 0.005f; private static float heightSample = 0f; private static SplatTexture.Channel paintChannel; @@ -148,7 +151,7 @@ public void act() { if (terrainAsset == null) return; // sample height - if (action == BrushAction.SECONDARY && mode == BrushMode.FLATTEN) { + if (action == BrushAction.SECONDARY && (mode == BrushMode.FLATTEN)) { // brushPos is in world coords, convert to terrains local height by negating world height heightSample = brushPos.y - getTerrainPosition(tVec0).y; UI.INSTANCE.getToaster().success("Height Sampled: " + heightSample); @@ -156,6 +159,15 @@ public void act() { return; } + // Sample end point for ramp + if (action == BrushAction.SECONDARY && (mode == BrushMode.RAMP)) { + // brushPos is in world coords, convert to terrains local height by negating world height + rampEndPoint.set(brushPos.x, brushPos.y - getTerrainPosition(tVec0).y, brushPos.z); + UI.INSTANCE.getToaster().success("End Point Sampled: " + rampEndPoint); + action = null; + return; + } + // only act if mouse has been moved if (!mouseMoved) return; mouseMoved = false; @@ -168,6 +180,8 @@ public void act() { flatten(); } else if (mode == BrushMode.SMOOTH) { smooth(); + } else if (mode == BrushMode.RAMP) { + createRamp(); } } @@ -178,8 +192,7 @@ private void paint() { if (sm == null) return; // should convert world position to terrain local position - tVec1.set(brushPos); - tVec1.mul(tmpMatrix.set(terrainComponent.getModelInstance().transform).inv()); + getBrushLocalPosition(tVec1); final float splatX = (tVec1.x / (float) terrain.terrainWidth) * sm.getWidth(); final float splatY = (tVec1.z / (float) terrain.terrainDepth) * sm.getHeight(); @@ -209,6 +222,9 @@ private void paint() { private void smooth() { Terrain terrain = terrainAsset.getTerrain(); + // should convert world position to terrain local position + getBrushLocalPosition(tVec2); + int weights = 0; float totalHeights = 0; @@ -216,13 +232,9 @@ private void smooth() { for (int x = 0; x < terrain.vertexResolution; x++) { for (int z = 0; z < terrain.vertexResolution; z++) { final Vector3 vertexPos = terrain.getVertexPosition(tVec0, x, z); - - // should convert world position to terrain local position - tVec2.set(brushPos); - tVec2.mul(tmpMatrix.set(terrainComponent.getModelInstance().transform).inv()); - tVec2.y = vertexPos.y; float distance = vertexPos.dst(tVec2); + tVec2.y = vertexPos.y; if (distance <= radius) { totalHeights += vertexPos.y; weights++; @@ -236,19 +248,15 @@ private void smooth() { for (int x = 0; x < terrain.vertexResolution; x++) { for (int z = 0; z < terrain.vertexResolution; z++) { final Vector3 vertexPos = terrain.getVertexPosition(tVec0, x, z); - - // should convert world position to terrain local position - tVec2.set(brushPos); - tVec2.mul(tmpMatrix.set(terrainComponent.getModelInstance().transform).inv()); tVec2.y = vertexPos.y; float distance = vertexPos.dst(tVec2); if (distance <= radius) { final int index = z * terrain.vertexResolution + x; float heightAtIndex = terrain.heightData[index]; - // Take radius - distance to get a falloff effect - float lerpProgress = MathUtils.clamp((radius - distance) * (strength * smoothingFactor), 0.0f, 1.0f); - float smoothedHeight = Interpolation.smooth2.apply(heightAtIndex, averageHeight, lerpProgress); + // Determine how much to interpolate based on distance from radius + float elevation = getValueOfBrushPixmap(tVec2.x, tVec2.z, vertexPos.x, vertexPos.z, radius); + float smoothedHeight = Interpolation.smooth2.apply(heightAtIndex, averageHeight, elevation * strength); terrain.heightData[index] = smoothedHeight; } } @@ -260,15 +268,76 @@ private void smooth() { Mundus.INSTANCE.postEvent(new TerrainVerticesChangedEvent(terrainComponent)); } + private void createRamp() { + Terrain terrain = terrainAsset.getTerrain(); + + // tvec2 represents the start (brush) point of the ramp + getBrushLocalPosition(tVec2); + tVec2.y = brushPos.y - getTerrainPosition(tVec0).y; + Vector3 startPoint = tVec2; + + // Calculate the direction and length of the ramp + Vector3 rampDirection = tVec1.set(startPoint).sub(rampEndPoint).nor(); + float rampLength = startPoint.dst(rampEndPoint); + + // Half width for distance checking + float rampWidth = radius * 2f; + float halfWidth = rampWidth * 0.5f; + + Vector3 toVertex = Pools.vector3Pool.obtain(); + Vector2 nearestPoint = Pools.vector2Pool.obtain(); + Vector2 vertexPos2 = Pools.vector2Pool.obtain(); + Vector2 startPoint2 = Pools.vector2Pool.obtain().set(startPoint.x, startPoint.z); + Vector2 rampEnd2 = Pools.vector2Pool.obtain().set(rampEndPoint.x, rampEndPoint.z);; + + for (int i = 0; i < 1; i++) { + + for (int x = 0; x < terrain.vertexResolution; x++) { + for (int z = 0; z < terrain.vertexResolution; z++) { + final Vector3 vertexPos = terrain.getVertexPosition(tVec0, x, z); + toVertex.set(vertexPos).sub(TerrainBrush.rampEndPoint); + + vertexPos2.set(vertexPos.x, vertexPos.z); + MathUtils.findNearestPointOnLine(rampEnd2, startPoint2, vertexPos2, nearestPoint); + + float distanceToRampLine = vertexPos2.sub(nearestPoint).len(); + + if (distanceToRampLine <= halfWidth) { + // Calculate the height from the ramp line + float projectedLength = rampDirection.dot(toVertex); + float slope = (startPoint.y - TerrainBrush.rampEndPoint.y) / rampLength; + float rampHeight = TerrainBrush.rampEndPoint.y + projectedLength * slope; + + // Interpolate the height based on the distance from the center of the ramp + float interpolationFactor = 1.0f - (distanceToRampLine / halfWidth); + float interpolatedHeight = Interpolation.smooth2.apply(vertexPos.y, rampHeight, interpolationFactor * strength); + + // Set the height of the vertex + final int index = z * terrain.vertexResolution + x; + terrain.heightData[index] = interpolatedHeight; + } + } + } + } + + Pools.free(nearestPoint, vertexPos2, startPoint2, rampEnd2); + Pools.vector3Pool.free(toVertex); + + terrain.update(); + terrainHeightModified = true; + getProjectManager().current().assetManager.addModifiedAsset(terrainAsset); + Mundus.INSTANCE.postEvent(new TerrainVerticesChangedEvent(terrainComponent)); + } + private void flatten() { Terrain terrain = terrainAsset.getTerrain(); + + // should convert world position to terrain local position + getBrushLocalPosition(tVec2); + for (int x = 0; x < terrain.vertexResolution; x++) { for (int z = 0; z < terrain.vertexResolution; z++) { final Vector3 vertexPos = terrain.getVertexPosition(tVec0, x, z); - - // should convert world position to terrain local position - tVec2.set(brushPos); - tVec2.mul(tmpMatrix.set(terrainComponent.getModelInstance().transform).inv()); tVec2.y = vertexPos.y; float distance = vertexPos.dst(tVec2); @@ -281,11 +350,11 @@ private void flatten() { final float elevation = getValueOfBrushPixmap(tVec2.x, tVec2.z, vertexPos.x, vertexPos.z, radius); // current height is lower than sample - if(heightSample > terrain.heightData[index]) { + if (heightSample > terrain.heightData[index]) { terrain.heightData[index] += elevation * strength; } else { float newHeight = terrain.heightData[index] - elevation * strength; - if(diff > Math.abs(newHeight) || terrain.heightData[index] > heightSample) { + if (diff > Math.abs(newHeight) || terrain.heightData[index] > heightSample) { terrain.heightData[index] = newHeight; } @@ -303,15 +372,14 @@ private void flatten() { private void raiseLower(BrushAction action) { Terrain terrain = terrainAsset.getTerrain(); + + // should convert world position to terrain local position + getBrushLocalPosition(tVec2); + float dir = (action == BrushAction.PRIMARY) ? 1 : -1; for (int x = 0; x < terrain.vertexResolution; x++) { for (int z = 0; z < terrain.vertexResolution; z++) { final Vector3 vertexPos = terrain.getVertexPosition(tVec0, x, z); - - // should convert world position to terrain local position - tVec2.set(brushPos); - tVec2.mul(tmpMatrix.set(terrainComponent.getModelInstance().transform).inv()); - // for the dist calc, we do not want to factor in global Y height tVec2.y = vertexPos.y; float distance = vertexPos.dst(tVec2); @@ -414,6 +482,7 @@ public boolean supportsMode(BrushMode mode) { case FLATTEN: case PAINT: case SMOOTH: + case RAMP: return true; } @@ -425,6 +494,11 @@ private Vector3 getTerrainPosition(Vector3 vector3) { return vector3; } + private void getBrushLocalPosition(Vector3 value) { + value.set(brushPos); + value.mul(tmpMatrix.set(terrainComponent.getModelInstance().transform).inv()); + } + @Override public void render() { // rendering of the brush is done in the editor terrain shader @@ -483,7 +557,7 @@ public boolean touchDown(int screenX, int screenY, int pointer, int button) { action = null; } - if (mode == BrushMode.FLATTEN || mode == BrushMode.RAISE_LOWER || mode == BrushMode.SMOOTH) { + if (mode == BrushMode.FLATTEN || mode == BrushMode.RAISE_LOWER || mode == BrushMode.SMOOTH || mode == BrushMode.RAMP) { heightCommand = new TerrainHeightCommand(terrainAsset.getTerrain()); heightCommand.setHeightDataBefore(terrainAsset.getTerrain().heightData); } else if (mode == BrushMode.PAINT) { diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainBrushGrid.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainBrushGrid.kt index 9046c608b..7538cea27 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainBrushGrid.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainBrushGrid.kt @@ -21,6 +21,7 @@ import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener import com.badlogic.gdx.scenes.scene2d.utils.ClickListener import com.badlogic.gdx.utils.Align +import com.badlogic.gdx.utils.Array import com.kotcrab.vis.ui.layout.GridGroup import com.kotcrab.vis.ui.util.dialog.Dialogs import com.kotcrab.vis.ui.widget.VisLabel @@ -30,6 +31,7 @@ import com.mbrlabs.mundus.editor.events.GlobalBrushSettingsChangedEvent import com.mbrlabs.mundus.editor.events.ToolActivatedEvent import com.mbrlabs.mundus.editor.events.ToolDeactivatedEvent import com.mbrlabs.mundus.editor.tools.ToolManager +import com.mbrlabs.mundus.editor.tools.brushes.CircleBrush import com.mbrlabs.mundus.editor.tools.brushes.TerrainBrush import com.mbrlabs.mundus.editor.ui.UI import com.mbrlabs.mundus.editor.ui.widgets.FaTextButton @@ -43,6 +45,7 @@ class TerrainBrushGrid(private val parent: TerrainComponentWidget, private val brushMode: TerrainBrush.BrushMode) : VisTable(), GlobalBrushSettingsChangedEvent.GlobalBrushSettingsChangedListener, ToolDeactivatedEvent.ToolDeactivatedEventListener, ToolActivatedEvent.ToolActivatedEventListener { + private val brushItems = Array() private val grid = GridGroup(40f, 0f) private val strengthSlider = ImprovedSlider(0f, 1f, 0.1f) @@ -59,7 +62,9 @@ class TerrainBrushGrid(private val parent: TerrainComponentWidget, // grid for (brush in toolManager.terrainBrushes) { - grid.addActor(BrushItem(brush)) + val brushItem = BrushItem(brush) + brushItems.add(brushItem) + grid.addActor(brushItem) } brushGridContainerTable.add(grid).expand().fill().row() @@ -94,6 +99,22 @@ class TerrainBrushGrid(private val parent: TerrainComponentWidget, } + fun hideBrushes() { + for (brushItem in brushItems) { + if (brushItem.brush is CircleBrush) continue + brushItem.isVisible = false + } + } + + fun showCircleBrush() { + for (brushItem in brushItems) { + if (brushItem.brush is CircleBrush) { + brushItem.isVisible = true + break + } + } + } + override fun onSettingsChanged(event: GlobalBrushSettingsChangedEvent) { strengthSlider.value = TerrainBrush.getStrength() } @@ -108,7 +129,7 @@ class TerrainBrushGrid(private val parent: TerrainComponentWidget, /** */ - private inner class BrushItem(brush: TerrainBrush) : VisTable() { + private inner class BrushItem(val brush: TerrainBrush) : VisTable() { init { val button = FaTextButton(brush.iconFont) button.name = brush.name diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainComponentWidget.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainComponentWidget.kt index a9b79fc97..1fcbc830e 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainComponentWidget.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainComponentWidget.kt @@ -39,6 +39,7 @@ class TerrainComponentWidget(terrainComponent: TerrainComponent) : private val raiseLowerTab = TerrainUpDownTab(this) private val flattenTab = TerrainFlattenTab(this) private val smoothTab = TerrainSmoothTab(this) + private val rampTab = TerrainRampTab(this) private val paintTab = TerrainPaintTab(this) private val genTab = TerrainGenTab(this) private val settingsTab = TerrainSettingsTab(this) @@ -49,6 +50,7 @@ class TerrainComponentWidget(terrainComponent: TerrainComponent) : tabbedPane.add(raiseLowerTab) tabbedPane.add(flattenTab) tabbedPane.add(smoothTab) + tabbedPane.add(rampTab) tabbedPane.add(paintTab) tabbedPane.add(genTab) tabbedPane.add(settingsTab) diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainRampTab.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainRampTab.kt new file mode 100644 index 000000000..d4cb97271 --- /dev/null +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/terrain/TerrainRampTab.kt @@ -0,0 +1,34 @@ +package com.mbrlabs.mundus.editor.ui.modules.inspector.components.terrain + +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Align +import com.kotcrab.vis.ui.widget.VisLabel +import com.kotcrab.vis.ui.widget.VisTable +import com.mbrlabs.mundus.editor.tools.brushes.TerrainBrush + +/** + * @author JamesTKhan + * @version May 10, 2023 + */ +class TerrainRampTab(parent: TerrainComponentWidget) : BaseBrushTab(parent, TerrainBrush.BrushMode.RAMP) { + + private val table = VisTable() + + init { + table.align(Align.left) + table.add(VisLabel("Hold shift to sample the ramp end point")).center().row() + + table.add(terrainBrushGrid).expand().fill().row() + terrainBrushGrid.hideBrushes() + terrainBrushGrid.showCircleBrush() + } + + override fun getTabTitle(): String { + return "Ramp" + } + + override fun getContentTable(): Table { + return table + } + +} \ No newline at end of file