Arbeiten aus dem URlaub

This commit is contained in:
2026-05-19 12:55:05 +02:00
parent b8a0234ad2
commit 4f48834e2c
403 changed files with 23402 additions and 6389 deletions

2
blight-editor/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/.gradle/
/build/

View File

@@ -1,66 +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
}
}
// group / version / java / repositories kommen vom Root-Build.
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
javafx {
version = '26'
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=${buildDir}/natives",
]
}
ext {
jmeVersion = '3.9.0-stable'
}
dependencies {
implementation project(':blight-common')
implementation project(':blight-assets')
implementation project(':ez-tree-jme')
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
}
sourceSets {
main {
resources {
srcDirs = ['src/main/resources']
}
}
}
run {
dependsOn extractNatives
workingDir = rootDir // gemeinsames Arbeitsverzeichnis = Projekt-Root
}
jar {
manifest {
attributes 'Main-Class': application.mainClass
}
}

View File

@@ -1,7 +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
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

498
blight-editor/gradlew vendored
View File

@@ -1,249 +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" "$@"
#!/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" "$@"

View File

@@ -1,92 +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
@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

View File

@@ -1 +1,11 @@
rootProject.name = 'blight-editor'
rootProject.name = 'blight-editor'
// Sibling-Projekte einbinden.
// Funktioniert sowohl wenn dieses Projekt direkt in Eclipse importiert wird
// als auch im übergeordneten Multi-Projekt-Build (dort werden diese Zeilen ignoriert,
// da die Projekte bereits vom Root-settings.gradle bekannt sind).
include 'blight-common'
project(':blight-common').projectDir = new File(settingsDir, '../blight-common')
include 'blight-assets'
project(':blight-assets').projectDir = new File(settingsDir, '../blight-assets')

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +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);
}
}
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);
}
}

View File

@@ -22,7 +22,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class FrameTransfer implements SceneProcessor {
private final WritableImage image;
private final PixelWriter pw;
private final int width;
private final int height;
@@ -30,12 +29,11 @@ public class FrameTransfer implements SceneProcessor {
private Renderer renderer;
private ByteBuffer cpuBuf;
private byte[] snapshot;
private int[] argbRow; // Zeile für JavaFX-PixelWriter
private int[] argbBuf; // gesamtes Bild für einmaligen bulk-Write
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();
@@ -46,34 +44,39 @@ public class FrameTransfer implements SceneProcessor {
this.renderer = rm.getRenderer();
this.cpuBuf = ByteBuffer.allocateDirect(width * height * 4);
this.snapshot = new byte[width * height * 4];
this.argbRow = new int[width];
this.argbBuf = new int[width * height];
}
@Override
public void postFrame(FrameBuffer out) {
if (!jfxBusy.compareAndSet(false, true)) return;
cpuBuf.clear();
renderer.readFrameBuffer(out, cpuBuf);
cpuBuf.rewind();
cpuBuf.get(snapshot);
try {
cpuBuf.clear();
renderer.readFrameBuffer(out, cpuBuf);
cpuBuf.rewind();
cpuBuf.get(snapshot);
} catch (Exception e) {
jfxBusy.set(false);
return;
}
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;
int dstBase = y * width;
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;
argbBuf[dstBase + x] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
pw.setPixels(0, y, width, 1, fmt, argbRow, 0, width);
}
pw.setPixels(0, 0, width, height, fmt, argbBuf, 0, width);
} finally {
jfxBusy.set(false);
}

View File

@@ -1,53 +1,83 @@
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) {}
}
package de.blight.editor;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeContext;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture2D;
import de.blight.editor.state.EzTreeState;
import de.blight.editor.state.PalmGeneratorState;
import de.blight.editor.state.SceneObjectState;
import de.blight.editor.state.TerrainEditorState;
import de.blight.editor.state.TreeGeneratorState;
import javafx.scene.image.WritableImage;
public class JmeEditorApp extends SimpleApplication {
private final SharedInput input;
private final WritableImage jfxImage;
private final int vpWidth;
private final int vpHeight;
public JmeEditorApp(SharedInput input, WritableImage jfxImage, int vpWidth, int vpHeight) {
this.input = input;
this.jfxImage = jfxImage;
this.vpWidth = vpWidth;
this.vpHeight = vpHeight;
}
/** 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, vpWidth, vpHeight);
AppSettings settings = new AppSettings(true);
settings.setTitle("Blight Editor JME3");
settings.setResolution(vpWidth, vpHeight);
settings.setRenderer(AppSettings.LWJGL_OPENGL32);
settings.setAudioRenderer(null);
settings.setSamples(1);
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);
// editor-assets/ im AssetManager registrieren, damit Texturen und Modelle
// aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis).
try {
assetManager.registerLocator(
java.nio.file.Paths.get("editor-assets").toAbsolutePath().toString(),
FileLocator.class);
} catch (Exception ignored) {}
// Texture2D-Attachment: readFrameBuffer() funktioniert nur mit Texture, nicht Renderbuffer
Texture2D colorTex = new Texture2D(vpWidth, vpHeight, Image.Format.RGBA8);
FrameBuffer fb = new FrameBuffer(vpWidth, vpHeight, 1);
fb.setDepthBuffer(Image.Format.Depth);
fb.setColorTexture(colorTex);
viewPort.setOutputFrameBuffer(fb);
// Frame-Export in das JavaFX-WritableImage
viewPort.addProcessor(new FrameTransfer(jfxImage));
stateManager.attach(new SceneObjectState(input));
stateManager.attach(new TerrainEditorState(input));
stateManager.attach(new TreeGeneratorState(input));
stateManager.attach(new EzTreeState(input));
stateManager.attach(new PalmGeneratorState(input));
}
@Override
public void simpleUpdate(float tpf) {}
}

View File

@@ -1,11 +1,33 @@
package de.blight.editor;
import de.blight.editor.tool.EditorTool;
import de.blight.editor.tool.GrassTool;
import de.blight.editor.tool.HeightTool;
import de.blight.editor.tool.HoleTool;
import de.blight.editor.tool.TextureTool;
import de.blight.editor.tool.UpperHeightTool;
import de.blight.editor.tree.PalmOptions;
import de.blight.editor.tree.TreeParams;
import javafx.scene.image.WritableImage;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
public class SharedInput {
// ── Aktive Tools ─────────────────────────────────────────────────────────
public final HeightTool heightTool = new HeightTool();
public final UpperHeightTool upperHeightTool = new UpperHeightTool();
public final HoleTool holeTool = new HoleTool();
public final GrassTool grassTool = new GrassTool();
public final TextureTool textureTool = new TextureTool();
public volatile EditorTool activeTool = heightTool;
// ── Aktive Ebene: 0=Basis-Terrain, 1=Obere Schicht, 2=Höhlen, 3=Gras, 4=Textur ──
public volatile int activeLayer = 0;
public volatile boolean upperLayerVisible = true;
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
public volatile boolean forward, backward, left, right, up, down;
@@ -18,7 +40,6 @@ public class SharedInput {
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) };
}
@@ -27,7 +48,119 @@ public class SharedInput {
public record TerrainEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<TerrainEdit> editQueue = new ConcurrentLinkedQueue<>();
// ── Upper-Layer-Edits ─────────────────────────────────────────────────────
public record UpperLayerEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<UpperLayerEdit> upperLayerEditQueue = new ConcurrentLinkedQueue<>();
// ── Gras-Edits ────────────────────────────────────────────────────────────
/** action +1 = Dichte erhöhen (Linksklick), -1 = Dichte verringern (Rechtsklick). */
public record GrassEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<GrassEdit> grassEditQueue = new ConcurrentLinkedQueue<>();
// ── Textur-Edits ─────────────────────────────────────────────────────────
/** action +1 = selektierte Textur malen, -1 = auf Gras zurücksetzen. */
public record TextureEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<TextureEdit> textureEditQueue = new ConcurrentLinkedQueue<>();
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
public volatile double viewportScaleX = 1.0;
public volatile double viewportScaleY = 1.0;
// ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
public volatile float mouseScreenX = -1f;
public volatile float mouseScreenY = -1f;
// ── Speichern ─────────────────────────────────────────────────────────────
public volatile boolean saveRequested = false;
public volatile String saveStatusMsg = null;
// ── Vorschau-Viewport (gemeinsam für Baum-Generator & EZ-Tree) ───────────
public volatile float treePreviewRotY = 0f; // Yaw-Winkel in Grad
public volatile float treePreviewRotX = 30f; // Elevation in Grad [5, 80]
public volatile float treePreviewZoom = 1.0f; // Zoom-Faktor [0.25, 4.0]
/** Gewünschte Framebuffer-Größe von JavaFX gesetzt, von JME3 gelesen. */
public volatile int treePreviewW = 1024;
public volatile int treePreviewH = 1024;
public volatile String treeGenStatusMsg = null;
public volatile boolean refreshAssets = false;
/**
* Aktuelles Vorschau-Bild. JME3 ersetzt die Referenz bei Größenänderung;
* treePreviewResized signalisiert JavaFX, die ImageView zu aktualisieren.
*/
public volatile WritableImage treePreviewImage = new WritableImage(1024, 1024);
public volatile boolean treePreviewResized = false;
// ── Baum-Generator ───────────────────────────────────────────────────────
public record TreeGenRequest(TreeParams params, boolean exportAfter, String exportName) {}
public final ConcurrentLinkedQueue<TreeGenRequest> treeGenQueue = new ConcurrentLinkedQueue<>();
// ── EZ-Tree-Generator ─────────────────────────────────────────────────────
public record EzTreeGenRequest(de.blight.eztree.TreeOptions options, boolean exportAfter, String exportName) {}
public final ConcurrentLinkedQueue<EzTreeGenRequest> ezTreeGenQueue = new ConcurrentLinkedQueue<>();
// ── Palmen-Generator ──────────────────────────────────────────────────────
public record PalmGenRequest(PalmOptions options, boolean exportAfter, String exportName) {}
public final ConcurrentLinkedQueue<PalmGenRequest> palmGenQueue = new ConcurrentLinkedQueue<>();
// ── Objekt-Werkzeug ──────────────────────────────────────────────────────
/** activeLayer==5 → Objekte platzieren */
public static final int LAYER_OBJECTS = 5;
/** activeLayer==6 → Objekte bearbeiten (Selektion + Gizmo) */
public static final int LAYER_OBJECTS_EDIT = 6;
/** Klick im Viewport: Objekt auswählen oder am Terrain-Treffpunkt platzieren. */
public record ObjectClick(float screenX, float screenY, boolean rightButton) {}
public final ConcurrentLinkedQueue<ObjectClick> objectClickQueue = new ConcurrentLinkedQueue<>();
/**
* Rohe Maus-Delta beim Drag im Objekt-Modus.
* JME3 projiziert dx/dy je nach aktivem Gizmo-Pfeil auf die Weltachse.
*/
public record ObjectDrag(float dx, float dy) {}
public final ConcurrentLinkedQueue<ObjectDrag> objectDragQueue = new ConcurrentLinkedQueue<>();
/** Wird von JME3 gesetzt wenn ein neues Objekt oder eine neue Selektion vorliegt. */
public volatile String selectedObjectInfo = null; // "modelPath|solid|x|y|z|rotY|scale"
public volatile boolean objectSelectionChanged = false;
/** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */
public volatile boolean objectJustPlaced = false;
/** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu editor-assets/). */
public volatile String pendingModelPath = null;
/** JavaFX → JME3: Solid-Flag des selektierten Objekts ändern. */
public volatile Boolean pendingSolidChange = null;
// ── Mesh-Erstellung ───────────────────────────────────────────────────────
/**
* Form: "Box" | "Kugel" | "Zylinder" | "Ebene"
* sizeX: Breite (Box/Ebene) oder Radius (Kugel/Zylinder)
* sizeY: Höhe (Box/Zylinder)
* sizeZ: Tiefe (Box/Ebene)
* matType: "Unshaded" | "Phong"
* texturePath: relativ zu editor-assets/ oder null
*/
public record MeshCreateRequest(
String form,
float sizeX, float sizeY, float sizeZ,
String matType,
float r, float g, float b, float a,
String texturePath,
boolean wireframe,
String name
) {}
public final ConcurrentLinkedQueue<MeshCreateRequest> meshCreateQueue =
new ConcurrentLinkedQueue<>();
// ── Modell-Konvertierung ──────────────────────────────────────────────────
/**
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.
* assetPath : Pfad relativ zu editor-assets/ (z. B. "models/tree.obj")
* destJ3o : absoluter Ziel-Pfad der .j3o-Datei
* srcToDelete: absoluter Pfad der Original-Datei (wird nach Konvertierung gelöscht)
*/
public record ModelConvertRequest(String assetPath, java.nio.file.Path destJ3o,
java.nio.file.Path srcToDelete) {}
public final ConcurrentLinkedQueue<ModelConvertRequest> modelConvertQueue =
new ConcurrentLinkedQueue<>();
}

View File

@@ -0,0 +1,15 @@
package de.blight.editor.object;
public final class GrassObject extends PlacedObject {
/** Klingenhöhe in Welteinheiten. */
public final float height;
public GrassObject(float worldX, float worldZ, float groundY, float height) {
super(worldX, worldZ, groundY);
this.height = height;
}
@Override
public String getType() { return "grass"; }
}

View File

@@ -0,0 +1,29 @@
package de.blight.editor.object;
/**
* Abstrakte Basisklasse für alle Objekte, die auf der Karte platziert werden.
* Die horizontale Position (worldX, worldZ) ist unveränderlich.
* groundY folgt dem Terrain wenn dieses angehoben/abgesenkt wird.
*/
public abstract class PlacedObject {
protected final float worldX;
protected final float worldZ;
protected float groundY; // Geländeoberfläche am Standort wird bei Terrain-Edit angepasst
protected PlacedObject(float worldX, float worldZ, float groundY) {
this.worldX = worldX;
this.worldZ = worldZ;
this.groundY = groundY;
}
/** Eindeutiger Typ-Name (z. B. "grass"). */
public abstract String getType();
public float getWorldX() { return worldX; }
public float getWorldZ() { return worldZ; }
public float getGroundY() { return groundY; }
/** Wird aufgerufen wenn sich das Terrain an dieser Stelle hebt oder senkt. */
public void adjustGroundY(float delta) { groundY += delta; }
}

View File

@@ -0,0 +1,43 @@
package de.blight.editor.object;
/**
* Ein platziertes 3D-Objekt auf der Karte.
* X/Z sind jetzt veränderlich (per Gizmo verschiebbar).
*/
public class SceneObject extends PlacedObject {
private float worldXMut;
private float worldZMut;
private float rotY; // Y-Achsen-Rotation in Radiant
private float scale;
public boolean solid; // Charakter-Kollision
public String modelPath; // relativ zu editor-assets/
public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
boolean solid) {
super(worldX, worldZ, groundY);
this.worldXMut = worldX;
this.worldZMut = worldZ;
this.rotY = 0f;
this.scale = 1f;
this.solid = solid;
this.modelPath = modelPath;
}
@Override public String getType() { return "sceneObject"; }
@Override public float getWorldX() { return worldXMut; }
@Override public float getWorldZ() { return worldZMut; }
public float getRotY() { return rotY; }
public float getScale() { return scale; }
public void translate(float dx, float dy, float dz) {
worldXMut += dx;
groundY += dy;
worldZMut += dz;
}
public void rotateY(float deltaRad) { rotY += deltaRad; }
public void setScale(float s) { scale = s; }
}

View File

@@ -0,0 +1,349 @@
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.bounding.BoundingBox;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.post.SceneProcessor;
import com.jme3.profile.AppProfiler;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput;
import de.blight.eztree.Tree;
import de.blight.eztree.TreeOptions;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* JME3-AppState für den EZ-Tree-Generator.
*
* Teilt den Vorschau-Viewport mit {@link TreeGeneratorState} (kein eigenes Framebuffer).
* Verarbeitet {@link SharedInput.EzTreeGenRequest}-Einträge aus der Queue,
* baut einen {@link Tree}-Node, weist Materialien zu und zeigt ihn in der Vorschau.
* Optional: .j3o-Export mit Impostor-PNG.
*/
public class EzTreeState extends BaseAppState {
private static final int IMPOSTOR_SIZE = 512;
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private final SharedInput input;
private SimpleApplication app;
private AssetManager assets;
private TreeGeneratorState previewHost;
// ── Laufende Capture-Operation ────────────────────────────────────────────
private SharedInput.EzTreeGenRequest pendingRequest = null;
private Node pendingTreeNode = null;
private ViewPort captureVP = null;
private FrameBuffer captureFB = null;
private volatile boolean captureReady = false;
public EzTreeState(SharedInput input) { this.input = input; }
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assets = app.getAssetManager();
// previewHost via lazy-init in update() TreeGeneratorState evtl. noch nicht attached
}
@Override protected void cleanup(Application app) { cleanupCapture(); }
@Override protected void onEnable() {}
@Override protected void onDisable() {}
// ── Update-Schleife ───────────────────────────────────────────────────────
@Override
public void update(float tpf) {
// Lazy-init: TreeGeneratorState muss initialisiert sein, bevor wir darauf zugreifen
if (previewHost == null) {
previewHost = getStateManager().getState(TreeGeneratorState.class);
if (previewHost == null) return;
}
if (pendingRequest != null && captureReady) {
finishCapture();
} else if (pendingRequest == null) {
SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
if (req != null) startGeneration(req);
}
}
// ── Phase 1: Generierung ──────────────────────────────────────────────────
private void startGeneration(SharedInput.EzTreeGenRequest req) {
cleanupCapture();
Tree tree = new Tree(req.options());
tree.generate();
applyMaterials(tree, req.options());
tree.updateGeometricState();
BoundingBox bb = boundsOf(tree);
float camDist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
: 20f;
Vector3f target = bb != null
? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 5f, 0f);
// Szenenänderung über enqueue() läuft am Anfang des nächsten Frames,
// bevor TreeGeneratorState.update() updateGeometricState() aufruft.
final float dist = camDist;
final Vector3f tgt = target;
app.enqueue(() -> {
previewHost.setPreviewContent(tree, dist, tgt);
if (req.exportAfter()) {
setupCapture(tree, boundsOf(tree), req);
}
});
if (!req.exportAfter()) {
input.treeGenStatusMsg = "EZ-Tree Vorschau: '" + req.exportName() + "'";
} else {
input.treeGenStatusMsg = "EZ-Tree: generiere…";
}
}
// ── Phase 2: Impostor-Capture ─────────────────────────────────────────────
private void setupCapture(Tree tree, BoundingBox bb, SharedInput.EzTreeGenRequest req) {
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
captureFB.addColorTexture(capTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
captureVP = buildCaptureViewPort(tree, safeBb, captureFB);
captureReady = false;
pendingRequest = req;
pendingTreeNode = tree;
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
}
private void finishCapture() {
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
app.getRenderer().readFrameBuffer(captureFB, pixels);
cleanupCapture();
saveImpostor(pixels, "ez_impostor_" + pendingRequest.exportName());
exportTree(pendingTreeNode, pendingRequest.exportName());
pendingRequest = null;
pendingTreeNode = null;
}
// ── Material-Aufbau ───────────────────────────────────────────────────────
private void applyMaterials(Tree tree, TreeOptions opts) {
for (Spatial child : tree.getChildren()) {
if (child instanceof Geometry g) {
switch (g.getName()) {
case "bark" -> {
g.setMaterial(buildBarkMat(opts));
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
case "leaves" -> {
g.setMaterial(buildLeafMat(opts));
g.setQueueBucket(RenderQueue.Bucket.Transparent);
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
}
} else if (child instanceof Node trellis) {
// Trellis-Node: Rinden-Material auf alle Geometrien
Material mat = buildBarkMat(opts);
for (Spatial s : trellis.getChildren()) {
if (s instanceof Geometry g) g.setMaterial(mat.clone());
}
}
}
}
private Material buildBarkMat(TreeOptions opts) {
try {
Material mat = new Material(assets, "MatDefs/Tree.j3md");
mat.setColor("Diffuse", new ColorRGBA(opts.bark.r, opts.bark.g, opts.bark.b, 1f));
mat.setFloat("WindStrength", 0.15f);
mat.setFloat("WindSpeed", 0.5f);
if (opts.bark.textureFile != null) {
try {
mat.setTexture("BarkMap", assets.loadTexture(opts.bark.textureFile));
mat.setBoolean("HasBarkMap", true);
} catch (Exception ignored) {}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(opts.bark.r, opts.bark.g, opts.bark.b, 1f));
return mat;
}
}
private Material buildLeafMat(TreeOptions opts) {
try {
Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
mat.setColor("Diffuse", new ColorRGBA(opts.leaves.r, opts.leaves.g, opts.leaves.b, 1f));
mat.setFloat("WindStrength", 0.30f);
mat.setFloat("WindSpeed", 0.7f);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
if (opts.leaves.textureFile != null) {
try {
mat.setTexture("LeafMap", assets.loadTexture(opts.leaves.textureFile));
mat.setBoolean("HasLeafMap", true);
} catch (Exception ignored) {}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(opts.leaves.r, opts.leaves.g, opts.leaves.b, 1f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
}
// ── Offscreen-Viewport für Impostor ───────────────────────────────────────
private ViewPort buildCaptureViewPort(Tree src, BoundingBox bb, FrameBuffer fb) {
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
float dist = extent * 3f;
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
cam.setLocation(center.add(0f, 0f, dist));
cam.lookAt(center, Vector3f.UNIT_Y);
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
ViewPort vp = app.getRenderManager()
.createPostView("ezCapture_" + System.nanoTime(), cam);
vp.setOutputFrameBuffer(fb);
vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
vp.setClearFlags(true, true, true);
Node scene = new Node("ezCapScene");
scene.addLight(new DirectionalLight(
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
scene.attachChild(cloneForCapture(src));
vp.attachScene(scene);
scene.updateGeometricState();
vp.addProcessor(new SceneProcessor() {
@Override public void initialize(RenderManager rm, ViewPort v) {}
@Override public void reshape(ViewPort v, int w, int h) {}
@Override public boolean isInitialized() { return true; }
@Override public void preFrame(float t) {}
@Override public void postQueue(RenderQueue rq) {}
@Override public void cleanup() {}
@Override public void setProfiler(AppProfiler p) {}
@Override
public void postFrame(FrameBuffer out) {
vp.removeProcessor(this);
captureReady = true;
}
});
return vp;
}
private static Node cloneForCapture(Tree src) {
Node copy = new Node("ezCap");
copy.setLocalTranslation(src.getLocalTranslation());
for (Spatial child : src.getChildren()) {
if (child instanceof Geometry g) {
Geometry gc = new Geometry(g.getName(), g.getMesh());
gc.setMaterial(g.getMaterial().clone());
copy.attachChild(gc);
} else if (child instanceof Node n) {
Node nc = new Node(n.getName());
for (Spatial ns : n.getChildren()) {
if (ns instanceof Geometry ng) {
Geometry ngc = new Geometry(ng.getName(), ng.getMesh());
ngc.setMaterial(ng.getMaterial().clone());
nc.attachChild(ngc);
}
}
copy.attachChild(nc);
}
}
return copy;
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static BoundingBox boundsOf(Tree tree) {
if (tree.getWorldBound() instanceof BoundingBox bb) return bb;
return null;
}
private void cleanupCapture() {
if (captureVP != null) {
app.getRenderManager().removePostView(captureVP);
captureVP = null;
}
if (captureFB != null) {
try { captureFB.dispose(); } catch (Exception ignored) {}
captureFB = null;
}
captureReady = false;
}
private void saveImpostor(ByteBuffer pixels, String name) {
try {
pixels.rewind();
BufferedImage img = new BufferedImage(
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
int r = pixels.get() & 0xFF, g = pixels.get() & 0xFF,
b = pixels.get() & 0xFF, a = pixels.get() & 0xFF;
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
}
}
Path texDir = ASSET_ROOT.resolve("textures");
Files.createDirectories(texDir);
ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
} catch (IOException e) {
System.err.println("[EzTreeState] Impostor-Fehler: " + e.getMessage());
}
}
private void exportTree(Node treeNode, String name) {
try {
Path modelDir = ASSET_ROOT.resolve("models");
Files.createDirectories(modelDir);
File out = modelDir.resolve("EzTree_" + name + ".j3o").toFile();
BinaryExporter.getInstance().save(treeNode, out);
input.treeGenStatusMsg = "EZ-Tree exportiert: " + out.getName();
input.refreshAssets = true;
} catch (IOException e) {
input.treeGenStatusMsg = "EZ-Tree Export-Fehler: " + e.getMessage();
}
}
}

View File

@@ -0,0 +1,158 @@
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.bounding.BoundingBox;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import de.blight.editor.SharedInput;
import de.blight.editor.tree.PalmMeshBuilder;
import de.blight.editor.tree.PalmOptions;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PalmGeneratorState extends BaseAppState {
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private final SharedInput input;
private SimpleApplication app;
private AssetManager assets;
private TreeGeneratorState previewHost;
public PalmGeneratorState(SharedInput input) { this.input = input; }
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assets = app.getAssetManager();
}
@Override protected void cleanup(Application app) {}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void update(float tpf) {
if (previewHost == null) {
previewHost = getStateManager().getState(TreeGeneratorState.class);
if (previewHost == null) return;
}
SharedInput.PalmGenRequest req = input.palmGenQueue.poll();
if (req == null) return;
Node palm = PalmMeshBuilder.build(req.options());
applyMaterials(palm, req.options());
palm.updateGeometricState();
BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null;
float dist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f
: 20f;
Vector3f target = bb != null
? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 6f, 0f);
final float finalDist = dist;
final Vector3f finalTarget = target;
final Node finalPalm = palm;
final PalmOptions finalOpts = req.options();
final String finalName = req.exportName();
final boolean doExport = req.exportAfter();
app.enqueue(() -> {
previewHost.setPreviewContent(finalPalm, finalDist, finalTarget);
if (doExport) exportPalm(finalPalm, finalName);
});
input.treeGenStatusMsg = doExport
? "Palme: exportiere…"
: "Palme: Vorschau '" + req.exportName() + "'";
}
private void applyMaterials(Node palm, PalmOptions opts) {
for (Spatial child : palm.getChildren()) {
if (!(child instanceof Geometry g)) continue;
switch (g.getName()) {
case "bark" -> {
g.setMaterial(buildBarkMat(opts));
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
case "leaves" -> {
g.setMaterial(buildLeafMat(opts));
g.setQueueBucket(RenderQueue.Bucket.Transparent);
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
}
}
}
private Material buildBarkMat(PalmOptions opts) {
try {
Material mat = new Material(assets, "MatDefs/Tree.j3md");
mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f));
mat.setFloat("WindStrength", 0.08f);
mat.setFloat("WindSpeed", 0.4f);
if (opts.barkTexture != null) {
try {
mat.setTexture("BarkMap", assets.loadTexture(opts.barkTexture));
mat.setBoolean("HasBarkMap", true);
} catch (Exception ignored) {}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f));
return mat;
}
}
private Material buildLeafMat(PalmOptions opts) {
try {
Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f));
mat.setFloat("WindStrength", 0.20f);
mat.setFloat("WindSpeed", 0.5f);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
if (opts.leafTexture != null) {
try {
mat.setTexture("LeafMap", assets.loadTexture(opts.leafTexture));
mat.setBoolean("HasLeafMap", true);
} catch (Exception ignored) {}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
}
private void exportPalm(Node palmNode, String name) {
try {
Path modelDir = ASSET_ROOT.resolve("models");
Files.createDirectories(modelDir);
File out = modelDir.resolve("Palm_" + name + ".j3o").toFile();
BinaryExporter.getInstance().save(palmNode, out);
input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
input.refreshAssets = true;
} catch (IOException e) {
input.treeGenStatusMsg = "Palme Export-Fehler: " + e.getMessage();
}
}
}

View File

@@ -0,0 +1,343 @@
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.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.util.BufferUtils;
import de.blight.common.MapData;
import de.blight.editor.SharedInput;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.*;
/**
* Rendert Gras auf dem Basis-Terrain.
*
* Datenmodell: Dichte-Map (513×513 Bytes, gleiche Auflösung wie Splatmap).
* Rendering: Pro 128×128-WE-Chunk ein gebatchtes Kreuz-Quad-Mesh.
* LOD: GrassVisibilityControl cullt Chunks jenseits FAR_DIST.
* Wind: MatDefs/Grass.j3md (Vertex-Shader mit Sinus-Wind).
*/
public class PlacedObjectState extends BaseAppState {
// ── Terrain-Konstanten ────────────────────────────────────────────────────
private static final int TERRAIN_HALF = 2048;
private static final float WORLD_SIZE = 4096f;
// ── Dichte-Map ────────────────────────────────────────────────────────────
private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; // 513
private static final float SPLAT_WE_PER_PX = WORLD_SIZE / (SPLAT_SIZE - 1); // 8.0
// ── Chunks ────────────────────────────────────────────────────────────────
private static final int CHUNK_SIZE = 128;
private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE; // 32
private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS;
// ── Gras-Generierung ──────────────────────────────────────────────────────
private static final int MAX_BLADES_PER_PIXEL = 3;
private static final float BLADE_WIDTH_FACTOR = 0.18f;
// ── LOD ───────────────────────────────────────────────────────────────────
private static final float GRASS_FAR_DIST = 400f;
private static final float GRASS_FAR_DIST_SQ = GRASS_FAR_DIST * GRASS_FAR_DIST;
// ── Rebuild-Budget ────────────────────────────────────────────────────────
private static final int MAX_REBUILDS_PER_FRAME = 3;
// ── Zustand ───────────────────────────────────────────────────────────────
private final SharedInput input;
private Camera cam;
private TerrainQuad terrain;
private Node grassNode;
private Material grassMat;
private byte[] densityMap;
private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT];
private final Geometry[] chunkGeos = new Geometry[CHUNK_COUNT];
// ── Konstruktor ───────────────────────────────────────────────────────────
public PlacedObjectState(SharedInput input, MapData loadedData) {
this.input = input;
this.densityMap = new byte[SPLAT_SIZE * SPLAT_SIZE];
if (loadedData != null && loadedData.grassDensity != null) {
System.arraycopy(loadedData.grassDensity, 0, densityMap, 0, densityMap.length);
Arrays.fill(dirtyChunks, true);
}
}
public void setTerrain(TerrainQuad terrain) {
this.terrain = terrain;
}
/** Gibt die aktuelle Dichte-Map zurück (für performSave). */
public byte[] getDensityMap() { return densityMap; }
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.cam = app.getCamera();
grassNode = new Node("grassNode");
((SimpleApplication) app).getRootNode().attachChild(grassNode);
grassMat = buildGrassMaterial(app.getAssetManager());
}
@Override
protected void cleanup(Application app) {
((SimpleApplication) app).getRootNode().detachChild(grassNode);
}
@Override protected void onEnable() { grassNode.setCullHint(Spatial.CullHint.Inherit); }
@Override protected void onDisable() { grassNode.setCullHint(Spatial.CullHint.Always); }
@Override
public void update(float tpf) {
processGrassEdits();
rebuildDirtyChunks();
}
// ── Material ──────────────────────────────────────────────────────────────
private Material buildGrassMaterial(AssetManager assets) {
try {
Material mat = new Material(assets, "MatDefs/Grass.j3md");
mat.setColor("Color", new ColorRGBA(0.25f, 0.70f, 0.15f, 1f));
mat.setFloat("WindSpeed", 0.5f);
mat.setFloat("WindStrength", 0.12f);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
} catch (Exception e) {
System.err.println("[PlacedObjectState] Grass.j3md nicht gefunden, Fallback: " + e.getMessage());
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
}
// ── Pinsel: Dichte-Map anpassen ───────────────────────────────────────────
private void processGrassEdits() {
SharedInput.GrassEdit edit;
while ((edit = input.grassEditQueue.poll()) != null) {
if (terrain == null) continue;
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);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) continue;
Vector3f contact = hits.getClosestCollision().getContactPoint();
float radius = (float) input.grassTool.brushRadius.getValue();
paintDensity(contact.x, contact.z, radius, edit.action());
}
}
private void paintDensity(float cx, float cz, float radius, int action) {
int centerPX = Math.round((cx + TERRAIN_HALF) / SPLAT_WE_PER_PX);
int centerPZ = Math.round((cz + TERRAIN_HALF) / SPLAT_WE_PER_PX);
int pixR = (int) Math.ceil(radius / SPLAT_WE_PER_PX);
float strength = (float) input.grassTool.density.getValue() / 10f; // 0.15.0
for (int dz = -pixR; dz <= pixR; dz++) {
int pz = centerPZ + dz;
if (pz < 0 || pz >= SPLAT_SIZE) continue;
for (int dx = -pixR; dx <= pixR; dx++) {
int px = centerPX + dx;
if (px < 0 || px >= SPLAT_SIZE) continue;
float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
if (distWE >= radius) continue;
float t = distWE / radius;
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
int delta = (int)(strength * falloff * 40f);
int idx = pz * SPLAT_SIZE + px;
int cur = densityMap[idx] & 0xFF;
int nxt = (action > 0)
? Math.min(255, cur + delta)
: Math.max(0, cur - delta);
if (nxt != cur) {
densityMap[idx] = (byte) nxt;
markChunkDirtyAtPixel(px, pz);
}
}
}
}
// ── Höhenanpassung bei Terrain-Edit ───────────────────────────────────────
/**
* Markiert alle Chunks dirty, deren Fläche eine der übergebenen Terrain-Positionen
* enthält. Die Blatt-Y-Koordinaten werden beim nächsten Rebuild neu von
* terrain.getHeight() abgelesen.
*/
public void adjustObjectHeights(List<Vector2f> locs, List<Float> deltas) {
for (Vector2f loc : locs) {
int cx = (int)((loc.x + TERRAIN_HALF) / CHUNK_SIZE);
int cz = (int)((loc.y + TERRAIN_HALF) / CHUNK_SIZE);
if (cx >= 0 && cx < CHUNKS_PER_AXIS && cz >= 0 && cz < CHUNKS_PER_AXIS) {
dirtyChunks[cx + cz * CHUNKS_PER_AXIS] = true;
}
}
}
// ── Chunk-Rebuild ─────────────────────────────────────────────────────────
private void rebuildDirtyChunks() {
int rebuilt = 0;
for (int i = 0; i < CHUNK_COUNT && rebuilt < MAX_REBUILDS_PER_FRAME; i++) {
if (!dirtyChunks[i]) continue;
rebuildChunk(i);
dirtyChunks[i] = false;
rebuilt++;
}
}
private void rebuildChunk(int idx) {
if (terrain == null) return;
int cx = idx % CHUNKS_PER_AXIS;
int cz = idx / CHUNKS_PER_AXIS;
float wXMin = -TERRAIN_HALF + cx * CHUNK_SIZE;
float wZMin = -TERRAIN_HALF + cz * CHUNK_SIZE;
// Dichte-Pixel-Bereich dieses Chunks
int pxMin = Math.max(0, (int)((wXMin + TERRAIN_HALF) / SPLAT_WE_PER_PX));
int pzMin = Math.max(0, (int)((wZMin + TERRAIN_HALF) / SPLAT_WE_PER_PX));
int pxMax = Math.min(SPLAT_SIZE - 1, (int)((wXMin + CHUNK_SIZE + TERRAIN_HALF) / SPLAT_WE_PER_PX));
int pzMax = Math.min(SPLAT_SIZE - 1, (int)((wZMin + CHUNK_SIZE + TERRAIN_HALF) / SPLAT_WE_PER_PX));
float baseH = (float) input.grassTool.grassHeight.getValue();
// Blatt-Positionen generieren
List<float[]> blades = new ArrayList<>(); // [x, y, z, height]
for (int pz = pzMin; pz <= pzMax; pz++) {
for (int px = pxMin; px <= pxMax; px++) {
int d = densityMap[pz * SPLAT_SIZE + px] & 0xFF;
if (d == 0) continue;
int count = Math.max(1, (int)(d / 255f * MAX_BLADES_PER_PIXEL));
Random rng = new Random((long) px * 100003L + pz);
float pixWorldX = px * SPLAT_WE_PER_PX - TERRAIN_HALF;
float pixWorldZ = pz * SPLAT_WE_PER_PX - TERRAIN_HALF;
for (int b = 0; b < count; b++) {
float bx = pixWorldX + (rng.nextFloat() - 0.5f) * SPLAT_WE_PER_PX;
float bz = pixWorldZ + (rng.nextFloat() - 0.5f) * SPLAT_WE_PER_PX;
float th = terrain.getHeight(new Vector2f(bx, bz));
if (Float.isNaN(th)) continue;
float h = baseH * (0.7f + rng.nextFloat() * 0.6f);
blades.add(new float[]{bx, th, bz, h});
}
}
}
// Alte Geometrie entfernen
if (chunkGeos[idx] != null) {
grassNode.detachChild(chunkGeos[idx]);
chunkGeos[idx] = null;
}
if (blades.isEmpty()) return;
Mesh mesh = buildGrassMesh(blades);
float chunkCX = wXMin + CHUNK_SIZE * 0.5f;
float chunkCZ = wZMin + CHUNK_SIZE * 0.5f;
Geometry geo = new Geometry("grassChunk_" + idx, mesh);
geo.setMaterial(grassMat);
geo.addControl(new GrassVisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ)));
grassNode.attachChild(geo);
chunkGeos[idx] = geo;
}
// ── Mesh: Kreuz-Quad pro Halm mit UV-Koordinaten ──────────────────────────
private static Mesh buildGrassMesh(List<float[]> blades) {
int n = blades.size();
FloatBuffer pos = BufferUtils.createFloatBuffer(n * 8 * 3);
FloatBuffer uv = BufferUtils.createFloatBuffer(n * 8 * 2);
IntBuffer idx = BufferUtils.createIntBuffer(n * 12);
int vi = 0;
for (float[] blade : blades) {
float x = blade[0], y = blade[1], z = blade[2], h = blade[3];
float w = Math.max(0.05f, h * BLADE_WIDTH_FACTOR);
// Quad A Breite entlang X-Achse
pos.put(x-w).put(y ).put(z); uv.put(0).put(0);
pos.put(x+w).put(y ).put(z); uv.put(1).put(0);
pos.put(x+w).put(y+h).put(z); uv.put(1).put(1);
pos.put(x-w).put(y+h).put(z); uv.put(0).put(1);
// Quad B Breite entlang Z-Achse
pos.put(x).put(y ).put(z-w); uv.put(0).put(0);
pos.put(x).put(y ).put(z+w); uv.put(1).put(0);
pos.put(x).put(y+h).put(z+w); uv.put(1).put(1);
pos.put(x).put(y+h).put(z-w); uv.put(0).put(1);
idx.put(vi ).put(vi+1).put(vi+2);
idx.put(vi ).put(vi+2).put(vi+3);
idx.put(vi+4).put(vi+5).put(vi+6);
idx.put(vi+4).put(vi+6).put(vi+7);
vi += 8;
}
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
return mesh;
}
// ── LOD-Control ───────────────────────────────────────────────────────────
private static final class GrassVisibilityControl extends AbstractControl {
private final Camera cam;
private final Vector3f center;
GrassVisibilityControl(Camera cam, Vector3f center) {
this.cam = cam;
this.center = center;
}
@Override
protected void controlUpdate(float tpf) {
float distSq = cam.getLocation().distanceSquared(center);
spatial.setCullHint(distSq > GRASS_FAR_DIST_SQ
? Spatial.CullHint.Always
: Spatial.CullHint.Inherit);
}
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private void markChunkDirtyAtPixel(int px, int pz) {
float worldX = px * SPLAT_WE_PER_PX - TERRAIN_HALF;
float worldZ = pz * SPLAT_WE_PER_PX - TERRAIN_HALF;
int cx = (int)((worldX + TERRAIN_HALF) / CHUNK_SIZE);
int cz = (int)((worldZ + TERRAIN_HALF) / CHUNK_SIZE);
if (cx >= 0 && cx < CHUNKS_PER_AXIS && cz >= 0 && cz < CHUNKS_PER_AXIS) {
dirtyChunks[cx + cz * CHUNKS_PER_AXIS] = true;
}
}
}

View File

@@ -0,0 +1,595 @@
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.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.Camera;
import com.jme3.scene.*;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import com.jme3.scene.shape.Quad;
import com.jme3.scene.shape.Sphere;
import com.jme3.texture.Texture;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.export.binary.BinaryExporter;
import de.blight.editor.SharedInput;
import de.blight.editor.object.SceneObject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* Verwaltet platzierte Szenen-Objekte im Welt-Editor.
*
* - Linksklick im Objekt-Modus: selektiert ein vorhandenes Objekt
* oder platziert ein neues (wenn pendingModelPath gesetzt).
* - Gizmo: 3 Translationspfeile (X/Y/Z) + 1 Rotationsring (Y),
* am ausgewählten Objekt angebracht.
* - Drag auf Pfeil → translateY/X/Z; Drag auf Ring → rotY.
*/
public class SceneObjectState extends BaseAppState {
private static final Path ASSET_ROOT = Paths.get("editor-assets");
// ── Gizmo-Farben ─────────────────────────────────────────────────────────
private static final ColorRGBA COL_X = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f);
private static final ColorRGBA COL_Y = new ColorRGBA(0.1f, 0.9f, 0.1f, 1f);
private static final ColorRGBA COL_Z = new ColorRGBA(0.1f, 0.3f, 1.0f, 1f);
private static final ColorRGBA COL_ROT = new ColorRGBA(1.0f, 0.7f, 0.0f, 1f);
private static final float ARROW_LEN = 3.0f;
private static final float ARROW_RADIUS = 0.12f;
private static final float PX_PER_WE = 0.15f; // Sensitivität Translation
private static final float ROT_PER_PX = 0.015f; // Sensitivität Rotation
// ── Zustand ──────────────────────────────────────────────────────────────
private final SharedInput input;
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private Node rootNode;
private TerrainQuad terrain;
private final List<SceneObject> objects = new ArrayList<>();
private final List<Node> objNodes = new ArrayList<>();
private Node objectRoot; // hält alle Objekt-Nodes
private Node gizmoNode; // hält Gizmo-Geometrien
private Geometry arrowX, arrowY, arrowZ, ringRot;
private int selectedIdx = -1;
private int activeGizmo = -1; // 0=X,1=Y,2=Z,3=rot; -1=keins
private Node previewNode;
private String previewModelPath; // gecachter Pfad, um Reload zu vermeiden
// ── Konstruktor ──────────────────────────────────────────────────────────
public SceneObjectState(SharedInput input) {
this.input = input;
}
public void setTerrain(TerrainQuad terrain) {
this.terrain = terrain;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.cam = app.getCamera();
this.assets = app.getAssetManager();
this.rootNode = this.app.getRootNode();
objectRoot = new Node("sceneObjects");
rootNode.attachChild(objectRoot);
gizmoNode = new Node("gizmo");
buildGizmo();
gizmoNode.setCullHint(Spatial.CullHint.Always);
previewNode = new Node("objectPreview");
previewNode.setCullHint(Spatial.CullHint.Always);
rootNode.attachChild(previewNode);
}
@Override
protected void cleanup(Application app) {
objectRoot.removeFromParent();
gizmoNode.removeFromParent();
previewNode.removeFromParent();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
// Modell-Konvertierung und Mesh-Erstellung (unabhängig vom aktiven Layer)
SharedInput.ModelConvertRequest conv;
while ((conv = input.modelConvertQueue.poll()) != null) {
convertModel(conv);
}
SharedInput.MeshCreateRequest meshReq;
while ((meshReq = input.meshCreateQueue.poll()) != null) {
createMesh(meshReq);
}
updatePreview();
boolean isObjectLayer = input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT;
if (!isObjectLayer) return;
// Solid-Flag-Änderung von JavaFX
Boolean solidChange = input.pendingSolidChange;
if (solidChange != null) {
input.pendingSolidChange = null;
if (selectedIdx >= 0) objects.get(selectedIdx).solid = solidChange;
}
// Klick-Events
SharedInput.ObjectClick click;
while ((click = input.objectClickQueue.poll()) != null) {
handleClick(click);
}
// Gizmo-Drags
SharedInput.ObjectDrag drag;
while ((drag = input.objectDragQueue.poll()) != null) {
handleGizmoDrag(drag);
}
// Gizmo nachführen
if (selectedIdx >= 0) {
updateGizmoPosition();
}
}
// ── Platzierungs-Vorschau ─────────────────────────────────────────────────
private void updatePreview() {
String modelPath = input.pendingModelPath;
if (input.activeLayer != SharedInput.LAYER_OBJECTS || modelPath == null
|| input.mouseScreenX < 0 || terrain == null) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
if (!modelPath.equals(previewModelPath)) {
previewNode.detachAllChildren();
previewModelPath = modelPath;
try {
Spatial model = modelPath.startsWith("@")
? createPrimitiveSpatial(modelPath.substring(1))
: assets.loadModel(modelPath);
stripControlsRecursive(model);
applyPreviewMaterial(model);
previewNode.attachChild(model);
} catch (Exception e) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
}
float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
Ray ray = screenToRay(jmeX, jmeY);
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) {
previewNode.setCullHint(Spatial.CullHint.Always);
return;
}
Vector3f pt = hits.getClosestCollision().getContactPoint();
previewNode.setLocalTranslation(pt.x, pt.y, pt.z);
previewNode.setCullHint(Spatial.CullHint.Inherit);
}
private void applyPreviewMaterial(Spatial s) {
if (s instanceof Geometry geo) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.3f, 0.8f, 1.0f, 1f));
mat.getAdditionalRenderState().setWireframe(true);
mat.getAdditionalRenderState().setDepthTest(false);
geo.setMaterial(mat);
geo.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
} else if (s instanceof Node n) {
for (Spatial child : new java.util.ArrayList<>(n.getChildren())) {
applyPreviewMaterial(child);
}
}
}
// ── Klick-Handling ────────────────────────────────────────────────────────
private void handleClick(SharedInput.ObjectClick click) {
if (click.rightButton()) return; // Rechtsklick reserviert für Kamera
float jmeX = (float)(click.screenX() * input.viewportScaleX);
float jmeY = cam.getHeight() - (float)(click.screenY() * input.viewportScaleY);
Ray ray = screenToRay(jmeX, jmeY);
// 1. Gizmo-Test (Priorität)
if (selectedIdx >= 0) {
int hit = pickGizmo(ray);
if (hit >= 0) { activeGizmo = hit; return; }
}
activeGizmo = -1;
// 2. Objekt-Treffer?
CollisionResults objHits = new CollisionResults();
objectRoot.collideWith(ray, objHits);
if (objHits.size() > 0) {
Spatial hit = objHits.getClosestCollision().getGeometry();
int idx = findObjectIndexByNode(hit);
if (idx >= 0) { selectObject(idx); return; }
}
// 3. Terrain-Treffer Platzieren nur im Platzieren-Modus
if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; }
String modelPath = input.pendingModelPath;
if (terrain == null) return;
CollisionResults terrHits = new CollisionResults();
terrain.collideWith(ray, terrHits);
if (terrHits.size() == 0) return;
if (modelPath == null) { deselectAll(); return; }
Vector3f pt = terrHits.getClosestCollision().getContactPoint();
previewNode.setCullHint(Spatial.CullHint.Always);
placeObject(modelPath, pt.x, pt.z, pt.y);
}
// ── Objekt platzieren ────────────────────────────────────────────────────
private void placeObject(String modelPath, float wx, float wz, float wy) {
SceneObject so = new SceneObject(modelPath, wx, wz, wy, false);
objects.add(so);
Node node = loadModelNode(modelPath, wx, wy, wz);
objNodes.add(node);
objectRoot.attachChild(node);
selectObject(objects.size() - 1);
input.objectJustPlaced = true;
setStatus("Platziert: " + modelPath);
}
private Node loadModelNode(String modelPath, float wx, float wy, float wz) {
Node node = new Node("obj_" + objects.size());
try {
Spatial model = modelPath.startsWith("@")
? createPrimitiveSpatial(modelPath.substring(1))
: assets.loadModel(modelPath);
if (!modelPath.startsWith("@")) stripControlsRecursive(model);
if (modelPath.startsWith("@")) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.75f, 0.75f, 0.75f, 1f));
if (model instanceof Geometry g) g.setMaterial(mat);
}
node.attachChild(model);
} catch (Exception e) {
Geometry box = new Geometry("placeholder", new Box(0.5f, 0.5f, 0.5f));
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Red);
box.setMaterial(mat);
node.attachChild(box);
}
node.setLocalTranslation(wx, wy, wz);
return node;
}
private Spatial createPrimitiveSpatial(String type) {
return switch (type) {
case "sphere" -> new Geometry("Kugel", new Sphere(24, 24, 1f));
case "cylinder" -> new Geometry("Zylinder", new Cylinder(2, 24, 0.5f, 2f, true));
case "plane" -> {
Geometry g = new Geometry("Ebene", new Quad(2f, 2f));
g.rotate(-FastMath.HALF_PI, 0, 0);
g.setLocalTranslation(-1f, 0, 1f);
yield g;
}
default -> new Geometry("Box", new Box(0.5f, 0.5f, 0.5f));
};
}
/**
* Entfernt alle Controls (auch null-Einträge aus fehlgeschlagener Deserialisierung)
* rekursiv aus dem Szene-Graphen. Nötig, weil TreeLodControl keinen no-arg
* Konstruktor hat und als null im controls-Array zurückbleibt.
*/
private static void stripControlsRecursive(Spatial s) {
try {
java.lang.reflect.Field f = com.jme3.scene.Spatial.class.getDeclaredField("controls");
f.setAccessible(true);
((java.util.List<?>) f.get(s)).clear();
} catch (Exception ignored) {}
if (s instanceof Node n) {
for (Spatial child : new java.util.ArrayList<>(n.getChildren())) {
stripControlsRecursive(child);
}
}
}
// ── Selektion ─────────────────────────────────────────────────────────────
private void selectObject(int idx) {
selectedIdx = idx;
SceneObject so = objects.get(idx);
gizmoNode.removeFromParent();
objNodes.get(idx).attachChild(gizmoNode);
gizmoNode.setLocalTranslation(0, 0, 0);
gizmoNode.setCullHint(Spatial.CullHint.Inherit);
// Info für JavaFX serialisieren
input.selectedObjectInfo = so.modelPath + "|" + so.solid + "|"
+ so.getWorldX() + "|" + so.getGroundY() + "|" + so.getWorldZ()
+ "|" + so.getRotY() + "|" + so.getScale();
input.objectSelectionChanged = true;
}
private void deselectAll() {
selectedIdx = -1;
activeGizmo = -1;
gizmoNode.removeFromParent();
gizmoNode.setCullHint(Spatial.CullHint.Always);
input.selectedObjectInfo = null;
input.objectSelectionChanged = true;
}
// ── Gizmo-Drag ───────────────────────────────────────────────────────────
private void handleGizmoDrag(SharedInput.ObjectDrag drag) {
if (selectedIdx < 0 || activeGizmo < 0) return;
SceneObject so = objects.get(selectedIdx);
Node node = objNodes.get(selectedIdx);
// dx = horizontale Mausbewegung, dy = vertikale (positiv = nach unten)
float dx = drag.dx() * PX_PER_WE;
float dy = drag.dy() * PX_PER_WE;
switch (activeGizmo) {
case 0 -> { so.translate(dx, 0, 0); node.move(dx, 0, 0); } // X
case 1 -> { so.translate(0, -dy, 0); node.move(0, -dy, 0); } // Y (oben = negatives dy)
case 2 -> { so.translate(0, 0, dx); node.move(0, 0, dx); } // Z
case 3 -> {
float rad = drag.dx() * ROT_PER_PX;
so.rotateY(rad);
node.rotate(0, rad, 0);
}
}
updateGizmoPosition();
input.selectedObjectInfo = so.modelPath + "|" + so.solid + "|"
+ so.getWorldX() + "|" + so.getGroundY() + "|" + so.getWorldZ()
+ "|" + so.getRotY() + "|" + so.getScale();
input.objectSelectionChanged = true;
}
// ── Gizmo-Bau ────────────────────────────────────────────────────────────
private void buildGizmo() {
arrowX = makeArrow(COL_X);
arrowY = makeArrow(COL_Y);
arrowZ = makeArrow(COL_Z);
ringRot = makeRing(COL_ROT);
// X-Pfeil: entlang +X
arrowX.setLocalRotation(new Quaternion().fromAngleAxis(
-FastMath.HALF_PI, Vector3f.UNIT_Z));
arrowX.setLocalTranslation(ARROW_LEN * 0.5f, 0, 0);
// Y-Pfeil: entlang +Y (Standard)
arrowY.setLocalTranslation(0, ARROW_LEN * 0.5f, 0);
// Z-Pfeil: entlang +Z
arrowZ.setLocalRotation(new Quaternion().fromAngleAxis(
FastMath.HALF_PI, Vector3f.UNIT_X));
arrowZ.setLocalTranslation(0, 0, ARROW_LEN * 0.5f);
// Rotationsring: horizontal (XZ-Ebene), leicht oberhalb
ringRot.setLocalTranslation(0, ARROW_LEN + 0.3f, 0);
gizmoNode.attachChild(arrowX);
gizmoNode.attachChild(arrowY);
gizmoNode.attachChild(arrowZ);
gizmoNode.attachChild(ringRot);
}
private Geometry makeArrow(ColorRGBA color) {
Cylinder cyl = new Cylinder(2, 6, ARROW_RADIUS, ARROW_LEN, true);
Geometry g = new Geometry("gizmoArrow", cyl);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color);
mat.getAdditionalRenderState().setDepthTest(false);
g.setMaterial(mat);
g.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
return g;
}
private Geometry makeRing(ColorRGBA color) {
// Einfacher dünner Torus-Ersatz: Kreis aus Liniensegmenten
int segs = 24;
float r = ARROW_LEN * 0.7f;
com.jme3.scene.shape.Torus torus =
new com.jme3.scene.shape.Torus(segs, 6, ARROW_RADIUS, r);
Geometry g = new Geometry("gizmoRing", torus);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color);
mat.getAdditionalRenderState().setDepthTest(false);
g.setMaterial(mat);
g.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
return g;
}
private void updateGizmoPosition() {
// Gizmo ist Kind des Objekt-Nodes → lokal (0,0,0) ist am Objekt-Ursprung.
// Skalierung: unveränderliche Pixelgröße durch Abstandsskalierung.
if (selectedIdx < 0) return;
Node objNode = objNodes.get(selectedIdx);
float dist = cam.getLocation().distance(objNode.getWorldTranslation());
float scale = Math.max(1f, dist * 0.1f);
gizmoNode.setLocalScale(scale);
}
// ── Gizmo-Picking ─────────────────────────────────────────────────────────
private int pickGizmo(Ray ray) {
CollisionResults hits = new CollisionResults();
arrowX.collideWith(ray, hits);
if (hits.size() > 0) return 0;
hits.clear();
arrowY.collideWith(ray, hits);
if (hits.size() > 0) return 1;
hits.clear();
arrowZ.collideWith(ray, hits);
if (hits.size() > 0) return 2;
hits.clear();
ringRot.collideWith(ray, hits);
if (hits.size() > 0) return 3;
return -1;
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private Ray screenToRay(float jmeX, float jmeY) {
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Vector3f dir = far.subtract(near).normalizeLocal();
return new Ray(near, dir);
}
private int findObjectIndexByNode(Spatial hit) {
for (int i = 0; i < objNodes.size(); i++) {
if (isDescendantOf(hit, objNodes.get(i))) return i;
}
return -1;
}
private static boolean isDescendantOf(Spatial child, Node ancestor) {
Spatial cur = child;
while (cur != null) {
if (cur == ancestor) return true;
cur = cur.getParent();
}
return false;
}
// ── Mesh-Erstellung ───────────────────────────────────────────────────────
private void createMesh(SharedInput.MeshCreateRequest req) {
Mesh mesh = switch (req.form()) {
case "Kugel" -> new Sphere(32, 32, req.sizeX());
case "Zylinder" -> new Cylinder(2, 32, req.sizeX(), req.sizeY(), true);
case "Ebene" -> new Quad(req.sizeX(), req.sizeZ());
default -> new Box(req.sizeX() * 0.5f, req.sizeY() * 0.5f, req.sizeZ() * 0.5f);
};
Geometry geo = new Geometry(req.name(), mesh);
// Ebene horizontal ausrichten (XZ-Ebene)
if ("Ebene".equals(req.form())) {
geo.rotate(-FastMath.HALF_PI, 0, 0);
geo.setLocalTranslation(-req.sizeX() * 0.5f, 0, req.sizeZ() * 0.5f);
}
geo.setMaterial(buildMeshMaterial(req));
if (req.a() < 1f) geo.setQueueBucket(RenderQueue.Bucket.Transparent);
Node wrapper = new Node(req.name());
wrapper.attachChild(geo);
try {
Path destDir = ASSET_ROOT.resolve("models");
Files.createDirectories(destDir);
Path dest = destDir.resolve(req.name() + ".j3o");
BinaryExporter.getInstance().save(wrapper, dest.toFile());
setStatus("Mesh gespeichert: " + req.name() + ".j3o");
input.refreshAssets = true;
} catch (IOException e) {
setStatus("Fehler beim Speichern des Meshes: " + e.getMessage());
}
}
private Material buildMeshMaterial(SharedInput.MeshCreateRequest req) {
ColorRGBA color = new ColorRGBA(req.r(), req.g(), req.b(), req.a());
Material mat;
if ("Phong".equals(req.matType())) {
mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
mat.setColor("Diffuse", color);
mat.setColor("Ambient", new ColorRGBA(req.r() * 0.3f, req.g() * 0.3f, req.b() * 0.3f, req.a()));
mat.setColor("Specular", ColorRGBA.White);
mat.setFloat("Shininess", 32f);
mat.setBoolean("UseMaterialColors", true);
if (req.texturePath() != null) {
try {
Texture tex = assets.loadTexture(req.texturePath());
tex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("DiffuseMap", tex);
} catch (Exception e) {
setStatus("Textur nicht geladen (" + req.texturePath() + "): " + e.getMessage());
}
}
} else {
mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color);
if (req.texturePath() != null) {
try {
Texture tex = assets.loadTexture(req.texturePath());
tex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
setStatus("Textur nicht geladen (" + req.texturePath() + "): " + e.getMessage());
}
}
}
if (req.wireframe()) mat.getAdditionalRenderState().setWireframe(true);
if (req.a() < 1f) mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
return mat;
}
// ── Modell-Konvertierung ──────────────────────────────────────────────────
private void convertModel(SharedInput.ModelConvertRequest req) {
setStatus("Konvertiere " + req.assetPath() + "");
try {
Spatial model = assets.loadModel(req.assetPath());
stripControlsRecursive(model);
Files.createDirectories(req.destJ3o().getParent());
BinaryExporter.getInstance().save(model, req.destJ3o().toFile());
if (req.srcToDelete() != null) Files.deleteIfExists(req.srcToDelete());
setStatus("Konvertiert: " + req.destJ3o().getFileName());
input.refreshAssets = true;
} catch (Exception e) {
setStatus("Konvertierung fehlgeschlagen (" + req.assetPath() + "): " + e.getMessage());
System.err.println("[SceneObject] Konvertierung fehlgeschlagen: " + e.getMessage());
}
}
private void setStatus(String msg) {
input.treeGenStatusMsg = msg; // recycled volatile field für Statuszeile
}
}

View File

@@ -15,44 +15,75 @@ 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.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Quad;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import com.jme3.util.SkyFactory;
import de.blight.common.MapData;
import de.blight.common.MapIO;
import de.blight.editor.SharedInput;
import de.blight.editor.tool.HeightTool;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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;
// ── Terrain-Konstanten ────────────────────────────────────────────────────
private static final int TERRAIN_SIZE = 4096;
private static final int TOTAL_SIZE = TERRAIN_SIZE + 1; // 4097
private static final int PATCH_SIZE = 65;
// ── Zustand ─────────────────────────────────────────────────────────────
// ── Splatmap-Konstanten ────────────────────────────────────────────────────
private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; // 513
private static final float WORLD_HALF = 2048f;
private static final float SPLAT_WE_PER_PX = 4096f / (SPLAT_SIZE - 1); // 8 WE/px
// ── Kamera ────────────────────────────────────────────────────────────────
private static final float CAM_SPEED = 300f;
private static final float ORBIT_SPEED = 1.5f;
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;
private TerrainQuad terrain;
private Geometry brushIndicator;
private UpperLayerState upperLayerState;
private PlacedObjectState placedObjectState;
private MapData loadedMapData;
// Kamera-Euler-Winkel
private float camYaw = 0f;
private float camPitch = -0.4f;
private final Vector3f camPos = new Vector3f(0, 14, 22);
// ── Splatmap ─────────────────────────────────────────────────────────────
private byte[] splatR, splatG, splatB;
private ByteBuffer splatBuf;
private Image splatImage;
private Texture2D splatTex;
// ── Kameraposition ────────────────────────────────────────────────────────
private float camYaw = 0f;
private float camPitch = -1.0f;
private final Vector3f camPos = new Vector3f(0f, 800f, (float)(800.0 / Math.tan(1.0)));
public TerrainEditorState(SharedInput input) {
this.input = input;
}
// ── Lifecycle ────────────────────────────────────────────────────────────
// ── Lifecycle ────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
@@ -60,6 +91,16 @@ public class TerrainEditorState extends BaseAppState {
this.cam = app.getCamera();
this.assets = app.getAssetManager();
this.rootNode = this.app.getRootNode();
cam.setFrustumFar(8000f);
if (MapIO.exists()) {
try {
loadedMapData = MapIO.load();
System.out.println("[TerrainEditor] Karte geladen: " + MapIO.getMapPath());
} catch (IOException e) {
System.err.println("[TerrainEditor] Karte nicht ladbar: " + e.getMessage());
}
}
}
@Override
@@ -75,115 +116,505 @@ public class TerrainEditorState extends BaseAppState {
@Override protected void cleanup(Application app) {}
// ── Szene aufbauen ───────────────────────────────────────────────────────
// ── 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);
DirectionalLight topLight = new DirectionalLight();
topLight.setDirection(Vector3f.UNIT_Y.negate());
topLight.setColor(new ColorRGBA(0.8f, 0.8f, 0.8f, 1f));
rootNode.addLight(topLight);
AmbientLight ambient = new AmbientLight(new ColorRGBA(0.35f, 0.38f, 0.45f, 1f));
rootNode.addLight(ambient);
// Terrain
terrainGeo = buildTerrainGeometry();
rootNode.attachChild(terrainGeo);
terrain = buildTerrain();
rootNode.attachChild(terrain);
upperLayerState = new UpperLayerState(input, loadedMapData);
app.getStateManager().attach(upperLayerState);
placedObjectState = new PlacedObjectState(input, loadedMapData);
placedObjectState.setTerrain(terrain);
app.getStateManager().attach(placedObjectState);
SceneObjectState sceneObjState = app.getStateManager().getState(SceneObjectState.class);
if (sceneObjState != null) sceneObjState.setTerrain(terrain);
// Wasser bei Y = 0
rootNode.attachChild(buildWater());
rootNode.attachChild(buildGrid());
// 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());
brushIndicator = buildBrushIndicator();
rootNode.attachChild(brushIndicator);
// Himmel (einfacher Hintergrund-Farbverlauf über Viewport-BG-Farbe)
app.getViewPort().setBackgroundColor(new ColorRGBA(0.45f, 0.60f, 0.80f, 1f));
try {
rootNode.attachChild(SkyFactory.createSky(assets,
"Textures/Sky/Bright/BrightSky.dds", SkyFactory.EnvMapType.CubeMap));
} catch (Exception e) {
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);
// ── Terrain ───────────────────────────────────────────────────────────────
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);
}
private TerrainQuad buildTerrain() {
float[] heights;
if (loadedMapData != null) {
heights = loadedMapData.terrainHeight;
} else {
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
Arrays.fill(heights, 1f);
}
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();
TerrainQuad tq = new TerrainQuad("terrain", PATCH_SIZE, TOTAL_SIZE, heights);
// Kein scaleTerrainUVs Terrain.j3md nutzt TexNScale direkt
Geometry geo = new Geometry("terrain", terrainMesh);
geo.setLocalTranslation(-8, 0, -8); // Terrain zentriert bei Ursprung
TerrainLodControl lod = new TerrainLodControl(tq, cam);
tq.addControl(lod);
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;
initSplatmap();
tq.setMaterial(buildTerrainMaterial());
return tq;
}
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
private void initSplatmap() {
if (loadedMapData != null) {
splatR = loadedMapData.splatR.clone();
splatG = loadedMapData.splatG.clone();
splatB = loadedMapData.splatB.clone();
// Ältere Maps haben splatR noch auf 0 (altes falsches Mapping).
// Alpha.R=0 → tex1*0=schwarz. Wenn R überall Null, auf 255 (=Gras) initialisieren.
boolean rAllZero = true;
for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } }
if (rAllZero) Arrays.fill(splatR, (byte) 255);
} else {
splatR = new byte[SPLAT_SIZE * SPLAT_SIZE];
splatG = new byte[SPLAT_SIZE * SPLAT_SIZE];
splatB = new byte[SPLAT_SIZE * SPLAT_SIZE];
Arrays.fill(splatR, (byte) 255); // R=1 → Tex1 (Gras) überall sichtbar
}
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;
splatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4);
for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) {
splatBuf.put(splatR[i]);
splatBuf.put(splatG[i]);
splatBuf.put(splatB[i]);
splatBuf.put((byte) 0);
}
splatBuf.flip();
splatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, splatBuf);
splatTex = new Texture2D(splatImage);
splatTex.setWrap(Texture.WrapMode.EdgeClamp);
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
splatTex.setMagFilter(Texture.MagFilter.Bilinear);
}
private Geometry buildGridOverlay() {
// Einfaches Wireframe-Duplikat des Terrains als Gitter
Geometry grid = new Geometry("grid", terrainMesh);
grid.setLocalTranslation(-8, 0.02f, -8);
private Material buildTerrainMaterial() {
Material mat = new Material(assets, "Common/MatDefs/Terrain/Terrain.j3md");
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;
// Terrain.j3md Shader: outColor = tex1*alpha.r → mix(tex2,g) → mix(tex3,b)
// d.h. Alpha.R = Tex1-Helligkeit (immer 1), Alpha.G = Tex2-Blend, Alpha.B = Tex3-Blend
Texture tex1 = loadOrFallback("Textures/Terrain/splat/grass.jpg",
new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
Texture tex2 = loadOrFallback("Textures/Terrain/splat/road.jpg",
new ColorRGBA(0.55f, 0.50f, 0.40f, 1f));
Texture tex3 = loadOrFallback("Textures/Terrain/splat/Gravel.jpg",
new ColorRGBA(0.45f, 0.35f, 0.25f, 1f));
for (Texture t : List.of(tex1, tex2, tex3)) {
t.setWrap(Texture.WrapMode.Repeat);
}
// Skalierung: 512 Kacheln über 4096 WE = 1 Kachel pro 8 WE (Zellgröße)
mat.setTexture("Tex1", tex1); mat.setFloat("Tex1Scale", 512f);
mat.setTexture("Tex2", tex2); mat.setFloat("Tex2Scale", 512f);
mat.setTexture("Tex3", tex3); mat.setFloat("Tex3Scale", 512f);
mat.setTexture("Alpha", splatTex);
return mat;
}
// ── Update-Schleife ──────────────────────────────────────────────────────
private Texture loadOrFallback(String path, ColorRGBA color) {
try {
return assets.loadTexture(path);
} catch (Exception e) {
ByteBuffer buf = BufferUtils.createByteBuffer(4);
buf.put((byte)(color.r * 255));
buf.put((byte)(color.g * 255));
buf.put((byte)(color.b * 255));
buf.put((byte)(color.a * 255));
buf.flip();
return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf));
}
}
// ── Splatmap malen ────────────────────────────────────────────────────────
/**
* Malt Textur-Gewichte auf die Splatmap.
* Shader-Mapping: Alpha.R=Tex1-Helligkeit (fest 1), Alpha.G=Tex2(Fels)-Blend, Alpha.B=Tex3(Erde)-Blend.
* @param textureIndex 0=Gras(Reset: G→0,B→0), 1=Fels(G→1,B→0), 2=Erde(G→0,B→1)
*/
private void applyTexturePaint(Vector3f contact, float strength, int textureIndex) {
float radius = (float) input.textureTool.brushRadius.getValue();
int centerPX = Math.round((contact.x + WORLD_HALF) / SPLAT_WE_PER_PX);
int centerPZ = Math.round((contact.z + WORLD_HALF) / SPLAT_WE_PER_PX);
int pixR = (int) Math.ceil(radius / SPLAT_WE_PER_PX);
boolean changed = false;
for (int dz = -pixR; dz <= pixR; dz++) {
int pz = centerPZ + dz;
if (pz < 0 || pz >= SPLAT_SIZE) continue;
for (int dx = -pixR; dx <= pixR; dx++) {
int px = centerPX + dx;
if (px < 0 || px >= SPLAT_SIZE) continue;
float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
if (distWE >= radius) continue;
float t = distWE / radius;
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
float blend = strength * falloff;
int idx = pz * SPLAT_SIZE + px;
// R bleibt immer 1 (tex1*R = tex1); G und B sind unabhängige mix()-Faktoren
float curG = (splatG[idx] & 0xFF) / 255f;
float curB = (splatB[idx] & 0xFF) / 255f;
// Zielwerte je Textur
float tgG = (textureIndex == 1) ? 1f : 0f; // Fels: G→1
float tgB = (textureIndex == 2) ? 1f : 0f; // Erde: B→1
// textureIndex==0 (Gras/Reset): beide Ziele 0
float newG = curG + (tgG - curG) * blend;
float newB = curB + (tgB - curB) * blend;
splatR[idx] = (byte) 255; // R immer voll
splatG[idx] = (byte) Math.round(newG * 255f);
splatB[idx] = (byte) Math.round(newB * 255f);
int bi = idx * 4;
splatBuf.put(bi, splatR[idx]);
splatBuf.put(bi + 1, splatG[idx]);
splatBuf.put(bi + 2, splatB[idx]);
changed = true;
}
}
if (changed) {
splatBuf.rewind();
splatImage.setUpdateNeeded();
}
}
// ── Update-Schleife ───────────────────────────────────────────────────────
@Override
public void update(float tpf) {
updateCamera(tpf);
processEdits();
processUpperLayerEdits();
processTextureEdits();
updateBrushIndicator();
if (input.saveRequested) {
input.saveRequested = false;
performSave();
}
}
private static final int MAX_EDITS_PER_FRAME = 2;
private void processTextureEdits() {
SharedInput.TextureEdit edit;
int processed = 0;
while ((edit = input.textureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
processed++;
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);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) continue;
Vector3f contact = hits.getClosestCollision().getContactPoint();
int texIdx = (edit.action() > 0)
? input.textureTool.textureIndex.getSelectedIndex()
: 0; // Rechtsklick = Gras (zurücksetzen)
applyTexturePaint(contact, (float) input.textureTool.brushStrength.getValue(), texIdx);
}
}
// ── Speichern ─────────────────────────────────────────────────────────────
private void performSave() {
try {
MapData data = new MapData();
float[] hmap = terrain.getHeightMap();
if (hmap != null) {
System.arraycopy(hmap, 0, data.terrainHeight, 0,
Math.min(hmap.length, data.terrainHeight.length));
}
if (upperLayerState != null) {
UpperLayerData ud = upperLayerState.data;
System.arraycopy(ud.topHeight, 0, data.upperTop, 0, data.upperTop.length);
System.arraycopy(ud.bottomHeight, 0, data.upperBottom, 0, data.upperBottom.length);
for (int i = 0; i < data.upperHole.length; i++) {
data.upperHole[i] = ud.hole[i] ? (byte) 1 : (byte) 0;
}
}
if (splatR != null) {
System.arraycopy(splatR, 0, data.splatR, 0, data.splatR.length);
System.arraycopy(splatG, 0, data.splatG, 0, data.splatG.length);
System.arraycopy(splatB, 0, data.splatB, 0, data.splatB.length);
}
if (placedObjectState != null) {
System.arraycopy(placedObjectState.getDensityMap(), 0,
data.grassDensity, 0, data.grassDensity.length);
}
MapIO.save(data);
input.saveStatusMsg = "Gespeichert: " + MapIO.getMapPath();
System.out.println("[TerrainEditor] " + input.saveStatusMsg);
} catch (IOException e) {
input.saveStatusMsg = "Fehler beim Speichern: " + e.getMessage();
System.err.println("[TerrainEditor] " + input.saveStatusMsg);
}
}
// ── Brush-Indikator ───────────────────────────────────────────────────────
private void updateBrushIndicator() {
float mx = input.mouseScreenX;
float my = input.mouseScreenY;
if (mx < 0) {
brushIndicator.setCullHint(Spatial.CullHint.Always);
return;
}
float jmeX = mx * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - my * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
Vector3f contactPoint = null;
float brushRadius = 0f;
int layer = input.activeLayer;
if (layer == 0 || layer == 4) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
contactPoint = hits.getClosestCollision().getContactPoint();
brushRadius = (layer == 0)
? (float) input.heightTool.brushRadius.getValue()
: (float) input.textureTool.brushRadius.getValue();
}
} else {
CollisionResults hits = new CollisionResults();
if (upperLayerState != null && input.upperLayerVisible) {
upperLayerState.getUpperNode().collideWith(ray, hits);
}
if (hits.size() > 0) {
contactPoint = hits.getClosestCollision().getContactPoint();
} else {
CollisionResults terrainHits = new CollisionResults();
terrain.collideWith(ray, terrainHits);
if (terrainHits.size() > 0)
contactPoint = terrainHits.getClosestCollision().getContactPoint();
}
brushRadius = (layer == 1)
? (float) input.upperHeightTool.brushRadius.getValue()
: (float) input.holeTool.brushRadius.getValue();
}
if (contactPoint != null) {
brushIndicator.setLocalTranslation(contactPoint.x, contactPoint.y + 0.5f, contactPoint.z);
brushIndicator.setLocalScale(brushRadius, 1f, brushRadius);
brushIndicator.setCullHint(Spatial.CullHint.Inherit);
} else {
brushIndicator.setCullHint(Spatial.CullHint.Always);
}
}
// ── Upper-Layer-Edits ─────────────────────────────────────────────────────
private void processUpperLayerEdits() {
if (upperLayerState == null) return;
SharedInput.UpperLayerEdit edit;
int processed = 0;
while ((edit = input.upperLayerEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
processed++;
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);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults();
if (input.upperLayerVisible) {
upperLayerState.getUpperNode().collideWith(ray, hits);
}
if (hits.size() == 0) terrain.collideWith(ray, hits);
if (hits.size() == 0) continue;
Vector3f contact = hits.getClosestCollision().getContactPoint();
if (input.activeLayer == 1) {
upperLayerState.applyHeightEdit(contact.x, contact.z, edit.action());
} else if (input.activeLayer == 2) {
upperLayerState.applyHoleEdit(contact.x, contact.z);
}
}
}
// ── Terrain-Höhen-Edits ───────────────────────────────────────────────────
private void processEdits() {
SharedInput.TerrainEdit edit;
int processed = 0;
while ((edit = input.editQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
processed++;
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);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) continue;
Vector3f contact = hits.getClosestCollision().getContactPoint();
int mode = input.heightTool.mode.getSelectedIndex();
if (mode == HeightTool.MODE_SMOOTH) {
smoothHeight(contact);
} else {
float delta = (float) input.heightTool.brushStrength.getValue() * edit.action();
modifyHeight(contact, delta, mode);
}
}
if (processed > 0) terrain.updateModelBound();
}
// ── Höhen-Werkzeug ────────────────────────────────────────────────────────
private void modifyHeight(Vector3f worldContact, float delta, int mode) {
float radius = (float) input.heightTool.brushRadius.getValue();
int r = (int) Math.ceil(radius);
int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
List<Vector2f> locs = new ArrayList<>();
List<Float> deltas = new ArrayList<>();
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz);
if (dist >= radius) continue;
float t = dist / radius;
float falloff;
switch (mode) {
case HeightTool.MODE_SINUS ->
falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
case HeightTool.MODE_PLATEAU -> {
float edge = 0.85f;
if (t < edge) {
falloff = 1f;
} else {
float s = (t - edge) / (1f - edge);
falloff = 1f - s * s * s * s;
}
}
default -> {
float u = 1f - t;
falloff = u * u * u * u;
}
}
locs.add(new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
deltas.add(delta * falloff);
}
}
if (!locs.isEmpty()) {
terrain.adjustHeight(locs, deltas);
if (upperLayerState != null) upperLayerState.adjustHeightsWithTerrain(locs, deltas);
if (placedObjectState != null) placedObjectState.adjustObjectHeights(locs, deltas);
}
}
private void smoothHeight(Vector3f worldContact) {
float radius = (float) input.heightTool.brushRadius.getValue();
float strength = (float) input.heightTool.brushStrength.getValue();
int r = (int) Math.ceil(radius);
int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
float sum = 0f; int count = 0;
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
if (FastMath.sqrt(dx * dx + dz * dz) >= radius) continue;
sum += terrain.getHeightmapHeight(
new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
count++;
}
}
if (count == 0) return;
float avg = sum / count;
List<Vector2f> locs = new ArrayList<>();
List<Float> newHts = new ArrayList<>();
List<Float> deltas = new ArrayList<>();
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz);
if (dist >= radius) continue;
float falloff = 1f - dist / radius;
float wx = vx - TERRAIN_SIZE * 0.5f;
float wz = vz - TERRAIN_SIZE * 0.5f;
float curH = terrain.getHeightmapHeight(new Vector2f(wx, wz));
float newH = curH + (avg - curH) * falloff * strength * 3f;
locs.add(new Vector2f(wx, wz));
newHts.add(newH);
deltas.add(newH - curH);
}
}
if (!locs.isEmpty()) {
terrain.setHeight(locs, newHts);
if (upperLayerState != null) upperLayerState.adjustHeightsWithTerrain(locs, deltas);
if (placedObjectState != null) placedObjectState.adjustObjectHeights(locs, deltas);
}
}
// ── Kamera ────────────────────────────────────────────────────────────────
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;
camYaw += delta[0] * MOUSE_SENS;
camPitch -= delta[1] * MOUSE_SENS;
camPitch = FastMath.clamp(camPitch,
-FastMath.HALF_PI + 0.05f,
@@ -192,23 +623,41 @@ public class TerrainEditorState extends BaseAppState {
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(cam.getDirection().mult(speed));
if (input.backward) camPos.addLocal(cam.getDirection().mult(-speed));
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;
Vector3f lft = cam.getLeft().clone().setY(0);
if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
if (input.left) camPos.addLocal(lft.mult(speed));
if (input.right) camPos.subtractLocal(lft.mult(speed));
if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf);
if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf);
cam.setLocation(camPos);
}
private void orbitAroundTerrain(float angle) {
Vector3f pivot = findTerrainIntersection();
if (pivot == null) return;
Vector3f offset = camPos.subtract(pivot);
Quaternion rot = new Quaternion().fromAngleAxis(angle, Vector3f.UNIT_Y);
camPos.set(pivot.add(rot.mult(offset)));
camYaw += angle;
}
private Vector3f findTerrainIntersection() {
float cx = cam.getWidth() * 0.5f;
float cy = cam.getHeight() * 0.5f;
Vector3f near = cam.getWorldCoordinates(new Vector2f(cx, cy), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(cx, cy), 1f);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
return hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null;
}
private void applyCameraTransform() {
Quaternion yawQ = new Quaternion().fromAngleAxis(camYaw, Vector3f.UNIT_Y);
Quaternion pitchQ = new Quaternion().fromAngleAxis(camPitch, Vector3f.UNIT_X);
@@ -216,78 +665,93 @@ public class TerrainEditorState extends BaseAppState {
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);
// ── Hilfsobjekte ─────────────────────────────────────────────────────────
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Vector3f dir = far.subtract(near).normalizeLocal();
private Geometry buildWater() {
float half = TERRAIN_SIZE * 0.5f;
Geometry water = new Geometry("water", new Quad(TERRAIN_SIZE, TERRAIN_SIZE));
water.rotate(-FastMath.HALF_PI, 0, 0);
water.setLocalTranslation(-half, 0.01f, half);
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);
}
}
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;
}
// ── Höhen-Werkzeug ───────────────────────────────────────────────────────
private Geometry buildGrid() {
float step = 8f;
float half = TERRAIN_SIZE * 0.5f;
int divs = (int)(TERRAIN_SIZE / step);
float y = 1.05f;
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;
int linesPerAxis = divs + 1;
int totalVerts = linesPerAxis * 4;
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;
}
}
FloatBuffer pos = BufferUtils.createFloatBuffer(totalVerts * 3);
for (int i = 0; i <= divs; i++) {
float z = -half + i * step;
pos.put(-half).put(y).put(z);
pos.put( half).put(y).put(z);
}
updateTerrainMesh();
for (int i = 0; i <= divs; i++) {
float x = -half + i * step;
pos.put(x).put(y).put(-half);
pos.put(x).put(y).put( half);
}
int totalLines = linesPerAxis * 2;
IntBuffer idx = BufferUtils.createIntBuffer(totalLines * 2);
for (int i = 0; i < totalLines; i++) {
idx.put(i * 2);
idx.put(i * 2 + 1);
}
Mesh mesh = new Mesh();
mesh.setMode(Mesh.Mode.Lines);
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Index, 2, idx);
mesh.updateBound();
Geometry geo = new Geometry("terrainGrid", mesh);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.5f, 0.5f, 0.5f, 0.4f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
geo.setMaterial(mat);
return geo;
}
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);
}
private Geometry buildBrushIndicator() {
int segments = 48;
FloatBuffer pos = BufferUtils.createFloatBuffer((segments + 1) * 3);
pos.put(0f).put(0f).put(0f);
for (int i = 0; i < segments; i++) {
float a = FastMath.TWO_PI * i / segments;
pos.put(FastMath.cos(a)).put(0f).put(FastMath.sin(a));
}
// 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);
}
IntBuffer idx = BufferUtils.createIntBuffer(segments * 3);
for (int i = 0; i < segments; i++) {
idx.put(0);
idx.put(1 + i);
idx.put(1 + (i + 1) % segments);
}
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
terrainMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
terrainMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
terrainMesh.updateBound();
terrainGeo.updateModelBound();
Geometry geo = new Geometry("brushIndicator", mesh);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 0f, 0f, 0.35f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setDepthTest(false);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
geo.setMaterial(mat);
geo.setCullHint(Spatial.CullHint.Always);
return geo;
}
}

View File

@@ -0,0 +1,671 @@
package de.blight.editor.state;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.imageio.ImageIO;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.bounding.BoundingBox;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.post.SceneProcessor;
import com.jme3.profile.AppProfiler;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
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.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.shape.Sphere;
import com.jme3.shadow.DirectionalLightShadowRenderer;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import com.jme3.util.SkyFactory;
import de.blight.editor.FrameTransfer;
import de.blight.editor.SharedInput;
import de.blight.editor.tree.TreeMeshBuilder;
import de.blight.editor.tree.TreeParams;
/**
* JME3-Zustand für den prozeduralen Baum-Generator.
*
* Ablauf pro Request:
* 1. HD-Mesh + LD-Mesh generieren
* 2. Impostor-Textur per Offscreen-Capture (SceneProcessor.postFrame) erzeugen
* 3. LOD-Node mit TreeLodControl assemblieren
* 4. Optional: .j3o-Export via BinaryExporter
*/
public class TreeGeneratorState extends BaseAppState {
private static final int IMPOSTOR_SIZE = 512;
private static final int PREVIEW_SIZE = 1024;
private static final Path ASSET_ROOT = Paths.get("editor-assets");
private final SharedInput input;
private SimpleApplication app;
private Node rootNode;
private AssetManager assets;
// ── Preview-Viewport ─────────────────────────────────────────────────────
private ViewPort previewVP;
private FrameBuffer previewFB;
private FrameTransfer previewTransfer;
private Node previewScene;
private Node previewTreeHolder;
private DirectionalLight previewSunLight;
private final Vector3f previewTarget = new Vector3f(0f, 5f, 0f);
private float previewCamDist = 20f;
private int currentPreviewW = PREVIEW_SIZE;
private int currentPreviewH = PREVIEW_SIZE;
// ── Offscreen-Capture-Kontext ─────────────────────────────────────────────
private SharedInput.TreeGenRequest pendingRequest = null;
private Node pendingHdNode = null;
private Node pendingLdNode = null;
private TreeMeshBuilder.MeshResult pendingHdResult = null;
private Material pendingBarkMat = null;
private Material pendingLeafMat = null;
private ViewPort captureVP = null;
private FrameBuffer captureFB = null;
private Texture2D captureTex = null;
private volatile boolean captureReady = false; // vom SceneProcessor gesetzt
public TreeGeneratorState(SharedInput input) { this.input = input; }
// ── Lifecycle ─────────────────────────────────────────────────────────────
@SuppressWarnings("deprecation")
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.rootNode = this.app.getRootNode();
this.assets = app.getAssetManager();
try {
assets.registerLocator(ASSET_ROOT.toAbsolutePath().toString(), FileLocator.class);
} catch (Exception ignored) {}
// Dedizierter Offscreen-Viewport für die Vorschau
previewFB = buildFrameBuffer(PREVIEW_SIZE, PREVIEW_SIZE);
Camera previewCam = new Camera(PREVIEW_SIZE, PREVIEW_SIZE);
previewCam.setFrustumPerspective(45f, 1f, 0.1f, 5000f);
previewVP = this.app.getRenderManager().createPostView("treePreview", previewCam);
previewVP.setOutputFrameBuffer(previewFB);
previewVP.setBackgroundColor(new ColorRGBA(0.50f, 0.72f, 0.95f, 1f));
previewVP.setClearFlags(true, true, true);
previewScene = new Node("previewScene");
previewSunLight = new DirectionalLight(
new Vector3f(-0.45f, -1.0f, -0.3f).normalizeLocal(),
new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
previewScene.addLight(previewSunLight);
previewScene.addLight(new DirectionalLight(
new Vector3f(0.4f, -0.6f, -0.8f).normalizeLocal(),
new ColorRGBA(0.35f, 0.40f, 0.55f, 1f)));
previewScene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.30f, 1f)));
previewTreeHolder = new Node("treeHolder");
previewScene.attachChild(previewTreeHolder);
previewScene.attachChild(buildPreviewGround());
previewScene.attachChild(buildPreviewSky());
previewVP.attachScene(previewScene);
DirectionalLightShadowRenderer shadowRenderer =
new DirectionalLightShadowRenderer(assets, 2048, 1);
shadowRenderer.setLight(previewSunLight);
shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
shadowRenderer.setShadowIntensity(0.55f);
shadowRenderer.setShadowZExtend(80f);
previewVP.addProcessor(shadowRenderer);
previewTransfer = new FrameTransfer(input.treePreviewImage);
previewVP.addProcessor(previewTransfer);
}
/**
* Wird von EzTreeState aufgerufen, um einen extern generierten Baum
* im gemeinsamen Vorschau-Viewport anzuzeigen.
*/
public void setPreviewContent(com.jme3.scene.Node node, float camDist,
com.jme3.math.Vector3f target) {
previewTreeHolder.detachAllChildren();
if (node != null) previewTreeHolder.attachChild(node);
this.previewCamDist = camDist;
this.previewTarget.set(target);
}
@Override protected void cleanup(Application app) {
if (previewVP != null) {
this.app.getRenderManager().removePostView(previewVP);
previewVP = null;
}
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
// ── Update-Schleife ───────────────────────────────────────────────────────
@Override
public void update(float tpf) {
// 1. Szenen-Änderungen zuerst (bevor updateGeometricState)
if (pendingRequest != null && captureReady) {
finishCapture();
} else if (pendingRequest == null) {
SharedInput.TreeGenRequest req = input.treeGenQueue.poll();
if (req != null) startGeneration(req);
}
// 2. Framebuffer-Resize falls JavaFX eine neue Größe gemeldet hat
int reqW = Math.max(64, input.treePreviewW);
int reqH = Math.max(64, input.treePreviewH);
if (previewVP != null
&& (Math.abs(reqW - currentPreviewW) > 8 || Math.abs(reqH - currentPreviewH) > 8)) {
resizePreviewViewport(reqW, reqH);
}
// 3. Kamera-Orbit + updateGeometricState immer zuletzt
// nach allen Szenenänderungen dieser Frame
if (previewVP != null) {
float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD;
float rotX = input.treePreviewRotX * FastMath.DEG_TO_RAD;
float dist = previewCamDist * input.treePreviewZoom;
float cosX = FastMath.cos(rotX);
Camera c = previewVP.getCamera();
c.setLocation(new Vector3f(
FastMath.sin(rotY) * cosX * dist,
previewTarget.y + FastMath.sin(rotX) * dist,
FastMath.cos(rotY) * cosX * dist));
c.lookAt(previewTarget, Vector3f.UNIT_Y);
previewScene.updateGeometricState();
}
}
// ── Framebuffer-Helpers ───────────────────────────────────────────────────
private FrameBuffer buildFrameBuffer(int w, int h) {
FrameBuffer fb = new FrameBuffer(w, h, 1);
fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8));
fb.setDepthTexture(new Texture2D(w, h, Image.Format.Depth));
return fb;
}
private void resizePreviewViewport(int newW, int newH) {
currentPreviewW = newW;
currentPreviewH = newH;
// Alten FrameTransfer entfernen, alten Framebuffer freigeben
previewVP.removeProcessor(previewTransfer);
try { previewFB.dispose(); } catch (Exception ignored) {}
// Neuen Framebuffer setzen
previewFB = buildFrameBuffer(newW, newH);
previewVP.setOutputFrameBuffer(previewFB);
// Kamera-Aspect anpassen
Camera cam = previewVP.getCamera();
cam.resize(newW, newH, true);
cam.setFrustumPerspective(45f, (float) newW / newH, 0.1f, 5000f);
// Neue WritableImage + neuen FrameTransfer
javafx.scene.image.WritableImage newImg =
new javafx.scene.image.WritableImage(newW, newH);
input.treePreviewImage = newImg;
previewTransfer = new FrameTransfer(newImg);
previewVP.addProcessor(previewTransfer);
// JavaFX signalisieren (nach dem Schreiben von treePreviewImage)
input.treePreviewResized = true;
}
// ── Phase 1: Generierung + Capture-Setup ──────────────────────────────────
@SuppressWarnings("deprecation")
private void startGeneration(SharedInput.TreeGenRequest req) {
previewTreeHolder.detachAllChildren();
cleanupCapture();
TreeParams p = req.params();
TreeMeshBuilder builder = new TreeMeshBuilder();
TreeMeshBuilder.MeshResult hd = builder.build(p, 1.0f);
TreeMeshBuilder.MeshResult ld = builder.build(p, 0.0f);
Material barkMat = buildBarkMaterial(p);
Material leafMat = buildLeafMaterial(p);
Node hdNode = makeTreeNode(hd, barkMat, leafMat, "hd");
Node ldNode = makeTreeNode(ld, barkMat.clone(), leafMat.clone(), "ld");
// Capture-Viewport aufbauen
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
captureFB.addColorTexture(captureTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
captureVP = buildCaptureViewPort(hdNode, hd.bounds(), captureFB);
captureReady = false;
pendingRequest = req;
pendingHdNode = hdNode;
pendingLdNode = ldNode;
pendingHdResult = hd;
pendingBarkMat = barkMat;
pendingLeafMat = leafMat;
input.treeGenStatusMsg = "Rendere Impostor…";
}
// ── Phase 2: Capture abschließen ──────────────────────────────────────────
private void finishCapture() {
// Pixel aus Framebuffer lesen
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
app.getRenderer().readFrameBuffer(captureFB, pixels);
cleanupCapture();
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + pendingRequest.exportName());
// HD-Mesh im Dialog-Preview anzeigen (keine LOD-Umschaltung, kein Welt-Platzierung)
Node previewTree = makeTreeNode(pendingHdResult,
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
previewTreeHolder.detachAllChildren();
previewTreeHolder.attachChild(previewTree);
BoundingBox bb = pendingHdResult.bounds();
previewTarget.set(0f, bb.getCenter().y, 0f);
previewCamDist = Math.max(bb.getXExtent(),
Math.max(bb.getYExtent(), bb.getZExtent())) * 3f;
if (pendingRequest.exportAfter()) {
Node treeNode = assembleLodNode(impostorTex);
exportTree(treeNode, pendingRequest.exportName());
} else {
input.treeGenStatusMsg = "Vorschau: '" + pendingRequest.exportName() + "'";
}
pendingRequest = null;
pendingHdNode = null;
pendingLdNode = null;
pendingHdResult = null;
pendingBarkMat = null;
pendingLeafMat = null;
}
// ── LOD-Aufbau ────────────────────────────────────────────────────────────
private Node assembleLodNode(Texture2D impostorTex) {
Node root = new Node("GeneratedTree_" + pendingRequest.exportName());
root.attachChild(pendingHdNode);
root.attachChild(pendingLdNode);
Node lod2 = makeImpostorNode(pendingHdResult.bounds(), impostorTex);
root.attachChild(lod2);
// Nur LOD0 initial sichtbar; Control steuert je nach Distanz
pendingLdNode.setCullHint(Spatial.CullHint.Always);
lod2.setCullHint(Spatial.CullHint.Always);
root.addControl(new TreeLodControl(app.getCamera(),
pendingHdNode, pendingLdNode, lod2, 60f, 200f));
return root;
}
private Node makeImpostorNode(BoundingBox bb, Texture2D tex) {
float h = bb.getYExtent() * 2f;
float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f;
float size = Math.max(h, w);
float yOff = bb.getCenter().y + 2f; // passt zur Baum-Offset-Y
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
if (tex != null) mat.setTexture("ColorMap", tex);
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
Node n = new Node("lod2");
n.attachChild(buildBillboardQuad("quad_a", 0f, yOff, size, mat));
n.attachChild(buildBillboardQuad("quad_b", FastMath.HALF_PI, yOff, size, mat.clone()));
n.setQueueBucket(RenderQueue.Bucket.Transparent);
return n;
}
private Geometry buildBillboardQuad(String name, float yRot, float yCent,
float size, Material mat) {
float hw = size * 0.5f;
float hh = size * 0.5f;
float cos = FastMath.cos(yRot);
float sin = FastMath.sin(yRot);
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{
-hw*cos, yCent-hh, -hw*sin,
hw*cos, yCent-hh, hw*sin,
hw*cos, yCent+hh, hw*sin,
-hw*cos, yCent+hh, -hw*sin
});
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ 0,0, 1,0, 1,1, 0,1 });
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
mesh.updateBound();
Geometry g = new Geometry(name, mesh);
g.setMaterial(mat);
return g;
}
// ── Material-Factories ────────────────────────────────────────────────────
private Material buildBarkMaterial(TreeParams p) {
try {
Material mat = new Material(assets, "MatDefs/Tree.j3md");
mat.setColor("Diffuse", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f));
mat.setFloat("WindStrength", 0.15f);
mat.setFloat("WindSpeed", 0.5f);
if (p.barkTexture != null) {
try {
mat.setTexture("BarkMap", assets.loadTexture(p.barkTexture));
mat.setBoolean("HasBarkMap", true);
} catch (Exception tex) {
System.err.println("[TreeGenerator] Bark-Textur nicht gefunden: " + p.barkTexture);
}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f));
return mat;
}
}
private Material buildLeafMaterial(TreeParams p) {
try {
Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
mat.setColor("Diffuse", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f));
mat.setFloat("WindStrength", 0.30f);
mat.setFloat("WindSpeed", 0.7f);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
if (p.leafTexture != null) {
try {
mat.setTexture("LeafMap", assets.loadTexture(p.leafTexture));
mat.setBoolean("HasLeafMap", true);
} catch (Exception tex) {
System.err.println("[TreeGenerator] Blatt-Textur nicht gefunden: " + p.leafTexture);
}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
}
// ── Tree-Node aus MeshResult ──────────────────────────────────────────────
private Node makeTreeNode(TreeMeshBuilder.MeshResult r,
Material barkMat, Material leafMat, String tag) {
Node n = new Node("tree_" + tag);
if (r.bark().getVertexCount() > 0) {
Geometry g = new Geometry("bark_" + tag, r.bark());
g.setMaterial(barkMat);
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
n.attachChild(g);
}
if (r.leaves().getVertexCount() > 0) {
Geometry g = new Geometry("leaves_" + tag, r.leaves());
g.setMaterial(leafMat);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
n.attachChild(g);
}
return n;
}
// ── Offscreen-ViewPort ────────────────────────────────────────────────────
private ViewPort buildCaptureViewPort(Node treeNode, BoundingBox bb, FrameBuffer fb) {
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
float dist = extent * 3.0f;
cam.setLocation(center.add(0f, 0f, dist));
cam.lookAt(center, Vector3f.UNIT_Y);
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
ViewPort vp = app.getRenderManager()
.createPostView("impostorCap_" + System.nanoTime(), cam);
vp.setOutputFrameBuffer(fb);
vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
vp.setClearFlags(true, true, true);
// Capture-Szene: Kopien der Geometrien + Beleuchtung
Node scene = new Node("captureScene");
scene.addLight(new DirectionalLight(
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
Node capTree = cloneForCapture(treeNode);
scene.attachChild(capTree);
vp.attachScene(scene);
scene.updateGeometricState();
// Einmaliger SceneProcessor signalisiert Fertigstellung
vp.addProcessor(new SceneProcessor() {
@Override public void initialize(RenderManager rm, ViewPort v) {}
@Override public void reshape(ViewPort v, int w, int h) {}
@Override public boolean isInitialized() { return true; }
@Override public void preFrame(float t) {}
@Override public void postQueue(RenderQueue rq) {}
@Override public void cleanup() {}
@Override public void setProfiler(AppProfiler profiler) {}
@Override
public void postFrame(FrameBuffer out) {
vp.removeProcessor(this);
captureReady = true;
}
});
return vp;
}
private Node cloneForCapture(Node src) {
Node copy = new Node(src.getName() + "_cap");
copy.setLocalTranslation(src.getLocalTranslation());
for (Spatial child : src.getChildren()) {
if (child instanceof Geometry g) {
Geometry gc = new Geometry(g.getName() + "_c", g.getMesh());
gc.setMaterial(g.getMaterial().clone());
copy.attachChild(gc);
}
}
return copy;
}
// ── Impostor-PNG speichern ────────────────────────────────────────────────
private Texture2D saveImpostor(ByteBuffer pixels, String name) {
try {
pixels.rewind();
BufferedImage img = new BufferedImage(
IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
int r = pixels.get() & 0xFF;
int g = pixels.get() & 0xFF;
int b = pixels.get() & 0xFF;
int a = pixels.get() & 0xFF;
img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
}
}
Path texDir = ASSET_ROOT.resolve("textures");
Files.createDirectories(texDir);
File pngFile = texDir.resolve(name + ".png").toFile();
ImageIO.write(img, "PNG", pngFile);
System.out.println("[TreeGenerator] Impostor: " + pngFile.getAbsolutePath());
try {
return (Texture2D) assets.loadTexture("textures/" + name + ".png");
} catch (Exception loadEx) {
pixels.rewind();
Image jmeImg = new Image(Image.Format.RGBA8, IMPOSTOR_SIZE, IMPOSTOR_SIZE,
pixels, null, com.jme3.texture.image.ColorSpace.sRGB);
return new Texture2D(jmeImg);
}
} catch (IOException e) {
System.err.println("[TreeGenerator] Impostor-Fehler: " + e.getMessage());
return null;
}
}
// ── .j3o-Export ───────────────────────────────────────────────────────────
private void exportTree(Node treeNode, String name) {
try {
Path modelDir = ASSET_ROOT.resolve("models");
Files.createDirectories(modelDir);
File out = modelDir.resolve("GeneratedTree_" + name + ".j3o").toFile();
// Strip runtime controls before export — they lack no-arg constructors
// and cannot be deserialized by BinaryImporter.
while (treeNode.getNumControls() > 0)
treeNode.removeControl(treeNode.getControl(0));
BinaryExporter.getInstance().save(treeNode, out);
input.treeGenStatusMsg = "Exportiert: " + out.getName();
input.refreshAssets = true;
System.out.println("[TreeGenerator] Exportiert: " + out.getAbsolutePath());
} catch (IOException e) {
input.treeGenStatusMsg = "Export-Fehler: " + e.getMessage();
System.err.println("[TreeGenerator] Export-Fehler: " + e.getMessage());
}
}
// ── Aufräumen ─────────────────────────────────────────────────────────────
private void cleanupCapture() {
if (captureVP != null) {
app.getRenderManager().removePostView(captureVP);
captureVP = null;
}
if (captureFB != null) {
try { captureFB.dispose(); } catch (Exception ignored) {}
captureFB = null;
}
captureTex = null;
captureReady = false;
}
// ── LOD-Control ───────────────────────────────────────────────────────────
private static final class TreeLodControl extends AbstractControl {
private final Camera cam;
private final Node lod0, lod1, lod2;
private final float d01sq, d12sq;
TreeLodControl(Camera cam, Node l0, Node l1, Node l2, float d01, float d12) {
this.cam = cam;
this.lod0 = l0; this.lod1 = l1; this.lod2 = l2;
this.d01sq = d01 * d01;
this.d12sq = d12 * d12;
}
@Override
protected void controlUpdate(float tpf) {
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
lod1.setCullHint(dSq>=d01sq && dSq<d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
}
// ── Vorschau-Boden (groß, Gras-Textur) ──────────────────────────────────
private Geometry buildPreviewGround() {
float size = 600f;
float tiles = 30f; // UV-Wiederholungen
// Eigenes Mesh mit gekachelten UVs (Quad unterstützt kein Tiling)
com.jme3.scene.Mesh mesh = new com.jme3.scene.Mesh();
float h = size * 0.5f;
mesh.setBuffer(VertexBuffer.Type.Position, 3,
new float[]{ -h,0,-h, h,0,-h, h,0,h, -h,0,h });
mesh.setBuffer(VertexBuffer.Type.Normal, 3,
new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 });
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
new float[]{ 0,0, tiles,0, tiles,tiles, 0,tiles });
mesh.setBuffer(VertexBuffer.Type.Index, 3,
new int[]{ 0,2,1, 0,3,2 });
mesh.updateBound();
Geometry ground = new Geometry("previewGround", mesh);
Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.45f, 0.14f, 1f));
mat.setColor("Ambient", new ColorRGBA(0.11f, 0.18f, 0.06f, 1f));
mat.setColor("Specular", ColorRGBA.Black);
mat.setFloat("Shininess", 0f);
try {
Texture grassTex = assets.loadTexture("Textures/gras.png");
grassTex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("DiffuseMap", grassTex);
} catch (Exception ignored) {
// Fallback auf Farbe, wenn Textur fehlt
}
ground.setMaterial(mat);
ground.setShadowMode(RenderQueue.ShadowMode.Receive);
return ground;
}
// ── Skybox (Kuppel) ───────────────────────────────────────────────────────
private Spatial buildPreviewSky() {
// Versuche zuerst SkyFactory mit einer Sphere-Map-Textur
String[] skyPaths = { "Textures/sky.png", "Textures/Sky.png", "Textures/skybox.png" };
for (String path : skyPaths) {
try {
Texture skyTex = assets.loadTexture(path);
return SkyFactory.createSky(assets, skyTex, SkyFactory.EnvMapType.SphereMap);
} catch (Exception ignored) {}
}
// Fallback: gefärbte Innenkugel als einfache Himmelskuppel
Sphere dome = new Sphere(16, 32, 800f, false, true);
Geometry sky = new Geometry("previewSky", dome);
sky.setQueueBucket(RenderQueue.Bucket.Sky);
sky.setCullHint(Spatial.CullHint.Never);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.50f, 0.72f, 0.95f, 1f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
sky.setMaterial(mat);
return sky;
}
}

View File

@@ -0,0 +1,74 @@
package de.blight.editor.state;
import java.util.Arrays;
/**
* Data arrays for the upper (mountain) layer.
*
* Grid: 512×512 cells → 513×513 vertices, covering 4096×4096 world units
* (8 world units per cell). World origin is at grid centre: vertex (256,256)
* maps to world (0,0).
*/
public class UpperLayerData {
public static final int CELLS = 512; // cells per axis
public static final int VERTS = 513; // vertices per axis (CELLS + 1)
/** Y of the top surface at each vertex [VERTS*VERTS]. */
public final float[] topHeight;
/** Y of the cave ceiling at each vertex [VERTS*VERTS]. */
public final float[] bottomHeight;
/** Whether a cell is an open hole (no geometry emitted) [CELLS*CELLS]. */
public final boolean[] hole;
/** Initiale Höhe der Gras-Oberfläche (muss mit TerrainEditorState übereinstimmen). */
public static final float INITIAL_TERRAIN_Y = 1f;
/** Dicke der Gesteinsschicht in Welteinheiten. */
public static final float LAYER_THICKNESS = 30f;
public UpperLayerData() {
topHeight = new float[VERTS * VERTS];
bottomHeight = new float[VERTS * VERTS];
hole = new boolean[CELLS * CELLS];
Arrays.fill(topHeight, INITIAL_TERRAIN_Y);
Arrays.fill(bottomHeight, INITIAL_TERRAIN_Y - LAYER_THICKNESS);
// hole[] defaults to false
}
// ── Convenience accessors ────────────────────────────────────────────────
/** Returns true when the cell is outside the grid (treated as solid). */
public boolean isHole(int cx, int cz) {
if (cx < 0 || cx >= CELLS || cz < 0 || cz >= CELLS) return false;
return hole[cx + cz * CELLS];
}
public float topAt(int vx, int vz) {
return topHeight[vx + vz * VERTS];
}
public float bottomAt(int vx, int vz) {
return bottomHeight[vx + vz * VERTS];
}
// ── World ↔ grid conversion ──────────────────────────────────────────────
/** World X/Z → nearest vertex index (clamped). */
public static int worldToVertexX(float wx) {
return Math.max(0, Math.min(VERTS - 1, Math.round((wx + 2048f) / 8f)));
}
public static int worldToVertexZ(float wz) {
return Math.max(0, Math.min(VERTS - 1, Math.round((wz + 2048f) / 8f)));
}
/** World X/Z → cell index (floored, clamped). */
public static int worldToCellX(float wx) {
return Math.max(0, Math.min(CELLS - 1, (int) ((wx + 2048f) / 8f)));
}
public static int worldToCellZ(float wz) {
return Math.max(0, Math.min(CELLS - 1, (int) ((wz + 2048f) / 8f)));
}
}

View File

@@ -0,0 +1,194 @@
package de.blight.editor.state;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
/**
* Builds a single triangle-list {@link Mesh} for one 16×16-cell chunk of the
* upper layer. UV coordinates tile once per 8-world-unit cell (= once per
* grid cell), matching the terrain grid. Vertical faces use the same scale
* so the texture does not stretch on tall walls.
*
* Winding convention (right-hand, JME3 default back-face cull):
* Top face (+Y normal) : FaceA indices (0,2,3),(0,3,1)
* Bot face (Y normal) : FaceB indices (0,1,3),(0,3,2)
* South wall (+Z normal): FaceA
* North wall (Z normal): FaceB
* East wall (+X normal): FaceB
* West wall (X normal): FaceA
*/
public final class UpperLayerMesher {
private static final int CPP = 16; // cells per chunk axis
private static final float CS = 8f; // cell size in world units
private static final float OFS = -2048f; // world origin offset
private UpperLayerMesher() {}
/**
* Builds a mesh for chunk (chunkX, chunkZ) where both indices are in
* [0, 31]. Returns {@code null} when every cell in the chunk is a hole.
*/
public static Mesh buildChunk(UpperLayerData d, int chunkX, int chunkZ) {
ArrayList<Float> pos = new ArrayList<>(CPP * CPP * 24 * 3);
ArrayList<Float> nor = new ArrayList<>(CPP * CPP * 24 * 3);
ArrayList<Float> uv = new ArrayList<>(CPP * CPP * 24 * 2);
ArrayList<Integer> idx = new ArrayList<>(CPP * CPP * 24);
int baseCX = chunkX * CPP;
int baseCZ = chunkZ * CPP;
for (int lz = 0; lz < CPP; lz++) {
for (int lx = 0; lx < CPP; lx++) {
int gcx = baseCX + lx;
int gcz = baseCZ + lz;
if (d.isHole(gcx, gcz)) continue;
float x0 = gcx * CS + OFS;
float x1 = (gcx + 1) * CS + OFS;
float z0 = gcz * CS + OFS;
float z1 = (gcz + 1) * CS + OFS;
float t00 = d.topAt(gcx, gcz);
float t10 = d.topAt(gcx + 1, gcz);
float t01 = d.topAt(gcx, gcz + 1);
float t11 = d.topAt(gcx + 1, gcz + 1);
float b00 = d.bottomAt(gcx, gcz);
float b10 = d.bottomAt(gcx + 1, gcz);
float b01 = d.bottomAt(gcx, gcz + 1);
float b11 = d.bottomAt(gcx + 1, gcz + 1);
// Horizontal UV: integer cell indices → 1 tile per cell with Repeat
float ux0 = gcx, ux1 = gcx + 1f;
float uz0 = gcz, uz1 = gcz + 1f;
// ── Top face (+Y) ──────────────────────────────────────────────
faceA(pos, nor, uv, idx,
x0, t00, z0, x1, t10, z0, x0, t01, z1, x1, t11, z1,
0, 1, 0,
ux0, uz0, ux1, uz0, ux0, uz1, ux1, uz1);
// ── Bottom face (Y) ───────────────────────────────────────────
faceB(pos, nor, uv, idx,
x0, b00, z0, x1, b10, z0, x0, b01, z1, x1, b11, z1,
0, -1, 0,
ux0, uz0, ux1, uz0, ux0, uz1, ux1, uz1);
// ── North wall (Z): edge at z0 ───────────────────────────────
if (d.isHole(gcx, gcz - 1)) {
faceB(pos, nor, uv, idx,
x0, t00, z0, x1, t10, z0, x0, b00, z0, x1, b10, z0,
0, 0, -1,
ux0, t00/CS, ux1, t10/CS, ux0, b00/CS, ux1, b10/CS);
}
// ── South wall (+Z): edge at z1 ───────────────────────────────
if (d.isHole(gcx, gcz + 1)) {
faceA(pos, nor, uv, idx,
x0, t01, z1, x1, t11, z1, x0, b01, z1, x1, b11, z1,
0, 0, 1,
ux0, t01/CS, ux1, t11/CS, ux0, b01/CS, ux1, b11/CS);
}
// ── West wall (X): edge at x0 ────────────────────────────────
if (d.isHole(gcx - 1, gcz)) {
faceA(pos, nor, uv, idx,
x0, t00, z0, x0, t01, z1, x0, b00, z0, x0, b01, z1,
-1, 0, 0,
uz0, t00/CS, uz1, t01/CS, uz0, b00/CS, uz1, b01/CS);
}
// ── East wall (+X): edge at x1 ────────────────────────────────
if (d.isHole(gcx + 1, gcz)) {
faceB(pos, nor, uv, idx,
x1, t10, z0, x1, t11, z1, x1, b10, z0, x1, b11, z1,
1, 0, 0,
uz0, t10/CS, uz1, t11/CS, uz0, b10/CS, uz1, b11/CS);
}
}
}
if (idx.isEmpty()) return null;
return toMesh(pos, nor, uv, idx);
}
// ── Winding helpers ──────────────────────────────────────────────────────
private static void faceA(ArrayList<Float> pos, ArrayList<Float> nor, ArrayList<Float> uv,
ArrayList<Integer> idx,
float x0, float y0, float z0,
float x1, float y1, float z1,
float x2, float y2, float z2,
float x3, float y3, float z3,
float nx, float ny, float nz,
float tu0, float tv0, float tu1, float tv1,
float tu2, float tv2, float tu3, float tv3) {
int b = pos.size() / 3;
addV(pos, nor, uv, x0, y0, z0, nx, ny, nz, tu0, tv0);
addV(pos, nor, uv, x1, y1, z1, nx, ny, nz, tu1, tv1);
addV(pos, nor, uv, x2, y2, z2, nx, ny, nz, tu2, tv2);
addV(pos, nor, uv, x3, y3, z3, nx, ny, nz, tu3, tv3);
idx.add(b); idx.add(b + 2); idx.add(b + 3);
idx.add(b); idx.add(b + 3); idx.add(b + 1);
}
private static void faceB(ArrayList<Float> pos, ArrayList<Float> nor, ArrayList<Float> uv,
ArrayList<Integer> idx,
float x0, float y0, float z0,
float x1, float y1, float z1,
float x2, float y2, float z2,
float x3, float y3, float z3,
float nx, float ny, float nz,
float tu0, float tv0, float tu1, float tv1,
float tu2, float tv2, float tu3, float tv3) {
int b = pos.size() / 3;
addV(pos, nor, uv, x0, y0, z0, nx, ny, nz, tu0, tv0);
addV(pos, nor, uv, x1, y1, z1, nx, ny, nz, tu1, tv1);
addV(pos, nor, uv, x2, y2, z2, nx, ny, nz, tu2, tv2);
addV(pos, nor, uv, x3, y3, z3, nx, ny, nz, tu3, tv3);
idx.add(b); idx.add(b + 1); idx.add(b + 3);
idx.add(b); idx.add(b + 3); idx.add(b + 2);
}
private static void addV(ArrayList<Float> pos, ArrayList<Float> nor, ArrayList<Float> uv,
float x, float y, float z,
float nx, float ny, float nz,
float u, float v) {
pos.add(x); pos.add(y); pos.add(z);
nor.add(nx); nor.add(ny); nor.add(nz);
uv.add(u); uv.add(v);
}
// ── List → JME3 Mesh ─────────────────────────────────────────────────────
private static Mesh toMesh(ArrayList<Float> pos,
ArrayList<Float> nor,
ArrayList<Float> uv,
ArrayList<Integer> idx) {
FloatBuffer pb = BufferUtils.createFloatBuffer(pos.size());
for (float v : pos) pb.put(v);
FloatBuffer nb = BufferUtils.createFloatBuffer(nor.size());
for (float v : nor) nb.put(v);
FloatBuffer ub = BufferUtils.createFloatBuffer(uv.size());
for (float v : uv) ub.put(v);
IntBuffer ib = BufferUtils.createIntBuffer(idx.size());
for (int v : idx) ib.put(v);
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pb);
mesh.setBuffer(VertexBuffer.Type.Normal, 3, nb);
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, ub);
mesh.setBuffer(VertexBuffer.Type.Index, 3, ib);
mesh.updateBound();
return mesh;
}
}

View File

@@ -0,0 +1,323 @@
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.math.Vector2f;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.texture.Texture;
import de.blight.common.MapData;
import de.blight.editor.SharedInput;
import de.blight.editor.tool.HoleTool;
import de.blight.editor.tool.UpperHeightTool;
import java.util.HashSet;
import java.util.List;
/**
* AppState that owns the upper (mountain) layer: 1024 chunk geometries
* arranged in a 32×32 grid, rebuilt lazily when marked dirty.
*/
public class UpperLayerState extends BaseAppState {
// ── Constants ────────────────────────────────────────────────────────────
private static final int CHUNKS_PER_AXIS = 32; // 512 cells / 16
private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS; // 1024
private static final int MAX_REBUILDS_PER_FRAME = 4;
// ── State ────────────────────────────────────────────────────────────────
private final SharedInput input;
private final MapData initialMapData; // null = Standardwerte
final UpperLayerData data = new UpperLayerData();
private Node upperNode;
private Material chunkMat;
private Geometry[] chunkGeos = new Geometry[CHUNK_COUNT];
private boolean[] dirtyChunks = new boolean[CHUNK_COUNT];
// ── Konstruktoren ─────────────────────────────────────────────────────────
public UpperLayerState(SharedInput input) {
this(input, null);
}
public UpperLayerState(SharedInput input, MapData initialMapData) {
this.input = input;
this.initialMapData = initialMapData;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
AssetManager assets = app.getAssetManager();
Node rootNode = ((SimpleApplication) app).getRootNode();
chunkMat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
try {
Texture rock = assets.loadTexture("Textures/Terrain/Rock2/rock.jpg");
rock.setWrap(Texture.WrapMode.Repeat);
chunkMat.setTexture("DiffuseMap", rock);
chunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.32f, 0.25f, 1f));
System.out.println("[UpperLayer] Vulkangestein-Textur geladen");
} catch (Exception e) {
System.out.println("[UpperLayer] Rock-Textur fehlt, Fallback: " + e.getMessage());
chunkMat.setBoolean("UseMaterialColors", true);
chunkMat.setColor("Diffuse", new ColorRGBA(0.18f, 0.12f, 0.08f, 1f));
}
chunkMat.setColor("Ambient", new ColorRGBA(0.10f, 0.07f, 0.05f, 1f));
chunkMat.setColor("Specular", new ColorRGBA(0.06f, 0.05f, 0.04f, 1f));
chunkMat.setFloat("Shininess", 6f);
// Polygon-Offset verhindert Z-Fighting mit dem Gras-Terrain an gleicher Y-Position
chunkMat.getAdditionalRenderState().setPolyOffset(2f, 2f);
// Gespeicherte Karte übernehmen (falls vorhanden)
if (initialMapData != null) {
System.arraycopy(initialMapData.upperTop, 0, data.topHeight, 0,
Math.min(initialMapData.upperTop.length, data.topHeight.length));
System.arraycopy(initialMapData.upperBottom, 0, data.bottomHeight, 0,
Math.min(initialMapData.upperBottom.length, data.bottomHeight.length));
for (int i = 0; i < Math.min(initialMapData.upperHole.length, data.hole.length); i++) {
data.hole[i] = initialMapData.upperHole[i] != 0;
}
}
upperNode = new Node("upperLayer");
rootNode.attachChild(upperNode);
// Alle Chunks beim Start aufbauen
for (int i = 0; i < CHUNK_COUNT; i++) {
rebuildChunk(i);
}
}
@Override
protected void onEnable() { upperNode.setCullHint(com.jme3.scene.Spatial.CullHint.Inherit); }
@Override
protected void onDisable() { upperNode.setCullHint(com.jme3.scene.Spatial.CullHint.Always); }
@Override
protected void cleanup(Application app) {
((SimpleApplication) app).getRootNode().detachChild(upperNode);
}
// ── Update loop ───────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
// Show/hide based on SharedInput flag
boolean shouldShow = input.upperLayerVisible;
com.jme3.scene.Spatial.CullHint hint = shouldShow
? com.jme3.scene.Spatial.CullHint.Inherit
: com.jme3.scene.Spatial.CullHint.Always;
if (upperNode.getCullHint() != hint) {
upperNode.setCullHint(hint);
}
int rebuilt = 0;
for (int i = 0; i < CHUNK_COUNT && rebuilt < MAX_REBUILDS_PER_FRAME; i++) {
if (dirtyChunks[i]) {
rebuildChunk(i);
dirtyChunks[i] = false;
rebuilt++;
}
}
}
// ── Public API ────────────────────────────────────────────────────────────
/** The Node containing all chunk geometries — use for ray-casting. */
public Node getUpperNode() { return upperNode; }
public void setTopHeight(int vx, int vz, float h) {
data.topHeight[vx + vz * UpperLayerData.VERTS] = h;
markVertexDirty(vx, vz);
}
public void setHole(int cx, int cz, boolean v) {
if (cx < 0 || cx >= UpperLayerData.CELLS || cz < 0 || cz >= UpperLayerData.CELLS) return;
data.hole[cx + cz * UpperLayerData.CELLS] = v;
// Cell itself + all 4 neighbours (their wall geometry may change)
markCellDirty(cx, cz);
markCellDirty(cx - 1, cz);
markCellDirty(cx + 1, cz);
markCellDirty(cx, cz - 1);
markCellDirty(cx, cz + 1);
}
// ── Height editing ────────────────────────────────────────────────────────
public void applyHeightEdit(float worldX, float worldZ, int action) {
int mode = input.upperHeightTool.mode.getSelectedIndex();
float radius = (float) input.upperHeightTool.brushRadius.getValue();
float str = (float) input.upperHeightTool.brushStrength.getValue();
if (mode == UpperHeightTool.MODE_SMOOTH) {
smoothHeight(worldX, worldZ, radius, str);
return;
}
int cx = UpperLayerData.worldToVertexX(worldX);
int cz = UpperLayerData.worldToVertexZ(worldZ);
int r = (int) Math.ceil(radius / 8f); // radius in vertex units
float sign = (mode == UpperHeightTool.MODE_LOWER) ? -1f : 1f;
float delta = sign * str * action;
// Flatten target: height at brush centre
float flatTarget = data.topAt(cx, cz);
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= UpperLayerData.VERTS) continue;
if (vz < 0 || vz >= UpperLayerData.VERTS) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz);
if (dist >= r + 1) continue;
float t = dist / (r + 1);
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
float cur = data.topAt(vx, vz);
float next;
if (mode == UpperHeightTool.MODE_FLATTEN) {
next = cur + (flatTarget - cur) * falloff * str * 3f;
} else {
next = cur + delta * falloff;
}
setTopHeight(vx, vz, next);
}
}
}
private void smoothHeight(float worldX, float worldZ, float radius, float str) {
int cx = UpperLayerData.worldToVertexX(worldX);
int cz = UpperLayerData.worldToVertexZ(worldZ);
int r = (int) Math.ceil(radius / 8f);
// Average height in brush
float sum = 0f; int count = 0;
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= UpperLayerData.VERTS) continue;
if (vz < 0 || vz >= UpperLayerData.VERTS) continue;
if (FastMath.sqrt(dx * dx + dz * dz) >= r + 1) continue;
sum += data.topAt(vx, vz);
count++;
}
}
if (count == 0) return;
float avg = sum / count;
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= UpperLayerData.VERTS) continue;
if (vz < 0 || vz >= UpperLayerData.VERTS) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz);
if (dist >= r + 1) continue;
float falloff = 1f - dist / (r + 1);
float cur = data.topAt(vx, vz);
setTopHeight(vx, vz, cur + (avg - cur) * falloff * str * 3f);
}
}
}
// ── Hole editing ──────────────────────────────────────────────────────────
public void applyHoleEdit(float worldX, float worldZ) {
boolean dig = input.holeTool.mode.getSelectedIndex() == HoleTool.MODE_DIG;
float radius = (float) input.holeTool.brushRadius.getValue();
int cx = UpperLayerData.worldToCellX(worldX);
int cz = UpperLayerData.worldToCellZ(worldZ);
int r = (int) Math.ceil(radius / 8f);
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
if (FastMath.sqrt(dx * dx + dz * dz) > r) continue;
setHole(cx + dx, cz + dz, dig);
}
}
}
// ── Terrain-Sync ──────────────────────────────────────────────────────────
/**
* Verschiebt top- und bottomHeight um dieselben Deltas wie das Basis-Terrain.
* Jeder obere Vertex wird dabei nur einmal angepasst, auch wenn mehrere
* Terrain-Vertices auf denselben oberen Vertex fallen.
*/
public void adjustHeightsWithTerrain(List<Vector2f> worldXZ, List<Float> deltas) {
HashSet<Integer> seen = new HashSet<>();
for (int i = 0; i < worldXZ.size(); i++) {
Vector2f p = worldXZ.get(i);
int uvx = UpperLayerData.worldToVertexX(p.x);
int uvz = UpperLayerData.worldToVertexZ(p.y);
int key = uvx + uvz * UpperLayerData.VERTS;
if (!seen.add(key)) continue;
float d = deltas.get(i);
data.topHeight[key] += d;
data.bottomHeight[key] += d;
markVertexDirty(uvx, uvz);
}
}
// ── Dirty tracking ────────────────────────────────────────────────────────
/** Mark all chunks that contain vertex (vx, vz). A vertex is shared by up to 4 cells/chunks. */
private void markVertexDirty(int vx, int vz) {
// The vertex sits on the corner of cells (vx-1,vz-1)..(vx,vz)
for (int dcz = -1; dcz <= 0; dcz++) {
for (int dcx = -1; dcx <= 0; dcx++) {
int cx = vx + dcx;
int cz = vz + dcz;
if (cx < 0 || cx >= UpperLayerData.CELLS) continue;
if (cz < 0 || cz >= UpperLayerData.CELLS) continue;
markCellDirty(cx, cz);
}
}
}
private void markCellDirty(int cx, int cz) {
if (cx < 0 || cx >= UpperLayerData.CELLS) return;
if (cz < 0 || cz >= UpperLayerData.CELLS) return;
int chunkX = cx / 16;
int chunkZ = cz / 16;
dirtyChunks[chunkX + chunkZ * CHUNKS_PER_AXIS] = true;
}
// ── Chunk rebuild ─────────────────────────────────────────────────────────
private void rebuildChunk(int idx) {
int chunkX = idx % CHUNKS_PER_AXIS;
int chunkZ = idx / CHUNKS_PER_AXIS;
Mesh mesh = UpperLayerMesher.buildChunk(data, chunkX, chunkZ);
if (mesh == null) {
// Chunk is fully empty — remove geometry if present
if (chunkGeos[idx] != null) {
upperNode.detachChild(chunkGeos[idx]);
chunkGeos[idx] = null;
}
} else if (chunkGeos[idx] == null) {
Geometry geo = new Geometry("chunk_" + chunkX + "_" + chunkZ, mesh);
geo.setMaterial(chunkMat);
upperNode.attachChild(geo);
chunkGeos[idx] = geo;
} else {
chunkGeos[idx].setMesh(mesh);
}
}
}

View File

@@ -0,0 +1,33 @@
package de.blight.editor.tool;
/**
* Ein benannter Auswahlparameter eines EditorTools (Enum-artig).
* Thread-sicher: JavaFX-Thread schreibt, JME3-Thread liest.
*/
public class ChoiceToolParameter {
private final String name;
private final String[] choices;
private final String[] imagePaths; // optional, null = ChoiceBox verwenden
private volatile int selectedIndex;
public ChoiceToolParameter(String name, String[] choices, int defaultIndex) {
this(name, choices, defaultIndex, null);
}
public ChoiceToolParameter(String name, String[] choices, int defaultIndex, String[] imagePaths) {
this.name = name;
this.choices = choices;
this.imagePaths = imagePaths;
this.selectedIndex = defaultIndex;
}
public String getName() { return name; }
public String[] getChoices() { return choices; }
public String[] getImagePaths() { return imagePaths; }
public int getSelectedIndex() { return selectedIndex; }
public void setSelectedIndex(int i) {
if (i >= 0 && i < choices.length) selectedIndex = i;
}
}

View File

@@ -0,0 +1,21 @@
package de.blight.editor.tool;
import java.util.List;
/**
* Basisklasse für alle Editor-Tools.
* Jedes Tool hat einen Namen, Auswahl-Parameter (ChoiceToolParameter)
* und numerische Schieberegler-Parameter (ToolParameter).
*/
public abstract class EditorTool {
public abstract String getName();
/** Numerische Slider-Parameter. */
public abstract List<ToolParameter> getParameters();
/** Diskrete Auswahl-Parameter (werden als ChoiceBox gerendert). */
public List<ChoiceToolParameter> getChoiceParameters() {
return List.of();
}
}

View File

@@ -0,0 +1,21 @@
package de.blight.editor.tool;
import java.util.List;
/**
* Graswerkzeug: Linksklick erhöht Dichte, Rechtsklick verringert sie.
*/
public class GrassTool extends EditorTool {
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 40.0, 1.0, 500.0);
public final ToolParameter grassHeight = new ToolParameter("Grashöhe", 1.5, 0.1, 10.0);
public final ToolParameter density = new ToolParameter("Dichte", 8.0, 1.0, 50.0);
@Override public String getName() { return "Gras"; }
@Override
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
@Override
public List<ToolParameter> getParameters() { return List.of(brushRadius, grassHeight, density); }
}

View File

@@ -0,0 +1,43 @@
package de.blight.editor.tool;
import java.util.List;
/**
* Tool zum Anheben und Absenken des Terrains.
* Modi: 0=Sinus, 1=Spike, 2=Plateau, 3=Smooth
*/
public class HeightTool extends EditorTool {
public static final int MODE_SINUS = 0;
public static final int MODE_SPIKE = 1;
public static final int MODE_PLATEAU = 2;
public static final int MODE_SMOOTH = 3;
public final ChoiceToolParameter mode = new ChoiceToolParameter(
"Modus",
new String[]{"Sinus", "Spike", "Plateau", "Smooth"},
MODE_SPIKE,
new String[]{
"img/editor/terraintool_sinus.png",
"img/editor/terraintool_spike.png",
"img/editor/terraintool_plateau.png",
"img/editor/terraintool_smooth.png"
}
);
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
@Override
public String getName() { return "Höhe"; }
@Override
public List<ChoiceToolParameter> getChoiceParameters() {
return List.of(mode);
}
@Override
public List<ToolParameter> getParameters() {
return List.of(brushRadius, brushStrength);
}
}

View File

@@ -0,0 +1,34 @@
package de.blight.editor.tool;
import java.util.List;
/**
* Tool for digging or filling holes (cave entrances) in the upper layer.
* Modes: 0=Dig, 1=Fill
*/
public class HoleTool extends EditorTool {
public static final int MODE_DIG = 0;
public static final int MODE_FILL = 1;
public final ChoiceToolParameter mode = new ChoiceToolParameter(
"Modus",
new String[]{"Graben", "Füllen"},
MODE_DIG
);
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 20.0, 1.0, 100.0);
@Override
public String getName() { return "Höhlen / Löcher"; }
@Override
public List<ChoiceToolParameter> getChoiceParameters() {
return List.of(mode);
}
@Override
public List<ToolParameter> getParameters() {
return List.of(brushRadius);
}
}

View File

@@ -0,0 +1,30 @@
package de.blight.editor.tool;
import java.util.List;
/**
* Tool zum Bemalen des Basis-Terrains mit Texturen (Splatmap).
* Textur-Slots: 0=Gras, 1=Fels, 2=Erde, 3=Sand
* Rechtsklick: setzt auf Gras zurück.
*/
public class TextureTool extends EditorTool {
// Terrain.j3md (unlit) hat nur Tex1Tex3; Slot 0=Gras(Base), 1=Fels(R), 2=Erde(G)
public static final String[] TEXTURE_NAMES = {"Gras", "Fels", "Erde"};
public final ChoiceToolParameter textureIndex = new ChoiceToolParameter(
"Textur", TEXTURE_NAMES, 0
);
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 0.05, 0.005, 0.5);
@Override
public String getName() { return "Textur"; }
@Override
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(textureIndex); }
@Override
public List<ToolParameter> getParameters() { return List.of(brushRadius, brushStrength); }
}

View File

@@ -0,0 +1,29 @@
package de.blight.editor.tool;
/**
* Ein benannter, numerischer Parameter eines EditorTools.
* Thread-sicher: JavaFX-Thread schreibt, JME3-Thread liest.
*/
public class ToolParameter {
private final String name;
private final double min;
private final double max;
private volatile double value;
public ToolParameter(String name, double defaultValue, double min, double max) {
this.name = name;
this.min = min;
this.max = max;
this.value = defaultValue;
}
public String getName() { return name; }
public double getMin() { return min; }
public double getMax() { return max; }
public double getValue() { return value; }
public void setValue(double v) {
this.value = Math.max(min, Math.min(max, v));
}
}

View File

@@ -0,0 +1,37 @@
package de.blight.editor.tool;
import java.util.List;
/**
* Tool for sculpting the top surface of the upper (mountain) layer.
* Modes: 0=Raise, 1=Lower, 2=Smooth, 3=Flatten
*/
public class UpperHeightTool extends EditorTool {
public static final int MODE_RAISE = 0;
public static final int MODE_LOWER = 1;
public static final int MODE_SMOOTH = 2;
public static final int MODE_FLATTEN = 3;
public final ChoiceToolParameter mode = new ChoiceToolParameter(
"Modus",
new String[]{"Anheben", "Absenken", "Smooth", "Abflachen"},
MODE_RAISE
);
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 200.0);
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
@Override
public String getName() { return "Obere Schicht Höhe"; }
@Override
public List<ChoiceToolParameter> getChoiceParameters() {
return List.of(mode);
}
@Override
public List<ToolParameter> getParameters() {
return List.of(brushRadius, brushStrength);
}
}

View File

@@ -0,0 +1,226 @@
package de.blight.editor.tree;
import com.jme3.math.FastMath;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Builds a palm tree Node with two child geometries:
* "bark" — tapered trunk cylinder only (palms have no branches)
* "leaves" — horizontal bilateral leaflet quads (Wedel/fronds) starting at trunk tip
*
* Wind weight gradient: 0 near trunk → 1 at leaflet tips, so tips sway most.
*/
public class PalmMeshBuilder {
public static Node build(PalmOptions opts) {
Random rng = new Random(opts.seed);
float[] azimuths = computeAzimuths(opts, rng);
float[] elevations = computeElevations(opts, rng);
Node palm = new Node("palm");
palm.attachChild(new Geometry("bark", buildBarkMesh(opts)));
palm.attachChild(new Geometry("leaves", buildLeafMesh(opts, azimuths, elevations)));
return palm;
}
// ── Bark mesh: trunk only ─────────────────────────────────────────────────
private static Mesh buildBarkMesh(PalmOptions opts) {
Accum acc = new Accum();
addTrunk(acc, opts);
return acc.toMesh();
}
private static void addTrunk(Accum acc, PalmOptions opts) {
int M = opts.trunkSections;
int N = opts.trunkSegments;
float H = opts.trunkHeight;
float r0 = opts.trunkRadiusBottom;
float r1 = opts.trunkRadiusTop;
int base = acc.vertexCount;
for (int i = 0; i <= M; i++) {
float t = (float) i / M;
float r = r0 + (r1 - r0) * t;
float y = H * t;
float wind = t * 0.4f; // trunk barely sways at root, moderate at crown
for (int j = 0; j <= N; j++) {
float angle = FastMath.TWO_PI * j / N;
float nx = FastMath.cos(angle);
float nz = FastMath.sin(angle);
acc.add(nx * r, y, nz * r, nx, 0f, nz, (float) j / N, t, wind);
}
}
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
int r0v = base + i * (N + 1) + j;
int r1v = base + (i + 1) * (N + 1) + j;
acc.tri(r0v, r1v, r1v + 1);
acc.tri(r0v, r1v + 1, r0v + 1);
}
}
}
// ── Leaf mesh: horizontal bilateral leaflets (Wedel) ─────────────────────
private static Mesh buildLeafMesh(PalmOptions opts, float[] azimuths, float[] elevations) {
Accum acc = new Accum();
for (int f = 0; f < opts.frondCount; f++) {
addFrondLeaflets(acc, opts, azimuths[f], elevations[f]);
}
return acc.toMesh();
}
private static void addFrondLeaflets(Accum acc, PalmOptions opts, float azimuth, float elevation) {
float sinE = FastMath.sin(elevation);
float cosE = FastMath.cos(elevation);
float cosA = FastMath.cos(azimuth);
float sinA = FastMath.sin(azimuth);
float dx = sinE * cosA; // frond direction vector
float dy = cosE;
float dz = sinE * sinA;
// Horizontal projection for leaflet orientation (keeps leaflets truly flat)
float hLen = FastMath.sqrt(dx * dx + dz * dz);
if (hLen < 1e-5f) hLen = 1e-5f;
float hx = dx / hLen;
float hz = dz / hLen;
// Side direction perpendicular to frond in horizontal plane:
// (hx,0,hz) × (0,1,0) = (-hz, 0, hx)
float sx = -hz;
float sz = hx;
int K = opts.frondLeafletPairs;
float step = opts.frondLength / K;
float halfW = step * 0.30f; // leaflet thickness along frond axis
float baseY = opts.trunkHeight;
// Tip width is a fixed fraction of base width — gives natural taper
float sizeBase = opts.frondWidth;
float sizeTip = opts.frondWidth * 0.18f;
for (int k = 0; k < K; k++) {
float tPos = (float) k / Math.max(1, K - 1); // 0→1 along frond, starts at trunk
float leafSize = sizeBase + (sizeTip - sizeBase) * tPos;
float ax = dx * opts.frondLength * tPos;
float ay = baseY + dy * opts.frondLength * tPos;
float az = dz * opts.frondLength * tPos;
// Wind: 0 near trunk tip, more at frond tip; extra delta for outer leaflet edge
float windBase = tPos * 0.65f; // inner edge of leaflet
float windTip = tPos * 0.65f + 0.35f; // outer edge of leaflet (leaf tip)
addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, +1f, windBase, windTip);
addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, -1f, windBase, windTip);
}
}
private static void addLeafletQuad(Accum acc,
float ax, float ay, float az,
float hx, float hz,
float sx, float sz,
float leafSize, float halfW, float side,
float windInner, float windOuter) {
// All 4 vertices at Y = ay (truly horizontal, normal = +Y)
float p0x = ax - hx * halfW, p0z = az - hz * halfW;
float p1x = ax + hx * halfW, p1z = az + hz * halfW;
float p2x = p1x + sx * side * leafSize, p2z = p1z + sz * side * leafSize;
float p3x = p0x + sx * side * leafSize, p3z = p0z + sz * side * leafSize;
int base = acc.vertexCount;
// p0, p1 = inner edge (attached to frond), p2, p3 = outer tip
acc.add(p0x, ay, p0z, 0f, 1f, 0f, 0f, 0f, windInner);
acc.add(p1x, ay, p1z, 0f, 1f, 0f, 1f, 0f, windInner);
acc.add(p2x, ay, p2z, 0f, 1f, 0f, 1f, 1f, windOuter);
acc.add(p3x, ay, p3z, 0f, 1f, 0f, 0f, 1f, windOuter);
acc.tri(base, base + 1, base + 2);
acc.tri(base, base + 2, base + 3);
}
// ── Random frond placement ────────────────────────────────────────────────
private static float[] computeAzimuths(PalmOptions opts, Random rng) {
float[] a = new float[opts.frondCount];
for (int f = 0; f < opts.frondCount; f++) {
a[f] = FastMath.TWO_PI * f / opts.frondCount + (rng.nextFloat() - 0.5f) * 0.4f;
}
return a;
}
private static float[] computeElevations(PalmOptions opts, Random rng) {
float minR = opts.frondAngleMin * FastMath.DEG_TO_RAD;
float maxR = opts.frondAngleMax * FastMath.DEG_TO_RAD;
float[] a = new float[opts.frondCount];
for (int f = 0; f < opts.frondCount; f++) {
a[f] = minR + rng.nextFloat() * (maxR - minR);
}
return a;
}
// ── Vertex accumulator ────────────────────────────────────────────────────
private static final class Accum {
final List<Float> pos = new ArrayList<>();
final List<Float> norm = new ArrayList<>();
final List<Float> uv = new ArrayList<>();
final List<Float> col = new ArrayList<>();
final List<Integer> idx = new ArrayList<>();
int vertexCount = 0;
void add(float x, float y, float z,
float nx, float ny, float nz,
float u, float v, float wind) {
pos.add(x); pos.add(y); pos.add(z);
norm.add(nx); norm.add(ny); norm.add(nz);
uv.add(u); uv.add(v);
col.add(wind); col.add(0f); col.add(0f); col.add(1f);
vertexCount++;
}
void tri(int a, int b, int c) { idx.add(a); idx.add(b); idx.add(c); }
Mesh toMesh() {
if (vertexCount == 0) return new Mesh();
int n = vertexCount;
FloatBuffer posB = BufferUtils.createFloatBuffer(n * 3);
FloatBuffer normB = BufferUtils.createFloatBuffer(n * 3);
FloatBuffer uvB = BufferUtils.createFloatBuffer(n * 2);
FloatBuffer colB = BufferUtils.createFloatBuffer(n * 4);
IntBuffer idxB = BufferUtils.createIntBuffer(idx.size());
for (Float f : pos) posB.put(f);
for (Float f : norm) normB.put(f);
for (Float f : uv) uvB.put(f);
for (Float f : col) colB.put(f);
for (Integer i : idx) idxB.put(i);
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, posB);
mesh.setBuffer(VertexBuffer.Type.Normal, 3, normB);
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uvB);
mesh.setBuffer(VertexBuffer.Type.Color, 4, colB);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idxB);
mesh.updateBound();
return mesh;
}
}
}

View File

@@ -0,0 +1,50 @@
package de.blight.editor.tree;
public class PalmOptions {
public int seed = 42;
// Trunk
public float trunkHeight = 12f;
public float trunkRadiusBottom = 0.35f;
public float trunkRadiusTop = 0.30f; // tapers but stays thick
public int trunkSections = 10;
public int trunkSegments = 8;
// Fronds
public int frondCount = 10;
public float frondAngleMin = 70f; // degrees from vertical (Y-up)
public float frondAngleMax = 110f;
public float frondLength = 6.5f;
public int frondLeafletPairs = 8;
public float frondWidth = 1.4f; // max leaflet width at frond base
// Colors
public float barkR = 0.68f, barkG = 0.54f, barkB = 0.34f;
public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f;
// Textures
public String barkTexture = "Textures/bark/Bark008_Color.jpg";
public String leafTexture = "Textures/leaves/palm.png";
public PalmOptions copy() {
PalmOptions c = new PalmOptions();
c.seed = seed;
c.trunkHeight = trunkHeight;
c.trunkRadiusBottom = trunkRadiusBottom;
c.trunkRadiusTop = trunkRadiusTop;
c.trunkSections = trunkSections;
c.trunkSegments = trunkSegments;
c.frondCount = frondCount;
c.frondAngleMin = frondAngleMin;
c.frondAngleMax = frondAngleMax;
c.frondLength = frondLength;
c.frondLeafletPairs = frondLeafletPairs;
c.frondWidth = frondWidth;
c.barkR = barkR; c.barkG = barkG; c.barkB = barkB;
c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
c.barkTexture = barkTexture;
c.leafTexture = leafTexture;
return c;
}
}

View File

@@ -0,0 +1,359 @@
package de.blight.editor.tree;
import com.jme3.bounding.BoundingBox;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.*;
/**
* Prozeduraler Baum-Generator nach dem ez-tree-Ansatz:
* • Jeder Ast wird in mehrere Sektionen unterteilt (organische Kurven)
* • Gnarliness-Perturbation: dünne Äste wackeln stärker
* • Gravitation: Äste hängen proportional zur Kraft nach unten (negativ = aufwärts)
* • Stratifizierte Kind-Platzierung: verhindert Clustering
* • MWC-RNG für reproduzierbare Ergebnisse mit beliebigem Seed
*
* quality 1.0 = HD (volle Sektionen/Segmente), 0.0 = LD (halbe Sektionen, -2 Segmente)
*/
public class TreeMeshBuilder {
public record MeshResult(Mesh bark, Mesh leaves, BoundingBox bounds) {}
// ── Haupt-Einstieg ────────────────────────────────────────────────────────
public MeshResult build(TreeParams p, float quality) {
boolean hd = quality >= 0.5f;
int maxLevel = hd ? p.levels : Math.max(1, p.levels - 1);
Rng rng = new Rng(p.seed);
VertexCollector barkCol = new VertexCollector();
VertexCollector leafCol = new VertexCollector();
record BranchTask(Vector3f origin, Vector3f dir, int level, float windBase) {}
record SectionPt (Vector3f pos, Vector3f dir, float wind) {}
Deque<BranchTask> stack = new ArrayDeque<>();
stack.push(new BranchTask(new Vector3f(0, 0, 0), new Vector3f(0, 1, 0),
0, p.trunkFlexibility));
while (!stack.isEmpty()) {
BranchTask t = stack.pop();
int lv = t.level();
if (lv >= maxLevel) continue;
// Per-Level-Parameter
int numSec = Math.max(1, hd ? TreeParams.lv(p.sections, lv)
: TreeParams.lv(p.sections, lv) / 2);
float branchLen = TreeParams.lv(p.length, lv);
float baseRad = TreeParams.lv(p.radius, lv);
float tapFactor = TreeParams.lv(p.taper, lv);
float gnarl = TreeParams.lv(p.gnarliness,lv);
int segs = Math.max(3, TreeParams.lv(p.segments, lv) - (hd ? 0 : 2));
float segLen = branchLen / numSec;
// Wind-Bereich für diesen Ast
float windEnd = p.trunkFlexibility
+ (p.branchFlexibility - p.trunkFlexibility)
* (float)(lv + 1) / p.levels;
windEnd = FastMath.clamp(Math.max(t.windBase(), windEnd), 0f, 1f);
Vector3f pos = t.origin().clone();
Vector3f dir = t.dir().clone();
List<SectionPt> sectionPts = new ArrayList<>(numSec);
for (int s = 0; s < numSec; s++) {
float rBot = baseRad * lerp(1f, tapFactor, (float) s / numSec);
float rTop = baseRad * lerp(1f, tapFactor, (float)(s + 1) / numSec);
float wBot = lerp(t.windBase(), windEnd, (float) s / numSec);
float wTop = lerp(t.windBase(), windEnd, (float)(s + 1) / numSec);
// Gnarliness: dünne Äste stärker perturbieren (nur ab Level 1)
if (lv > 0 && gnarl > 1e-4f) {
float g = gnarl / (float) Math.sqrt(Math.max(0.01f, rBot));
dir.x += rng.range(-g, g);
dir.z += rng.range(-g, g);
dir.normalizeLocal();
}
// Gravitation: Richtung graduell zur Gravitations-Richtung drehen
applyGravity(dir, rBot, p.gravityStrength);
Vector3f end = pos.add(dir.mult(segLen));
addCylinder(barkCol, pos, end, rBot, rTop, wBot, wTop, segs);
sectionPts.add(new SectionPt(pos.clone(), dir.clone(), wTop));
pos = end;
}
if (lv < maxLevel - 1) {
// Kind-Äste stratifiziert entlang des Eltern-Astes verteilen
int nChildren = TreeParams.lv(p.children, lv);
float childStart = TreeParams.lv(p.start, lv + 1); // Startfraktion
for (int k = 0; k < nChildren; k++) {
float frac = childStart + (1f - childStart)
* (k + rng.range(-0.35f, 0.35f)) / nChildren;
frac = FastMath.clamp(frac, childStart, 1f);
int si = Math.min((int)(frac * sectionPts.size()), sectionPts.size() - 1);
SectionPt sp = sectionPts.get(si);
float yRot = k * FastMath.TWO_PI / nChildren + rng.range(-0.4f, 0.4f);
float nextAngle = TreeParams.lv(p.angle, lv + 1);
stack.push(new BranchTask(
sp.pos(), branchDir(sp.dir(), yRot, nextAngle),
lv + 1, sp.wind()));
}
} else if (p.generateLeaves) {
// Blätter entlang des gesamten letzten Astes (jede Sektion)
int sectionLeafCount = Math.max(1, p.leafCount * 2 / 3);
for (SectionPt sp : sectionPts) {
addLeafCluster(leafCol, sp.pos(), sp.wind(),
p.leafScale * 0.75f, sectionLeafCount, p.leafAngle, rng);
}
// Blatt-Cluster an der Spitze (Originalgröße)
addLeafCluster(leafCol, pos, windEnd, p.leafScale, p.leafCount, p.leafAngle, rng);
// Seiten-Zweige am letzten Ast (leafBranchings, 03)
if (p.leafBranchings > 0) {
float twigLen = branchLen * 0.4f;
float twigRad = baseRad * 0.45f;
float twigAngle = TreeParams.lv(p.angle, lv) * 0.75f;
for (SectionPt sp : sectionPts) {
for (int b = 0; b < p.leafBranchings; b++) {
float yRot = b * FastMath.TWO_PI / p.leafBranchings
+ rng.range(-0.5f, 0.5f);
Vector3f twigDir = branchDir(sp.dir(), yRot, twigAngle);
Vector3f twigEnd = sp.pos().add(twigDir.mult(twigLen));
addCylinder(barkCol, sp.pos(), twigEnd,
twigRad, twigRad * 0.35f,
sp.wind(), windEnd, Math.max(3, segs - 1));
addLeafCluster(leafCol, twigEnd, windEnd,
p.leafScale, p.leafCount, p.leafAngle, rng);
}
}
}
}
}
return new MeshResult(barkCol.toMesh(), leafCol.toMesh(), computeBounds(barkCol));
}
// ── Gravitations-Kraft ────────────────────────────────────────────────────
private static void applyGravity(Vector3f dir, float radius, float strength) {
if (Math.abs(strength) < 1e-5f) return;
// strength > 0 → nach unten ziehen; strength < 0 → nach oben ziehen
Vector3f target = new Vector3f(0, strength > 0 ? -1f : 1f, 0);
Vector3f axis = dir.cross(target);
float sinFull = axis.length();
if (sinFull < 1e-6f) return;
axis.divideLocal(sinFull);
float fullAngle = FastMath.atan2(sinFull, dir.dot(target));
float step = Math.abs(strength) / Math.max(0.01f, radius);
float clamped = FastMath.clamp(step, 0f, Math.abs(fullAngle));
new Quaternion().fromAngleAxis(clamped, axis).multLocal(dir);
dir.normalizeLocal();
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private static float lerp(float a, float b, float t) { return a + (b - a) * t; }
// ── Zylinder-Segment ─────────────────────────────────────────────────────
private static void addCylinder(VertexCollector col,
Vector3f start, Vector3f end,
float rBot, float rTop,
float windBot, float windTop,
int N) {
Vector3f axis = end.subtract(start);
if (axis.lengthSquared() < 1e-8f) return;
axis.normalizeLocal();
Vector3f perp1 = (Math.abs(axis.y) < 0.9f)
? axis.cross(Vector3f.UNIT_Y).normalizeLocal()
: axis.cross(Vector3f.UNIT_X).normalizeLocal();
Vector3f perp2 = axis.cross(perp1).normalizeLocal();
int base = col.vertexCount;
int N1 = N + 1;
for (int ring = 0; ring < 2; ring++) {
Vector3f center = (ring == 0) ? start : end;
float r = (ring == 0) ? rBot : rTop;
float wind = (ring == 0) ? windBot : windTop;
float vCoord = ring;
for (int i = 0; i <= N; i++) {
float theta = FastMath.TWO_PI * i / N;
float cos = FastMath.cos(theta);
float sin = FastMath.sin(theta);
float nx = cos * perp1.x + sin * perp2.x;
float ny = cos * perp1.y + sin * perp2.y;
float nz = cos * perp1.z + sin * perp2.z;
col.add(center.x + nx * r, center.y + ny * r, center.z + nz * r,
nx, ny, nz,
(float) i / N, vCoord, wind);
}
}
for (int i = 0; i < N; i++) {
int b0 = base + i, b1 = base + i + 1;
int t0 = base + N1 + i, t1 = base + N1 + i + 1;
col.tri(b0, b1, t1);
col.tri(b0, t1, t0);
}
}
// ── Blatt-Cluster ────────────────────────────────────────────────────────
private static void addLeafCluster(VertexCollector col, Vector3f tip,
float wind, float scale, int count,
float angleDeg, Rng rng) {
for (int i = 0; i < count; i++) {
float ox = rng.range(-scale * 0.5f, scale * 0.5f);
float oy = rng.range(-scale * 0.25f, scale * 0.25f);
float oz = rng.range(-scale * 0.5f, scale * 0.5f);
float s = scale * (0.7f + rng.range(0f, 0.6f));
float tilt = angleDeg * FastMath.DEG_TO_RAD;
addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz,
s, wind, rng.range(0f, FastMath.TWO_PI), tilt);
}
}
private static void addLeafQuad(VertexCollector col,
float cx, float cy, float cz,
float s, float wind, float yRot, float tilt) {
float cosY = FastMath.cos(yRot), sinY = FastMath.sin(yRot);
float cosT = FastMath.cos(tilt), sinT = FastMath.sin(tilt);
float hw = s * 0.5f;
float hh = s * 0.65f;
// Quad A
for (int q = 0; q < 2; q++) {
// second quad perpendicular (yRot + PI/2)
float cy2 = (q == 0) ? cosY : -sinY;
float sz2 = (q == 0) ? sinY : cosY;
int base = col.vertexCount;
// 4 Ecken: unten-links, unten-rechts, oben-rechts, oben-links
// "oben" ist um tilt-Grad nach vorne/hinten geneigt
float[] xs = {-hw * cy2, hw * cy2, hw * cy2, -hw * cy2};
float[] zs = {-hw * sz2, hw * sz2, hw * sz2, -hw * sz2};
float[] ys = {-hh * cosT, -hh * cosT, hh * cosT, hh * cosT};
// Face normal = right × up = (cy2,0,sz2) × (0,1,0) = (-sz2, 0, cy2)
float nnx = -sz2, nnz = cy2;
col.add(cx + xs[0], cy + ys[0], cz + zs[0], nnx, 0, nnz, 0, 0, wind);
col.add(cx + xs[1], cy + ys[1], cz + zs[1], nnx, 0, nnz, 1, 0, wind);
col.add(cx + xs[2], cy + ys[2], cz + zs[2], nnx, 0, nnz, 1, 1, wind);
col.add(cx + xs[3], cy + ys[3], cz + zs[3], nnx, 0, nnz, 0, 1, wind);
col.tri(base, base+1, base+2);
col.tri(base, base+2, base+3);
col.tri(base+2, base+1, base); // back face
col.tri(base+3, base+2, base);
}
}
// ── Ast-Richtung berechnen ────────────────────────────────────────────────
private static Vector3f branchDir(Vector3f parent, float yRot, float tiltDeg) {
Vector3f perp = (Math.abs(parent.y) < 0.9f)
? parent.cross(Vector3f.UNIT_Y).normalizeLocal()
: parent.cross(Vector3f.UNIT_X).normalizeLocal();
Quaternion tilt = new Quaternion().fromAngleAxis(tiltDeg * FastMath.DEG_TO_RAD, perp);
Quaternion spin = new Quaternion().fromAngleAxis(yRot, parent);
return spin.mult(tilt.mult(parent)).normalizeLocal();
}
// ── BoundingBox ───────────────────────────────────────────────────────────
private static BoundingBox computeBounds(VertexCollector col) {
if (col.pos.isEmpty()) return new BoundingBox();
float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE, minZ = Float.MAX_VALUE;
float maxX = -Float.MAX_VALUE, maxY = -Float.MAX_VALUE, maxZ = -Float.MAX_VALUE;
for (int i = 0; i < col.pos.size(); i += 3) {
float x = col.pos.get(i), y = col.pos.get(i+1), z = col.pos.get(i+2);
if (x < minX) minX = x; if (x > maxX) maxX = x;
if (y < minY) minY = y; if (y > maxY) maxY = y;
if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
}
return new BoundingBox(
new Vector3f((minX+maxX)*0.5f, (minY+maxY)*0.5f, (minZ+maxZ)*0.5f),
(maxX-minX)*0.5f, (maxY-minY)*0.5f, (maxZ-minZ)*0.5f);
}
// ── MWC-RNG (portiert aus ez-tree/rng.js) ────────────────────────────────
private static final class Rng {
private long w, z;
Rng(int seed) {
w = (123456789L + seed) & 0xFFFFFFFFL;
z = (987654321L - seed) & 0xFFFFFFFFL;
}
/** Gleichverteilte Zufallszahl in [0, 1). */
float next() {
z = (36969L * (z & 65535L) + (z >> 16)) & 0xFFFFFFFFL;
w = (18000L * (w & 65535L) + (w >> 16)) & 0xFFFFFFFFL;
long result = ((z << 16) + (w & 65535L)) & 0xFFFFFFFFL;
return (float) result / 4294967296f;
}
float range(float lo, float hi) { return lo + (hi - lo) * next(); }
}
// ── Vertex-Sammler ────────────────────────────────────────────────────────
private static final class VertexCollector {
final List<Float> pos = new ArrayList<>();
final List<Float> norm = new ArrayList<>();
final List<Float> uv = new ArrayList<>();
final List<Float> col = new ArrayList<>();
final List<Integer> idx = new ArrayList<>();
int vertexCount = 0;
void add(float x, float y, float z,
float nx, float ny, float nz,
float u, float v, float wind) {
pos.add(x); pos.add(y); pos.add(z);
norm.add(nx); norm.add(ny); norm.add(nz);
uv.add(u); uv.add(v);
col.add(wind); col.add(0f); col.add(0f); col.add(1f);
vertexCount++;
}
void tri(int a, int b, int c) { idx.add(a); idx.add(b); idx.add(c); }
Mesh toMesh() {
if (vertexCount == 0) return new Mesh();
int n = vertexCount;
FloatBuffer posB = BufferUtils.createFloatBuffer(n * 3);
FloatBuffer normB = BufferUtils.createFloatBuffer(n * 3);
FloatBuffer uvB = BufferUtils.createFloatBuffer(n * 2);
FloatBuffer colB = BufferUtils.createFloatBuffer(n * 4);
IntBuffer idxB = BufferUtils.createIntBuffer(idx.size());
for (Float f : pos) posB.put(f);
for (Float f : norm) normB.put(f);
for (Float f : uv) uvB.put(f);
for (Float f : col) colB.put(f);
for (Integer i : idx) idxB.put(i);
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, posB);
mesh.setBuffer(VertexBuffer.Type.Normal, 3, normB);
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uvB);
mesh.setBuffer(VertexBuffer.Type.Color, 4, colB);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idxB);
mesh.updateBound();
return mesh;
}
}
}

View File

@@ -0,0 +1,194 @@
package de.blight.editor.tree;
/**
* Parameter für den prozeduralen Baum-Generator.
*
* Per-Level-Arrays (Index 0 = Stamm, 1 = Hauptäste, 2 = Sekundäräste, 3 = Endäste).
* Inspiriert von ez-tree (github.com/dgreenheck/ez-tree).
*/
public class TreeParams {
// ── Global ────────────────────────────────────────────────────────────────
public int seed = 42;
public int levels = 3; // Rekursionstiefe (14)
public float gravityStrength = 0.03f; // >0 = Äste hängen, <0 = aufwärts (Tanne)
// ── Per-Level-Arrays (Index = Level) ──────────────────────────────────────
/** Astwinkel zur Eltern-Richtung in Grad (Level 0 = Stamm, ignoriert). */
public float[] angle = { 0f, 55f, 45f, 30f };
/** Anzahl Kind-Äste, die ein Level-L-Ast erzeugt (bei Level-L = tiefstem Level → Blätter). */
public int[] children = { 5, 4, 3, 0 };
/** Ab welchem Anteil der Eltern-Astlänge Kinder beginnen (0 = ganz unten, 0.4 = ab 40%). */
public float[] start = { 0.0f, 0.35f, 0.25f, 0.10f };
/** Länge eines Astes auf diesem Level in Welteinheiten. */
public float[] length = { 14f, 10f, 7f, 1.5f};
/** Startradius eines Astes auf diesem Level. */
public float[] radius = { 0.40f, 0.18f, 0.10f, 0.06f };
/** Anzahl Sektionen pro Ast (mehr = organischerer Kurvenverlauf). */
public int[] sections = { 8, 6, 4, 2 };
/** Radiale Segmente pro Sektion (Zylinder-Querschnitt). */
public int[] segments = { 8, 6, 4, 3 };
/** Radius-Abnahme-Faktor pro Sektion (0.51.0). */
public float[] taper = { 0.70f, 0.65f, 0.65f, 0.70f };
/** Zufällige Richtungsabweichung; dünne Äste wackeln stärker. */
public float[] gnarliness= { 0.00f, 0.10f, 0.20f, 0.05f };
// ── Blätter ───────────────────────────────────────────────────────────────
public boolean generateLeaves = true;
public float leafScale = 1.2f;
public int leafCount = 5;
public float leafAngle = 45f;
/** Anzahl kleiner Seiten-Zweige am letzten Ast (0 = keine, 3 = dicht belaubt). */
public int leafBranchings = 1;
// ── Texturen (relative Pfade für den Asset-Manager) ──────────────────────
public String barkTexture = null; // z.B. "Textures/bark/Bark001_Color.jpg"
public String leafTexture = null; // z.B. "Textures/leaves/oak.png"
// ── Wind ─────────────────────────────────────────────────────────────────
public float trunkFlexibility = 0.05f;
public float branchFlexibility = 0.90f;
// ── Presets ───────────────────────────────────────────────────────────────
public static TreeParams oak() {
TreeParams p = new TreeParams();
p.seed = 35729;
p.levels = 3;
p.gravityStrength = 0.04f;
p.angle = new float[]{ 0f, 54f, 58f, 32f };
p.children = new int[] { 6, 4, 3, 0 };
p.start = new float[]{ 0.0f, 0.35f, 0.20f, 0.10f };
p.length = new float[]{ 14f, 11f, 8f, 1.5f};
p.radius = new float[]{ 0.45f, 0.20f, 0.11f, 0.06f };
p.sections = new int[] { 8, 6, 4, 2 };
p.segments = new int[] { 8, 6, 4, 3 };
p.taper = new float[]{ 0.73f, 0.65f, 0.69f, 0.75f };
p.gnarliness= new float[]{ 0.00f, 0.10f, 0.15f, 0.09f };
p.leafScale = 1.4f; p.leafCount = 6; p.leafAngle = 42f; p.leafBranchings = 2;
p.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f;
p.barkTexture = "Textures/bark/Bark001_Color.jpg";
p.leafTexture = "Textures/leaves/oak.png";
return p;
}
public static TreeParams birch() {
TreeParams p = new TreeParams();
p.seed = 11204;
p.levels = 3;
p.gravityStrength = 0.01f;
p.angle = new float[]{ 0f, 45f, 40f, 25f };
p.children = new int[] { 4, 3, 3, 0 };
p.start = new float[]{ 0.0f, 0.45f, 0.30f, 0.10f };
p.length = new float[]{ 18f, 12f, 6f, 1.2f};
p.radius = new float[]{ 0.30f, 0.12f, 0.07f, 0.04f };
p.sections = new int[] { 10, 7, 4, 2 };
p.segments = new int[] { 7, 5, 4, 3 };
p.taper = new float[]{ 0.68f, 0.60f, 0.62f, 0.70f };
p.gnarliness= new float[]{ 0.00f, 0.05f, 0.10f, 0.04f };
p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1;
p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f;
p.barkTexture = "Textures/bark/Bark002_Color.jpg";
p.leafTexture = "Textures/leaves/aspen.png";
return p;
}
public static TreeParams pine() {
TreeParams p = new TreeParams();
p.seed = 72831;
p.levels = 3;
p.gravityStrength = -0.015f;
p.angle = new float[]{ 0f, 75f, 60f, 40f };
p.children = new int[] { 7, 5, 4, 0 };
p.start = new float[]{ 0.0f, 0.15f, 0.20f, 0.10f };
p.length = new float[]{ 12f, 7f, 4f, 0.8f};
p.radius = new float[]{ 0.35f, 0.12f, 0.07f, 0.04f };
p.sections = new int[] { 8, 5, 3, 2 };
p.segments = new int[] { 7, 5, 4, 3 };
p.taper = new float[]{ 0.65f, 0.58f, 0.60f, 0.65f };
p.gnarliness= new float[]{ 0.00f, 0.03f, 0.08f, 0.02f };
p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1;
p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f;
p.barkTexture = "Textures/bark/Bark003_Color.jpg";
p.leafTexture = "Textures/leaves/pine.png";
return p;
}
public static TreeParams willow() {
TreeParams p = new TreeParams();
p.seed = 54321;
p.levels = 3;
p.gravityStrength = 0.12f;
p.angle = new float[]{ 0f, 60f, 50f, 35f };
p.children = new int[] { 5, 4, 3, 0 };
p.start = new float[]{ 0.0f, 0.30f, 0.20f, 0.10f };
p.length = new float[]{ 10f, 12f, 8f, 2.0f};
p.radius = new float[]{ 0.38f, 0.16f, 0.09f, 0.05f };
p.sections = new int[] { 8, 8, 5, 3 };
p.segments = new int[] { 7, 5, 4, 3 };
p.taper = new float[]{ 0.72f, 0.68f, 0.65f, 0.72f };
p.gnarliness= new float[]{ 0.00f, 0.25f, 0.35f, 0.15f };
p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2;
p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f;
p.barkTexture = "Textures/bark/Bark001_Color.jpg";
p.leafTexture = "Textures/leaves/ash.png";
return p;
}
public static TreeParams bush() {
TreeParams p = new TreeParams();
p.seed = 9876;
p.levels = 2;
p.gravityStrength = 0.02f;
p.angle = new float[]{ 0f, 65f, 55f, 40f };
p.children = new int[] { 8, 5, 0, 0 };
p.start = new float[]{ 0.0f, 0.10f, 0.15f, 0.10f };
p.length = new float[]{ 2f, 2f, 1.2f, 0.5f};
p.radius = new float[]{ 0.18f, 0.08f, 0.05f, 0.03f };
p.sections = new int[] { 4, 4, 3, 2 };
p.segments = new int[] { 6, 5, 4, 3 };
p.taper = new float[]{ 0.65f, 0.60f, 0.62f, 0.65f };
p.gnarliness= new float[]{ 0.00f, 0.15f, 0.25f, 0.10f };
p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2;
p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f;
p.barkTexture = "Textures/bark/Bark001_Color.jpg";
p.leafTexture = "Textures/leaves/ash.png";
return p;
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
public TreeParams copy() {
TreeParams c = new TreeParams();
c.seed = seed;
c.levels = levels;
c.gravityStrength = gravityStrength;
c.angle = angle.clone();
c.children = children.clone();
c.start = start.clone();
c.length = length.clone();
c.radius = radius.clone();
c.sections = sections.clone();
c.segments = segments.clone();
c.taper = taper.clone();
c.gnarliness = gnarliness.clone();
c.generateLeaves = generateLeaves;
c.leafScale = leafScale;
c.leafCount = leafCount;
c.leafAngle = leafAngle;
c.leafBranchings = leafBranchings;
c.trunkFlexibility = trunkFlexibility;
c.branchFlexibility = branchFlexibility;
c.barkTexture = barkTexture;
c.leafTexture = leafTexture;
return c;
}
/** Liefert arr[level], bei Überlauf den letzten Wert. */
public static float lv(float[] arr, int level) {
return arr[Math.min(level, arr.length - 1)];
}
public static int lv(int[] arr, int level) {
return arr[Math.min(level, arr.length - 1)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.