Initaler Commit
This commit is contained in:
66
blight-editor/build.gradle
Normal file
66
blight-editor/build.gradle
Normal file
@@ -0,0 +1,66 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
}
|
||||
|
||||
group = 'de.blight'
|
||||
version = '0.1.0'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = '21'
|
||||
modules = ['javafx.controls', 'javafx.swing']
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'de.blight.editor.EditorLauncher'
|
||||
applicationDefaultJvmArgs = [
|
||||
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
||||
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
|
||||
'-Djava.library.path=${rootDir}/build/natives',
|
||||
]
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
jmeVersion = '3.7.0-stable'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
|
||||
}
|
||||
|
||||
tasks.register('extractNatives', Copy) {
|
||||
def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
|
||||
.resolvedArtifacts
|
||||
.findAll { it.name.contains('natives') }
|
||||
.collect { zipTree(it.file) }
|
||||
|
||||
from nativeConf
|
||||
into "${buildDir}/natives"
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
|
||||
run {
|
||||
dependsOn extractNatives
|
||||
workingDir = rootDir
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': application.mainClass
|
||||
}
|
||||
}
|
||||
BIN
blight-editor/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
blight-editor/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
blight-editor/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
blight-editor/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
249
blight-editor/gradlew
vendored
Executable file
249
blight-editor/gradlew
vendored
Executable file
@@ -0,0 +1,249 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
blight-editor/gradlew.bat
vendored
Normal file
92
blight-editor/gradlew.bat
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
blight-editor/settings.gradle
Normal file
1
blight-editor/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'blight-editor'
|
||||
310
blight-editor/src/main/java/de/blight/editor/EditorApp.java
Normal file
310
blight-editor/src/main/java/de/blight/editor/EditorApp.java
Normal file
@@ -0,0 +1,310 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class EditorApp extends Application {
|
||||
|
||||
// ── Viewport-Auflösung (JME3 rendert intern auf diese Größe) ────────────
|
||||
static final int VP_WIDTH = 1024;
|
||||
static final int VP_HEIGHT = 640;
|
||||
|
||||
// ── Asset-Verzeichnis ────────────────────────────────────────────────────
|
||||
private static final Path ASSET_ROOT = Paths.get("editor-assets");
|
||||
|
||||
private final SharedInput input = new SharedInput();
|
||||
private WritableImage jfxImage;
|
||||
private ImageView viewport;
|
||||
private Label statusLabel;
|
||||
|
||||
// Drag-Tracking für Kamerarotation (mittlere Taste)
|
||||
private double prevDragX, prevDragY;
|
||||
|
||||
// ── Asset-Tree-Items ─────────────────────────────────────────────────────
|
||||
private TreeItem<String> modelsNode;
|
||||
private TreeItem<String> texturesNode;
|
||||
private TreeItem<String> audioNode;
|
||||
|
||||
// ── JavaFX Entry-Point ───────────────────────────────────────────────────
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
// WritableImage für den JME3-Frame-Export
|
||||
jfxImage = new WritableImage(VP_WIDTH, VP_HEIGHT);
|
||||
|
||||
// JME3 starten (Hintergrund-Daemon-Thread)
|
||||
JmeEditorApp.launch(input, jfxImage, VP_WIDTH, VP_HEIGHT);
|
||||
|
||||
// UI zusammenbauen
|
||||
BorderPane root = new BorderPane();
|
||||
root.setTop(buildTop());
|
||||
root.setLeft(buildAssetPanel());
|
||||
root.setCenter(buildViewport());
|
||||
root.setBottom(buildStatusBar());
|
||||
|
||||
Scene scene = new Scene(root, 1280, 760);
|
||||
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
|
||||
scene.setOnKeyReleased(e -> handleKeyPress(e.getCode(), false));
|
||||
|
||||
stage.setTitle("Blight World Editor");
|
||||
stage.setScene(scene);
|
||||
stage.setMinWidth(900);
|
||||
stage.setMinHeight(600);
|
||||
stage.setOnCloseRequest(e -> Platform.exit());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
// ── Oberer Bereich: MenuBar + ToolBar ────────────────────────────────────
|
||||
|
||||
private VBox buildTop() {
|
||||
// Menüleiste
|
||||
MenuBar menuBar = new MenuBar();
|
||||
Menu fileMenu = new Menu("Datei");
|
||||
MenuItem newItem = new MenuItem("Neue Karte");
|
||||
MenuItem saveItem = new MenuItem("Speichern");
|
||||
fileMenu.getItems().addAll(newItem, saveItem);
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); // noop, Kamera reset via SharedInput wäre aufwändiger
|
||||
viewMenu.getItems().add(resetCam);
|
||||
menuBar.getMenus().addAll(fileMenu, viewMenu);
|
||||
|
||||
// Werkzeugleiste
|
||||
ToolBar toolBar = new ToolBar();
|
||||
Button heightTool = new Button("▲▼ Höhe");
|
||||
heightTool.setStyle("-fx-font-weight:bold;");
|
||||
heightTool.setTooltip(new Tooltip(
|
||||
"Linksklick: Terrain anheben\nRechtsklick: Terrain absenken"));
|
||||
|
||||
Separator sep1 = new Separator(Orientation.VERTICAL);
|
||||
|
||||
Label brushLabel = new Label("Pinselstärke:");
|
||||
Slider brushSlider = new Slider(0.1, 1.0, 1.0);
|
||||
brushSlider.setPrefWidth(100);
|
||||
brushSlider.setMajorTickUnit(0.5);
|
||||
brushSlider.setShowTickMarks(true);
|
||||
|
||||
Separator sep2 = new Separator(Orientation.VERTICAL);
|
||||
|
||||
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
|
||||
hint.setStyle("-fx-text-fill: #555;");
|
||||
|
||||
toolBar.getItems().addAll(heightTool, sep1, brushLabel, brushSlider, sep2, hint);
|
||||
|
||||
VBox top = new VBox(menuBar, toolBar);
|
||||
return top;
|
||||
}
|
||||
|
||||
// ── Linke Seite: Asset-Panel ─────────────────────────────────────────────
|
||||
|
||||
private VBox buildAssetPanel() {
|
||||
VBox panel = new VBox(6);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setPrefWidth(210);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 1 0 0;");
|
||||
|
||||
Label title = new Label("Assets");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
|
||||
// Baum
|
||||
TreeItem<String> root = new TreeItem<>("Projekt");
|
||||
root.setExpanded(true);
|
||||
modelsNode = new TreeItem<>("Models");
|
||||
texturesNode = new TreeItem<>("Texturen");
|
||||
audioNode = new TreeItem<>("Audio");
|
||||
root.getChildren().addAll(modelsNode, texturesNode, audioNode);
|
||||
|
||||
// Bestehende Assets laden
|
||||
loadAssetsInto(modelsNode, "models", ".j3o", ".obj", ".fbx", ".gltf", ".glb");
|
||||
loadAssetsInto(texturesNode, "textures", ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds");
|
||||
loadAssetsInto(audioNode, "audio", ".ogg", ".wav", ".mp3");
|
||||
|
||||
TreeView<String> tree = new TreeView<>(root);
|
||||
tree.setShowRoot(false);
|
||||
VBox.setVgrow(tree, Priority.ALWAYS);
|
||||
|
||||
// Kontextmenü im Baum (Datei öffnen im Dateimanager)
|
||||
ContextMenu ctx = new ContextMenu();
|
||||
MenuItem showItem = new MenuItem("Im Dateisystem anzeigen");
|
||||
ctx.getItems().add(showItem);
|
||||
tree.setContextMenu(ctx);
|
||||
|
||||
// Import-Button
|
||||
Button importBtn = new Button("⊕ Import…");
|
||||
importBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importBtn.setOnAction(e -> handleImport(tree.getScene().getWindow()));
|
||||
|
||||
panel.getChildren().addAll(title, tree, importBtn);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void loadAssetsInto(TreeItem<String> parent, String subDir, String... exts) {
|
||||
Path dir = ASSET_ROOT.resolve(subDir);
|
||||
if (!Files.exists(dir)) return;
|
||||
try (var stream = Files.list(dir)) {
|
||||
stream.filter(p -> {
|
||||
String name = p.getFileName().toString().toLowerCase();
|
||||
for (String ext : exts) if (name.endsWith(ext)) return true;
|
||||
return false;
|
||||
}).map(p -> new TreeItem<>(p.getFileName().toString()))
|
||||
.forEach(parent.getChildren()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private void handleImport(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Assets importieren");
|
||||
fc.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("Alle unterstützten Dateien",
|
||||
"*.j3o","*.obj","*.fbx","*.gltf","*.glb",
|
||||
"*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds",
|
||||
"*.ogg","*.wav","*.mp3"),
|
||||
new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.fbx","*.gltf","*.glb"),
|
||||
new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"),
|
||||
new FileChooser.ExtensionFilter("Audio", "*.ogg","*.wav","*.mp3")
|
||||
);
|
||||
var files = fc.showOpenMultipleDialog(owner);
|
||||
if (files == null) return;
|
||||
|
||||
for (File file : files) {
|
||||
String name = file.getName().toLowerCase();
|
||||
String subDir;
|
||||
TreeItem<String> parent;
|
||||
if (name.matches(".*\\.(j3o|obj|fbx|gltf|glb)")) {
|
||||
subDir = "models"; parent = modelsNode;
|
||||
} else if (name.matches(".*\\.(ogg|wav|mp3)")) {
|
||||
subDir = "audio"; parent = audioNode;
|
||||
} else {
|
||||
subDir = "textures"; parent = texturesNode;
|
||||
}
|
||||
try {
|
||||
Path dest = ASSET_ROOT.resolve(subDir);
|
||||
Files.createDirectories(dest);
|
||||
Path target = dest.resolve(file.getName());
|
||||
Files.copy(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
|
||||
parent.getChildren().add(new TreeItem<>(file.getName()));
|
||||
parent.setExpanded(true);
|
||||
setStatus("Importiert: " + file.getName());
|
||||
} catch (IOException ex) {
|
||||
setStatus("Fehler beim Import: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zentraler Bereich: JME3-Viewport ────────────────────────────────────
|
||||
|
||||
private StackPane buildViewport() {
|
||||
viewport = new ImageView(jfxImage);
|
||||
viewport.setPreserveRatio(false);
|
||||
viewport.setFocusTraversable(true);
|
||||
|
||||
StackPane pane = new StackPane(viewport);
|
||||
pane.setStyle("-fx-background-color: #1a1a2e;");
|
||||
|
||||
// ImageView auf Pane-Größe binden → JME3-Pixelskalierung aktualisieren
|
||||
pane.widthProperty().addListener((o, oldW, newW) -> {
|
||||
viewport.setFitWidth(newW.doubleValue());
|
||||
input.viewportScaleX = VP_WIDTH / newW.doubleValue();
|
||||
});
|
||||
pane.heightProperty().addListener((o, oldH, newH) -> {
|
||||
viewport.setFitHeight(newH.doubleValue());
|
||||
input.viewportScaleY = VP_HEIGHT / newH.doubleValue();
|
||||
});
|
||||
|
||||
// ── Maus-Events ──────────────────────────────────────────────────────
|
||||
viewport.setOnMousePressed(e -> {
|
||||
viewport.requestFocus();
|
||||
if (e.getButton() == MouseButton.MIDDLE) {
|
||||
prevDragX = e.getX();
|
||||
prevDragY = e.getY();
|
||||
}
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
submitEdit(e.getX(), e.getY(), +1);
|
||||
}
|
||||
if (e.getButton() == MouseButton.SECONDARY) {
|
||||
submitEdit(e.getX(), e.getY(), -1);
|
||||
}
|
||||
});
|
||||
|
||||
viewport.setOnMouseDragged(e -> {
|
||||
if (e.isMiddleButtonDown()) {
|
||||
double dx = e.getX() - prevDragX;
|
||||
double dy = e.getY() - prevDragY;
|
||||
input.addMouseDelta((int) dx, (int) dy);
|
||||
prevDragX = e.getX();
|
||||
prevDragY = e.getY();
|
||||
}
|
||||
if (e.isPrimaryButtonDown()) {
|
||||
submitEdit(e.getX(), e.getY(), +1);
|
||||
}
|
||||
if (e.isSecondaryButtonDown()) {
|
||||
submitEdit(e.getX(), e.getY(), -1);
|
||||
}
|
||||
});
|
||||
|
||||
viewport.setOnScroll(e -> {
|
||||
// Scrollen = Kamera vorwärts/rückwärts
|
||||
double delta = e.getDeltaY();
|
||||
input.forward = delta > 0;
|
||||
input.backward = delta < 0;
|
||||
// Nach kurzem Delay zurücksetzen (kein physischer Key-Release)
|
||||
javafx.animation.PauseTransition pause =
|
||||
new javafx.animation.PauseTransition(javafx.util.Duration.millis(150));
|
||||
pause.setOnFinished(ev -> { input.forward = false; input.backward = false; });
|
||||
pause.play();
|
||||
});
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
private void submitEdit(double x, double y, int action) {
|
||||
input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
||||
}
|
||||
|
||||
// ── Statusleiste ─────────────────────────────────────────────────────────
|
||||
|
||||
private HBox buildStatusBar() {
|
||||
statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag: Drehen");
|
||||
statusLabel.setPadding(new Insets(3, 8, 3, 8));
|
||||
statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #333;");
|
||||
HBox bar = new HBox(statusLabel);
|
||||
bar.setStyle("-fx-background-color: #e8e8e8; -fx-border-color: #bbb; -fx-border-width: 1 0 0 0;");
|
||||
return bar;
|
||||
}
|
||||
|
||||
private void setStatus(String msg) {
|
||||
Platform.runLater(() -> statusLabel.setText(msg));
|
||||
}
|
||||
|
||||
// ── Tastatur-Handling ────────────────────────────────────────────────────
|
||||
|
||||
private void handleKeyPress(KeyCode code, boolean pressed) {
|
||||
switch (code) {
|
||||
case W -> input.forward = pressed;
|
||||
case S -> input.backward = pressed;
|
||||
case A -> input.left = pressed;
|
||||
case D -> input.right = pressed;
|
||||
case Q -> input.up = pressed;
|
||||
case E -> input.down = pressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.blight.editor;
|
||||
|
||||
/**
|
||||
* Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
|
||||
* aufgelöst wird, bevor Application.launch() aufgerufen wird.
|
||||
*/
|
||||
public class EditorLauncher {
|
||||
public static void main(String[] args) {
|
||||
EditorApp.main(args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import com.jme3.post.SceneProcessor;
|
||||
import com.jme3.profile.AppProfiler;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.Renderer;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* SceneProcessor: liest nach jedem JME3-Frame den Framebuffer aus und
|
||||
* schreibt die Pixel (RGBA→ARGB, Y-gespiegelt) in ein JavaFX WritableImage.
|
||||
*/
|
||||
public class FrameTransfer implements SceneProcessor {
|
||||
|
||||
private final WritableImage image;
|
||||
private final PixelWriter pw;
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
private Renderer renderer;
|
||||
private ByteBuffer cpuBuf;
|
||||
private byte[] snapshot;
|
||||
private int[] argbRow; // Zeile für JavaFX-PixelWriter
|
||||
|
||||
private final AtomicBoolean jfxBusy = new AtomicBoolean(false);
|
||||
|
||||
public FrameTransfer(WritableImage image) {
|
||||
this.image = image;
|
||||
this.pw = image.getPixelWriter();
|
||||
this.width = (int) image.getWidth();
|
||||
this.height = (int) image.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(RenderManager rm, ViewPort vp) {
|
||||
this.renderer = rm.getRenderer();
|
||||
this.cpuBuf = ByteBuffer.allocateDirect(width * height * 4);
|
||||
this.snapshot = new byte[width * height * 4];
|
||||
this.argbRow = new int[width];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postFrame(FrameBuffer out) {
|
||||
if (!jfxBusy.compareAndSet(false, true)) return;
|
||||
|
||||
cpuBuf.clear();
|
||||
renderer.readFrameBuffer(out, cpuBuf);
|
||||
cpuBuf.rewind();
|
||||
cpuBuf.get(snapshot);
|
||||
|
||||
final byte[] pixels = snapshot.clone();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
// GL: Y=0 unten → JavaFX: Y=0 oben + RGBA → 0xFFRRGGBB (int ARGB)
|
||||
PixelFormat<IntBuffer> fmt = PixelFormat.getIntArgbInstance();
|
||||
for (int y = 0; y < height; y++) {
|
||||
int srcBase = (height - 1 - y) * width * 4;
|
||||
for (int x = 0; x < width; x++) {
|
||||
int r = pixels[srcBase + x * 4 ] & 0xFF;
|
||||
int g = pixels[srcBase + x * 4 + 1] & 0xFF;
|
||||
int b = pixels[srcBase + x * 4 + 2] & 0xFF;
|
||||
argbRow[x] = 0xFF000000 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
pw.setPixels(0, y, width, 1, fmt, argbRow, 0, width);
|
||||
}
|
||||
} finally {
|
||||
jfxBusy.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Pflichtmethoden ──────────────────────────────────────────────────────
|
||||
|
||||
@Override public boolean isInitialized() { return renderer != null; }
|
||||
@Override public void reshape(ViewPort vp, int w, int h) {}
|
||||
@Override public void preFrame(float tpf) {}
|
||||
@Override public void postQueue(RenderQueue rq) {}
|
||||
@Override public void cleanup() {}
|
||||
@Override public void setProfiler(AppProfiler profiler) {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.system.AppSettings;
|
||||
import com.jme3.system.JmeContext;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
public class JmeEditorApp extends SimpleApplication {
|
||||
|
||||
private final SharedInput input;
|
||||
private final WritableImage jfxImage;
|
||||
|
||||
public JmeEditorApp(SharedInput input, WritableImage jfxImage) {
|
||||
this.input = input;
|
||||
this.jfxImage = jfxImage;
|
||||
}
|
||||
|
||||
/** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
|
||||
public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
|
||||
int vpWidth, int vpHeight) {
|
||||
JmeEditorApp app = new JmeEditorApp(input, jfxImage);
|
||||
|
||||
AppSettings settings = new AppSettings(true);
|
||||
settings.setTitle("Blight Editor – JME3");
|
||||
settings.setResolution(vpWidth, vpHeight);
|
||||
settings.setRenderer(AppSettings.LWJGL_OPENGL32);
|
||||
settings.setAudioRenderer(null);
|
||||
settings.setSamples(4);
|
||||
|
||||
app.setSettings(settings);
|
||||
app.setShowSettings(false);
|
||||
app.setPauseOnLostFocus(false);
|
||||
|
||||
Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleInitApp() {
|
||||
flyCam.setEnabled(false);
|
||||
|
||||
// Frame-Export in das JavaFX-WritableImage
|
||||
viewPort.addProcessor(new FrameTransfer(jfxImage));
|
||||
|
||||
stateManager.attach(new TerrainEditorState(input));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleUpdate(float tpf) {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
|
||||
public class SharedInput {
|
||||
|
||||
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
||||
public volatile boolean forward, backward, left, right, up, down;
|
||||
|
||||
// ── Kamerarotation (Maus-Drag mit mittlerer Taste) ───────────────────────
|
||||
private final AtomicInteger mouseDxAccum = new AtomicInteger();
|
||||
private final AtomicInteger mouseDyAccum = new AtomicInteger();
|
||||
|
||||
public void addMouseDelta(int dx, int dy) {
|
||||
mouseDxAccum.addAndGet(dx);
|
||||
mouseDyAccum.addAndGet(dy);
|
||||
}
|
||||
|
||||
/** Gibt akkumulierten Maus-Delta zurück und setzt ihn zurück. */
|
||||
public int[] consumeMouseDelta() {
|
||||
return new int[]{ mouseDxAccum.getAndSet(0), mouseDyAccum.getAndSet(0) };
|
||||
}
|
||||
|
||||
// ── Terrain-Edits ────────────────────────────────────────────────────────
|
||||
public record TerrainEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<TerrainEdit> editQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
|
||||
public volatile double viewportScaleX = 1.0;
|
||||
public volatile double viewportScaleY = 1.0;
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
// ── Konstanten ──────────────────────────────────────────────────────────
|
||||
private static final int V = 17; // Vertices pro Achse (16 Zellen)
|
||||
private static final float BRUSH_RADIUS = 2.0f; // Meter
|
||||
private static final float BRUSH_DELTA = 0.25f; // Höhenänderung pro Klick
|
||||
private static final float CAM_SPEED = 12f;
|
||||
private static final float MOUSE_SENS = 0.003f;
|
||||
|
||||
// ── Zustand ─────────────────────────────────────────────────────────────
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private final SharedInput input;
|
||||
|
||||
private final float[] heights = new float[V * V]; // flaches Array, Index = z*V+x
|
||||
private Mesh terrainMesh;
|
||||
private Geometry terrainGeo;
|
||||
|
||||
// Kamera-Euler-Winkel
|
||||
private float camYaw = 0f;
|
||||
private float camPitch = -0.4f;
|
||||
private final Vector3f camPos = new Vector3f(0, 14, 22);
|
||||
|
||||
public TerrainEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.cam = app.getCamera();
|
||||
this.assets = app.getAssetManager();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
buildScene();
|
||||
applyCameraTransform();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
rootNode.detachAllChildren();
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) {}
|
||||
|
||||
// ── Szene aufbauen ───────────────────────────────────────────────────────
|
||||
|
||||
private void buildScene() {
|
||||
// Licht
|
||||
DirectionalLight sun = new DirectionalLight();
|
||||
sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
|
||||
sun.setColor(new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
|
||||
rootNode.addLight(sun);
|
||||
|
||||
AmbientLight ambient = new AmbientLight(new ColorRGBA(0.35f, 0.38f, 0.45f, 1f));
|
||||
rootNode.addLight(ambient);
|
||||
|
||||
// Terrain
|
||||
terrainGeo = buildTerrainGeometry();
|
||||
rootNode.attachChild(terrainGeo);
|
||||
|
||||
// Wasser bei Y = 0
|
||||
rootNode.attachChild(buildWater());
|
||||
|
||||
// Raster-Linien auf dem Terrain (als dünne Linien-Node wäre komplex, Gitter via Grid-Overlay)
|
||||
// Einfache Grid-Markierung: ein flaches transparentes Quad mit Wireframe
|
||||
rootNode.attachChild(buildGridOverlay());
|
||||
|
||||
// Himmel (einfacher Hintergrund-Farbverlauf über Viewport-BG-Farbe)
|
||||
app.getViewPort().setBackgroundColor(new ColorRGBA(0.45f, 0.60f, 0.80f, 1f));
|
||||
}
|
||||
|
||||
private Geometry buildTerrainGeometry() {
|
||||
terrainMesh = new Mesh();
|
||||
FloatBuffer posBuf = BufferUtils.createFloatBuffer(V * V * 3);
|
||||
FloatBuffer normBuf = BufferUtils.createFloatBuffer(V * V * 3);
|
||||
FloatBuffer texBuf = BufferUtils.createFloatBuffer(V * V * 2);
|
||||
IntBuffer idxBuf = BufferUtils.createIntBuffer((V - 1) * (V - 1) * 6);
|
||||
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
posBuf.put(x).put(heights[z * V + x]).put(z);
|
||||
normBuf.put(0).put(1).put(0);
|
||||
texBuf.put(x / 16f).put(z / 16f);
|
||||
}
|
||||
}
|
||||
for (int z = 0; z < V - 1; z++) {
|
||||
for (int x = 0; x < V - 1; x++) {
|
||||
int bl = z * V + x, br = bl + 1;
|
||||
int tl = bl + V, tr = tl + 1;
|
||||
idxBuf.put(bl).put(tr).put(br);
|
||||
idxBuf.put(bl).put(tl).put(tr);
|
||||
}
|
||||
}
|
||||
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.TexCoord, 2, texBuf);
|
||||
terrainMesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
|
||||
terrainMesh.updateBound();
|
||||
|
||||
Geometry geo = new Geometry("terrain", terrainMesh);
|
||||
geo.setLocalTranslation(-8, 0, -8); // Terrain zentriert bei Ursprung
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
|
||||
mat.setBoolean("UseMaterialColors", true);
|
||||
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
|
||||
mat.setColor("Ambient", new ColorRGBA(0.12f, 0.28f, 0.08f, 1f));
|
||||
mat.setColor("Specular", ColorRGBA.Black);
|
||||
mat.setFloat("Shininess", 0f);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
private Geometry buildWater() {
|
||||
Geometry water = new Geometry("water", new Quad(16, 16));
|
||||
water.rotate(-FastMath.HALF_PI, 0, 0);
|
||||
water.setLocalTranslation(-8, 0.01f, 8); // leicht über Y=0 damit kein Z-Fighting
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.05f, 0.25f, 0.70f, 0.55f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
water.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
water.setMaterial(mat);
|
||||
return water;
|
||||
}
|
||||
|
||||
private Geometry buildGridOverlay() {
|
||||
// Einfaches Wireframe-Duplikat des Terrains als Gitter
|
||||
Geometry grid = new Geometry("grid", terrainMesh);
|
||||
grid.setLocalTranslation(-8, 0.02f, -8);
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0.25f));
|
||||
mat.getAdditionalRenderState().setWireframe(true);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
grid.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
grid.setMaterial(mat);
|
||||
return grid;
|
||||
}
|
||||
|
||||
// ── Update-Schleife ──────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
updateCamera(tpf);
|
||||
processEdits();
|
||||
}
|
||||
|
||||
private void updateCamera(float tpf) {
|
||||
// Rotation aus akkumuliertem Maus-Delta
|
||||
int[] delta = input.consumeMouseDelta();
|
||||
if (delta[0] != 0 || delta[1] != 0) {
|
||||
camYaw -= delta[0] * MOUSE_SENS;
|
||||
camPitch -= delta[1] * MOUSE_SENS;
|
||||
camPitch = FastMath.clamp(camPitch,
|
||||
-FastMath.HALF_PI + 0.05f,
|
||||
FastMath.HALF_PI - 0.05f);
|
||||
}
|
||||
|
||||
applyCameraTransform();
|
||||
|
||||
// Bewegung in Kamera-Vorwärtsrichtung projiziert auf XZ-Ebene
|
||||
float speed = CAM_SPEED * tpf;
|
||||
Vector3f fwd = cam.getDirection().clone().setY(0);
|
||||
if (fwd.lengthSquared() > 0.001f) fwd.normalizeLocal();
|
||||
Vector3f lft = cam.getLeft().clone().setY(0);
|
||||
if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
|
||||
|
||||
if (input.forward) camPos.addLocal(fwd.mult(speed));
|
||||
if (input.backward) camPos.subtractLocal(fwd.mult(speed));
|
||||
if (input.left) camPos.addLocal(lft.mult(speed));
|
||||
if (input.right) camPos.subtractLocal(lft.mult(speed));
|
||||
if (input.up) camPos.y += speed;
|
||||
if (input.down) camPos.y -= speed;
|
||||
|
||||
cam.setLocation(camPos);
|
||||
}
|
||||
|
||||
private void applyCameraTransform() {
|
||||
Quaternion yawQ = new Quaternion().fromAngleAxis(camYaw, Vector3f.UNIT_Y);
|
||||
Quaternion pitchQ = new Quaternion().fromAngleAxis(camPitch, Vector3f.UNIT_X);
|
||||
cam.setRotation(yawQ.mult(pitchQ));
|
||||
cam.setLocation(camPos);
|
||||
}
|
||||
|
||||
private void processEdits() {
|
||||
SharedInput.TerrainEdit edit;
|
||||
while ((edit = input.editQueue.poll()) != null) {
|
||||
// JavaFX-Koordinaten → JME3-Screen-Koordinaten (Y spiegeln)
|
||||
float jmeX = (float)(edit.screenX() * input.viewportScaleX);
|
||||
float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
|
||||
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Vector3f dir = far.subtract(near).normalizeLocal();
|
||||
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrainGeo.collideWith(new com.jme3.math.Ray(near, dir), hits);
|
||||
|
||||
if (hits.size() > 0) {
|
||||
Vector3f contact = hits.getClosestCollision().getContactPoint();
|
||||
modifyHeight(contact, edit.action() * BRUSH_DELTA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Höhen-Werkzeug ───────────────────────────────────────────────────────
|
||||
|
||||
private void modifyHeight(Vector3f worldContact, float delta) {
|
||||
// Terrain-Geometrie ist bei (-8, 0, -8), Vertices bei (0..16, h, 0..16)
|
||||
float localX = worldContact.x + 8;
|
||||
float localZ = worldContact.z + 8;
|
||||
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
float dx = x - localX;
|
||||
float dz = z - localZ;
|
||||
float dist = FastMath.sqrt(dx * dx + dz * dz);
|
||||
if (dist < BRUSH_RADIUS) {
|
||||
float falloff = 1f - dist / BRUSH_RADIUS;
|
||||
heights[z * V + x] += delta * falloff;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTerrainMesh();
|
||||
}
|
||||
|
||||
private void updateTerrainMesh() {
|
||||
FloatBuffer posBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
FloatBuffer normBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||
|
||||
posBuf.rewind();
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
posBuf.put(x).put(heights[z * V + x]).put(z);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalen per finite differences
|
||||
normBuf.rewind();
|
||||
for (int z = 0; z < V; z++) {
|
||||
for (int x = 0; x < V; x++) {
|
||||
float hL = x > 0 ? heights[z * V + (x - 1)] : heights[z * V + x];
|
||||
float hR = x < V-1 ? heights[z * V + (x + 1)] : heights[z * V + x];
|
||||
float hD = z > 0 ? heights[(z - 1) * V + x] : heights[z * V + x];
|
||||
float hU = z < V-1 ? heights[(z + 1) * V + x] : heights[z * V + x];
|
||||
float nx = -(hR - hL);
|
||||
float ny = 2.0f;
|
||||
float nz = -(hU - hD);
|
||||
float len = FastMath.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
normBuf.put(nx / len).put(ny / len).put(nz / len);
|
||||
}
|
||||
}
|
||||
|
||||
terrainMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
||||
terrainMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
|
||||
terrainMesh.updateBound();
|
||||
terrainGeo.updateModelBound();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user