Arbeiten aus dem URlaub
2
blight-editor/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/.gradle/
|
||||
/build/
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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" "$@"
|
||||
|
||||
184
blight-editor/gradlew.bat
vendored
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
blight-editor/src/main/java/de/blight/editor/FrameTransfer.class
Normal 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);
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
BIN
blight-editor/src/main/java/de/blight/editor/SharedInput.class
Normal 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<>();
|
||||
}
|
||||
|
||||
@@ -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"; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.1–5.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
BIN
blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 Tex1–Tex3; 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); }
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, 0–3)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (1–4)
|
||||
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.5–1.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)];
|
||||
}
|
||||
}
|
||||
BIN
blight-editor/src/main/resources/img/editor/grasstool.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
blight-editor/src/main/resources/img/editor/terraintool.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 93 KiB |
BIN
blight-editor/src/main/resources/img/editor/textruretool.png
Normal file
|
After Width: | Height: | Size: 115 KiB |