Skip to content

Commit

Permalink
Merge pull request #176 from JamesTKhan/ramp-brush
Browse files Browse the repository at this point in the history
Add Ramp brush and other fixes
  • Loading branch information
JamesTKhan authored May 16, 2023
2 parents 18a21e9 + 3798dfc commit 1d6dc69
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 33 deletions.
36 changes: 36 additions & 0 deletions commons/src/main/com/mbrlabs/mundus/commons/utils/MathUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

}
49 changes: 49 additions & 0 deletions commons/src/main/com/mbrlabs/mundus/commons/utils/Pools.java
Original file line number Diff line number Diff line change
@@ -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<Vector2> vector2Pool = new Pool<Vector2>() {
@Override
protected Vector2 newObject () {
return new Vector2();
}

@Override
protected void reset(Vector2 object) {
object.set(0, 0);
}
};

public final static Pool<Vector3> vector3Pool = new Pool<Vector3>() {
@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);
}
}
}
1 change: 1 addition & 0 deletions editor/CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -45,7 +46,7 @@

/**
* A Terrain Brush can modify the terrainAsset in various ways (BrushMode).
*
* <p>
* This includes the height of every vertex in the terrainAsset grid & according
* splatmap.
*
Expand All @@ -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
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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;

Expand Down Expand Up @@ -148,14 +151,23 @@ 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);
action = null;
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;
Expand All @@ -168,6 +180,8 @@ public void act() {
flatten();
} else if (mode == BrushMode.SMOOTH) {
smooth();
} else if (mode == BrushMode.RAMP) {
createRamp();
}

}
Expand All @@ -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();
Expand Down Expand Up @@ -209,20 +222,19 @@ 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;

// Get total height of all vertices within radius
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++;
Expand All @@ -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;
}
}
Expand All @@ -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);

Expand All @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -414,6 +482,7 @@ public boolean supportsMode(BrushMode mode) {
case FLATTEN:
case PAINT:
case SMOOTH:
case RAMP:
return true;
}

Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 1d6dc69

Please sign in to comment.