Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ramp brush and other fixes #176

Merged
merged 2 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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