diff --git a/.gitignore b/.gitignore index 398e814..6d729f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,74 +1,8 @@ -# ── Gradle ────────────────────────────────────────────────────────────────── -.gradle/ -**/build/ -gradle-app.setting -!gradle/wrapper/gradle-wrapper.jar -!**/gradle/wrapper/gradle-wrapper.jar +/.metadata/ +/.gradle/ -# ── Kompilate & Archive ────────────────────────────────────────────────────── -*.class -*.jar -!gradle/wrapper/gradle-wrapper.jar -!**/gradle/wrapper/gradle-wrapper.jar - -# ── Laufzeit-Konfiguration (benutzerspezifisch, nicht ins Repo) ────────────── -/config/ -/blight-game/config/ -/blight-editor/config/ -/blight-editor/editor-assets/ - -# ── JVM-Crashdumps & Logs ──────────────────────────────────────────────────── -hs_err_pid*.log -replay_pid*.log -*.log -nohup.out - -# ── Native Bibliotheken (werden beim Build extrahiert) ─────────────────────── -*.so -*.dll -*.dylib -*.jnilib - -# ── IntelliJ IDEA ──────────────────────────────────────────────────────────── -.idea/ -*.iml -*.ipr -*.iws -out/ - -# ── Eclipse ────────────────────────────────────────────────────────────────── +# Eclipse project files .project .classpath .settings/ -*.launch -.factorypath - -# ── NetBeans ───────────────────────────────────────────────────────────────── -nbproject/ -nbactions.xml -nb-configuration.xml - -# ── VS Code ────────────────────────────────────────────────────────────────── -.vscode/ - -# ── macOS ──────────────────────────────────────────────────────────────────── -.DS_Store -.AppleDouble -.LSOverride - -# ── Windows ────────────────────────────────────────────────────────────────── -Thumbs.db -desktop.ini -ehthumbs.db - -# ── Linux ──────────────────────────────────────────────────────────────────── -*~ -.fuse_hidden* - -# ── Editor-Backup & Temporärdateien ───────────────────────────────────────── -*.swp -*.swo -*.tmp -*.temp -*.bak -*.orig +bin/ diff --git a/.metadata/.lock_info b/.metadata/.lock_info index 02c94a1..0f01640 100644 --- a/.metadata/.lock_info +++ b/.metadata/.lock_info @@ -1,5 +1,4 @@ -#Thu May 07 06:27:32 CEST 2026 -display=\:0 -host=mario-mint -process-id=5833 -user=mario +#Sun May 10 10:13:47 CEST 2026 +host=BOOK-0F2PQ8OG21 +process-id=27380 +user=mario diff --git a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json index ac54edc..bb47060 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json +++ b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json @@ -1,7 +1,7 @@ [ { - "version" : "9.5.1-20260506020515+0000", - "buildTime" : "20260506020515+0000", - "commitId" : "b040c334940608c621fdbb778381ebdae66606d4", + "version" : "9.5.1-20260510022507+0000", + "buildTime" : "20260510022507+0000", + "commitId" : "fd78213f09782e62ca4957f9cfd3d90c6c3f1767", "current" : false, "snapshot" : true, "nightly" : false, @@ -10,15 +10,15 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260506020515+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260506020515+0000-bin.zip.sha256", - "checksum" : "ac52cb57303ce63a3131a7c079776ba70fc2b3e317dd311a27c092c96dc953f4", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260506020515+0000-wrapper.jar.sha256", + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260510022507+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260510022507+0000-bin.zip.sha256", + "checksum" : "31ee63072850e69db0372d24655dbef7680aee3afaec2442d6395fc7ca672fd2", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260510022507+0000-wrapper.jar.sha256", "wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" }, { - "version" : "9.6.0-20260506005135+0000", - "buildTime" : "20260506005135+0000", - "commitId" : "44a190b8db5f6911020cba90f7a2642c02eba7bb", + "version" : "9.6.0-20260510003052+0000", + "buildTime" : "20260510003052+0000", + "commitId" : "62000451ad7b25de53fa89a155ef8ecb401621bb", "current" : false, "snapshot" : true, "nightly" : true, @@ -27,11 +27,28 @@ "rcFor" : "", "milestoneFor" : "", "broken" : false, - "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260506005135+0000-bin.zip", - "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260506005135+0000-bin.zip.sha256", - "checksum" : "b746b4a02191ca19a49a8d492e371328b65d251f5aa463d9f68f96349d969546", - "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260506005135+0000-wrapper.jar.sha256", + "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260510003052+0000-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260510003052+0000-bin.zip.sha256", + "checksum" : "7dca215df18a26f56705b82a860eed7dde169117d0f298d4ed0a296778a3e1e4", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260510003052+0000-wrapper.jar.sha256", "wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" +}, { + "version" : "8.14.5", + "buildTime" : "20260507110329+0000", + "commitId" : "62345becae08b13e793521816d585102fea66398", + "current" : false, + "snapshot" : false, + "nightly" : false, + "releaseNightly" : false, + "activeRc" : false, + "rcFor" : "", + "milestoneFor" : "", + "broken" : false, + "downloadUrl" : "https://services.gradle.org/distributions/gradle-8.14.5-bin.zip", + "checksumUrl" : "https://services.gradle.org/distributions/gradle-8.14.5-bin.zip.sha256", + "checksum" : "6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854", + "wrapperChecksumUrl" : "https://services.gradle.org/distributions/gradle-8.14.5-wrapper.jar.sha256", + "wrapperChecksum" : "7d3a4ac4de1c32b59bc6a4eb8ecb8e612ccd0cf1ae1e99f66902da64df296172" }, { "version" : "9.5.0", "buildTime" : "20260428120530+0000", diff --git a/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle b/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle index 6153e78..b924492 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle +++ b/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle @@ -1,14 +1,14 @@ -/******************************************************************************* - * Copyright (c) 2023 Gradle Inc. and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - ******************************************************************************/ -initscript { - allprojects { - apply plugin: "eclipse" - } +/******************************************************************************* + * Copyright (c) 2023 Gradle Inc. and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ******************************************************************************/ +initscript { + allprojects { + apply plugin: "eclipse" + } } \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game index b7e6497..3156a95 100644 --- a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game +++ b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game @@ -1,12 +1,12 @@ -# -#Wed May 06 22:54:49 CEST 2026 -buildDir=build -buildScriptPath=build.gradle -classpath=\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n -derivedResources=.gradle\:build -gradleVersion=8.7 -hasAutoBuildTasks=false -linkedResources= -managedBuilders= -managedNatures= -subprojectPaths= +# +#Sun May 10 10:46:10 CEST 2026 +buildDir=build +buildScriptPath=build.gradle +classpath=\n\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\n +derivedResources=.gradle;build +gradleVersion=8.9 +hasAutoBuildTasks=false +linkedResources= +managedBuilders= +managedNatures= +subprojectPaths= diff --git a/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml index 4498ff9..9d6570f 100644 --- a/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml +++ b/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml @@ -1,13 +1,13 @@ - -
-
- -
- - - - - -
-
-
+ +
+
+ +
+ + + + + +
+
+
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/2/1073fe5c8a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/2/1073fe5c8a4900111396ab8a4c8e537b deleted file mode 100644 index 3956c47..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/2/1073fe5c8a4900111396ab8a4c8e537b +++ /dev/null @@ -1,12 +0,0 @@ -arguments= -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -eclipse.preferences.version=1 -gradle.user.home= -java.home= -jvm.arguments= -offline.mode=false -override.workspace.settings=false -show.console.view=false -show.executions.view=false diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/5/500711f38a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/5/500711f38a4900111396ab8a4c8e537b deleted file mode 100644 index 8377c9d..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/5/500711f38a4900111396ab8a4c8e537b +++ /dev/null @@ -1,35 +0,0 @@ -package de.blight.game; - -import com.jme3.app.SimpleApplication; -import com.jme3.system.AppSettings; -import de.blight.game.scene.WorldScene; - -public class BlightApp extends SimpleApplication { - - public static void main(String[] args) { - BlightApp app = new BlightApp(); - - AppSettings settings = new AppSettings(true); - settings.setTitle("Blight"); - settings.setResolution(1280, 720); - settings.setFrameRate(60); - settings.setVSync(true); - settings.setSamples(4); - - app.setSettings(settings); - app.setShowSettings(false); - app.start(); - } - - @Override - public void simpleInitApp() { - // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst - flyCam.setEnabled(false); - - stateManager.attach(new WorldScene()); - } - - @Override - public void simpleUpdate(float tpf) { - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/7/90d9d2fb8a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/7/90d9d2fb8a4900111396ab8a4c8e537b deleted file mode 100644 index 8b341c0..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/7/90d9d2fb8a4900111396ab8a4c8e537b +++ /dev/null @@ -1,34 +0,0 @@ -package de.blight.game; - -import com.jme3.app.SimpleApplication; -import com.jme3.system.AppSettings; -import de.blight.game.scene.WorldScene; - -public class BlightApp extends SimpleApplication { - - public static void main(String[] args) { - BlightApp app = new BlightApp(); - - AppSettings settings = new AppSettings(true); - settings.setTitle("Blight"); - settings.setResolution(1280, 720); - settings.setVSync(true); - settings.setSamples(4); - - app.setSettings(settings); - app.setShowSettings(false); - app.start(); - } - - @Override - public void simpleInitApp() { - // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst - flyCam.setEnabled(false); - - stateManager.attach(new WorldScene()); - } - - @Override - public void simpleUpdate(float tpf) { - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/7e/2089c33c8d4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/7e/2089c33c8d4900111396ab8a4c8e537b deleted file mode 100644 index 5859c5b..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/7e/2089c33c8d4900111396ab8a4c8e537b +++ /dev/null @@ -1,307 +0,0 @@ -package de.blight.game.config; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.KeyInput; -import com.jme3.input.MouseInput; -import com.jme3.input.RawInputListener; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.input.event.*; -import com.jme3.input.event.KeyInputEvent; -import com.jme3.input.event.MouseButtonEvent; -import com.jme3.input.event.MouseMotionEvent; -import com.jme3.input.event.JoyAxisEvent; -import com.jme3.input.event.JoyButtonEvent; -import com.jme3.input.event.TouchEvent; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; - -import java.util.ArrayList; -import java.util.List; - -/** - * Overlay-AppState der die Tastenbelegungs-Maske anzeigt. - * - * ESC → Schließen (ohne Speichern) - * Klick auf Row → wartet auf neue Taste - * ESC während Warten → bricht nur die Zuweisung ab - * Speichern → schreibt JSON, ruft onSave-Callback - */ -public class ConfigScreen extends BaseAppState implements RawInputListener { - - // Farben - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f); - private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f); - private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); - private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); - - // ----------------------------------------------------------------------- - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - - private KeyBindings liveBindings; // geteilt mit der ganzen App - private KeyBindings editCopy; // wird beim Öffnen geklont - - private Runnable onSave; // Callback → PlayerInputControl.reloadBindings - - private Node panel; - private List rows = new ArrayList<>(); - private int waitingRow = -1; // -1 = keine Zuweisung aktiv - - // UI-Elemente für Buttons (Bounds in Screen-Koordinaten) - private float saveBtnX, saveBtnY, saveBtnW, saveBtnH; - private float cancelBtnX, cancelBtnY; - - // ----------------------------------------------------------------------- - - private static class Row { - String field; - String label; - BitmapText keyText; - Geometry bg; - float x, y, w, h; // Button-Bounds - } - - // ----------------------------------------------------------------------- - - public ConfigScreen(KeyBindings liveBindings, Runnable onSave) { - this.liveBindings = liveBindings; - this.onSave = onSave; - } - - public boolean isWaiting() { return waitingRow >= 0; } - - // ----------------------------------------------------------------------- - // Lifecycle - // ----------------------------------------------------------------------- - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - editCopy = liveBindings.copy(); - waitingRow = -1; - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addRawInputListener(this); - app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_CfgClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - rows.clear(); - waitingRow = -1; - app.getInputManager().removeRawInputListener(this); - app.getInputManager().deleteMapping("_CfgClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - // ----------------------------------------------------------------------- - // UI aufbauen - // ----------------------------------------------------------------------- - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - - panel = new Node("cfg-panel"); - - // Halbdurchsichtiger Overlay über dem Spiel - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 720, ph = 440; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - // Titel - BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT); - centerText(title, px, py + ph - 40, pw); - panel.attachChild(title); - - BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f)); - centerText(hint, px, py + ph - 70, pw); - panel.attachChild(hint); - - // Reihen - float rowX = px + 30; - float keyX = px + pw - 220; - float rowW = 180; - float rowH = 36; - float startY = py + ph - 110; - float stepY = 48; - - for (int i = 0; i < KeyBindings.ENTRIES.length; i++) { - String[] entry = KeyBindings.ENTRIES[i]; - float ry = startY - i * stepY; - - BitmapText lbl = text(entry[1], 16, COL_TEXT); - lbl.setLocalTranslation(rowX, ry + rowH - 8, 0); - panel.attachChild(lbl); - - Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0); - - BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY); - kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1); - panel.attachChild(kt); - - Row row = new Row(); - row.field = entry[0]; - row.label = entry[1]; - row.keyText = kt; - row.bg = bg; - row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH; - rows.add(row); - } - - // Buttons - float btnW = 160, btnH = 42; - float btnY = py + 25; - saveBtnX = px + pw / 2f - btnW - 15; - saveBtnY = btnY; - saveBtnW = btnW; - saveBtnH = btnH; - cancelBtnX = px + pw / 2f + 15; - cancelBtnY = btnY; - - addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0); - BitmapText saveLabel = text("Speichern", 16, COL_TEXT); - centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW); - panel.attachChild(saveLabel); - - addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0); - BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT); - centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW); - panel.attachChild(cancelLabel); - - guiNode.attachChild(panel); - } - - // ----------------------------------------------------------------------- - // Mausklick - // ----------------------------------------------------------------------- - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f cursor = app.getInputManager().getCursorPosition(); - - // Reihen prüfen - for (int i = 0; i < rows.size(); i++) { - Row r = rows.get(i); - if (hits(cursor, r.x, r.y, r.w, r.h)) { - waitingRow = i; - r.bg.getMaterial().setColor("Color", COL_ROW_WAIT); - r.keyText.setText("..."); - return; - } - } - - // Speichern - if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) { - liveBindings.copyFrom(editCopy); - KeyBindingStore.save(liveBindings); - if (onSave != null) onSave.run(); - setEnabled(false); - return; - } - - // Abbrechen - if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) { - setEnabled(false); - } - }; - - // ----------------------------------------------------------------------- - // Tastendruck beim Warten auf Zuweisung (RawInputListener) - // ----------------------------------------------------------------------- - - @Override - public void onKeyEvent(KeyInputEvent evt) { - if (!evt.isPressed() || waitingRow < 0) return; - - if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) { - // Zuweisung abbrechen, Maske offen lassen - resetRowColor(waitingRow); - waitingRow = -1; - return; - } - - Row r = rows.get(waitingRow); - editCopy.set(r.field, evt.getKeyCode()); - r.keyText.setText(KeyNames.of(evt.getKeyCode())); - resetRowColor(waitingRow); - waitingRow = -1; - } - - private void resetRowColor(int idx) { - rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW); - } - - // RawInputListener-Pflichtmethoden - @Override public void beginInput() {} - @Override public void endInput() {} - @Override public void onMouseMotionEvent(MouseMotionEvent evt) {} - @Override public void onMouseButtonEvent(MouseButtonEvent evt) {} - @Override public void onJoyAxisEvent(JoyAxisEvent evt) {} - @Override public void onJoyButtonEvent(JoyButtonEvent evt) {} - @Override public void onTouchEvent(TouchEvent evt) {} - - // ----------------------------------------------------------------------- - // Hilfsmethoden - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText text(String content, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font, false); - t.setSize(size); - t.setColor(color); - t.setText(content); - return t; - } - - private void centerText(BitmapText t, float x, float y, float width) { - t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); - } - - private boolean hits(Vector2f p, float x, float y, float w, float h) { - return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/98/a08ecd828a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/98/a08ecd828a4900111396ab8a4c8e537b deleted file mode 100644 index a64207a..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/98/a08ecd828a4900111396ab8a4c8e537b +++ /dev/null @@ -1,90 +0,0 @@ -package de.blight.game.control; - -import com.jme3.input.InputManager; -import com.jme3.input.KeyInput; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.*; -import com.jme3.math.*; -import com.jme3.renderer.Camera; -import com.jme3.scene.Spatial; - -/** - * Bewegt einen Charakter relativ zur Kamera-Horizontal-Richtung - * und dreht ihn in Bewegungsrichtung. - */ -public class CharacterControl { - - private static final float MOVE_SPEED = 8f; - private static final float ROTATE_SPEED = 5f; - - private final InputManager inputManager; - private final Camera cam; - - private Spatial character; - - // Eingabezustand - private boolean forward, backward, left, right; - - public CharacterControl(InputManager inputManager, Camera cam) { - this.inputManager = inputManager; - this.cam = cam; - registerMappings(); - } - - public void setCharacter(Spatial character) { - this.character = character; - } - - // ----------------------------------------------------------------------- - // Eingabe-Registrierung - // ----------------------------------------------------------------------- - - private void registerMappings() { - inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W), new KeyTrigger(KeyInput.KEY_UP)); - inputManager.addMapping("Backward", new KeyTrigger(KeyInput.KEY_S), new KeyTrigger(KeyInput.KEY_DOWN)); - inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A), new KeyTrigger(KeyInput.KEY_LEFT)); - inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D), new KeyTrigger(KeyInput.KEY_RIGHT)); - - ActionListener listener = (name, isPressed, tpf) -> { - switch (name) { - case "Forward" -> forward = isPressed; - case "Backward" -> backward = isPressed; - case "Left" -> left = isPressed; - case "Right" -> right = isPressed; - } - }; - - inputManager.addListener(listener, "Forward", "Backward", "Left", "Right"); - } - - // ----------------------------------------------------------------------- - // Update pro Frame - // ----------------------------------------------------------------------- - - public void update(float tpf) { - if (character == null) return; - - // Kamerahorizontale Vorwärtsrichtung (Y ignorieren) - Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal(); - Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal(); - - Vector3f moveDir = Vector3f.ZERO.clone(); - if (forward) moveDir.addLocal(camDir); - if (backward) moveDir.subtractLocal(camDir); - if (left) moveDir.addLocal(camLeft); - if (right) moveDir.subtractLocal(camLeft); - - if (moveDir.lengthSquared() > 0.001f) { - moveDir.normalizeLocal(); - character.move(moveDir.mult(MOVE_SPEED * tpf)); - - // Charakter zur Bewegungsrichtung drehen - Quaternion targetRot = new Quaternion(); - targetRot.lookAt(moveDir, Vector3f.UNIT_Y); - - Quaternion current = character.getLocalRotation(); - current.slerp(targetRot, ROTATE_SPEED * tpf); - character.setLocalRotation(current); - } - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/b9/f019f32b8b4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/b9/f019f32b8b4900111396ab8a4c8e537b deleted file mode 100644 index 6c0f1c7..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/b9/f019f32b8b4900111396ab8a4c8e537b +++ /dev/null @@ -1,33 +0,0 @@ -package de.blight.game; - -import com.jme3.app.SimpleApplication; -import com.jme3.system.AppSettings; -import de.blight.game.scene.WorldScene; - -public class BlightApp extends SimpleApplication { - - public static void main(String[] args) { - BlightApp app = new BlightApp(); - - AppSettings settings = new AppSettings(true); - settings.setTitle("Blight"); - settings.setResolution(1280, 720); - settings.setSamples(4); - - app.setSettings(settings); - app.setShowSettings(true); - app.start(); - } - - @Override - public void simpleInitApp() { - // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst - flyCam.setEnabled(false); - - stateManager.attach(new WorldScene()); - } - - @Override - public void simpleUpdate(float tpf) { - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/dc/10c4d9948a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/dc/10c4d9948a4900111396ab8a4c8e537b deleted file mode 100644 index 7877a1e..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/dc/10c4d9948a4900111396ab8a4c8e537b +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'blight' diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/ea/408ace308b4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/ea/408ace308b4900111396ab8a4c8e537b deleted file mode 100644 index 738b45a..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/ea/408ace308b4900111396ab8a4c8e537b +++ /dev/null @@ -1,26 +0,0 @@ -package de.blight.game; - -import com.jme3.app.SimpleApplication; -import com.jme3.system.AppSettings; -import de.blight.game.scene.WorldScene; - -public class BlightApp extends SimpleApplication { - - public static void main(String[] args) { - BlightApp app = new BlightApp(); - - app.start(); - } - - @Override - public void simpleInitApp() { - // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst - flyCam.setEnabled(false); - - stateManager.attach(new WorldScene()); - } - - @Override - public void simpleUpdate(float tpf) { - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/f0/b0f574138b4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/f0/b0f574138b4900111396ab8a4c8e537b deleted file mode 100644 index d3af7c8..0000000 --- a/.metadata/.plugins/org.eclipse.core.resources/.history/f0/b0f574138b4900111396ab8a4c8e537b +++ /dev/null @@ -1,33 +0,0 @@ -package de.blight.game; - -import com.jme3.app.SimpleApplication; -import com.jme3.system.AppSettings; -import de.blight.game.scene.WorldScene; - -public class BlightApp extends SimpleApplication { - - public static void main(String[] args) { - BlightApp app = new BlightApp(); - - AppSettings settings = new AppSettings(true); - settings.setTitle("Blight"); - settings.setResolution(1280, 720); - settings.setSamples(4); - - app.setSettings(settings); - app.setShowSettings(false); - app.start(); - } - - @Override - public void simpleInitApp() { - // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst - flyCam.setEnabled(false); - - stateManager.attach(new WorldScene()); - } - - @Override - public void simpleUpdate(float tpf) { - } -} diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location index 086253f..b2c6373 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location and b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.markers.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.markers.snap deleted file mode 100644 index 91d6c54..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.markers.snap and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.syncinfo.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.syncinfo.snap deleted file mode 100644 index 91d6c54..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.syncinfo.snap and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index index f58f0eb..b80d5a7 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index and b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/5d/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/5d/history.index deleted file mode 100644 index 6a07d65..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/5d/history.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/be/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/be/history.index deleted file mode 100644 index 45354d5..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/be/history.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/history.index deleted file mode 100644 index 9489504..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/history.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/history.index deleted file mode 100644 index 16492ee..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/history.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location index 2e68d47..69d80cd 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location and b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers index 78b801f..b8b6ac1 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers and b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers.snap deleted file mode 100644 index 74ee311..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers.snap and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.syncinfo.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.syncinfo.snap deleted file mode 100644 index 91d6c54..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.syncinfo.snap and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index index 12acdc0..227e6f2 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index and b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.markers.snap b/.metadata/.plugins/org.eclipse.core.resources/.root/.markers.snap deleted file mode 100644 index bdb00bf..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.root/.markers.snap and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree b/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree deleted file mode 100644 index 0fb4d8a..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources index 22417f5..155c4ee 100644 Binary files a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources and b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources differ diff --git a/.metadata/.plugins/org.eclipse.core.resources/2.snap b/.metadata/.plugins/org.eclipse.core.resources/2.snap deleted file mode 100644 index 96076fa..0000000 Binary files a/.metadata/.plugins/org.eclipse.core.resources/2.snap and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml b/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml index c83f1a9..3a94050 100644 --- a/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml +++ b/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml @@ -1,30 +1,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi index 1c29e6a..19b0fa1 100644 --- a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi +++ b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi @@ -1,2764 +1,3342 @@ - - - - activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration - - - - - - - - topLevel - shellMaximized - - - - - persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation - persp.actionSet:org.eclipse.ui.cheatsheets.actionSet - persp.actionSet:org.eclipse.search.searchActionSet - persp.actionSet:org.eclipse.text.quicksearch.actionSet - persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation - persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation - persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo - persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet - persp.actionSet:org.eclipse.ui.actionSet.keyBindings - persp.actionSet:org.eclipse.ui.actionSet.openFiles - persp.actionSet:org.springsource.ide.eclipse.commons.launch.actionSet - persp.actionSet:org.eclipse.debug.ui.launchActionSet - persp.actionSet:org.eclipse.jdt.ui.JavaActionSet - persp.actionSet:org.eclipse.jdt.ui.JavaElementCreationActionSet - persp.actionSet:org.eclipse.ui.NavigateActionSet - persp.viewSC:org.eclipse.jdt.ui.PackageExplorer - persp.viewSC:org.eclipse.jdt.ui.TypeHierarchy - persp.viewSC:org.eclipse.jdt.ui.SourceView - persp.viewSC:org.eclipse.jdt.ui.JavadocView - persp.viewSC:org.eclipse.search.ui.views.SearchView - persp.viewSC:org.eclipse.ui.console.ConsoleView - persp.viewSC:org.eclipse.ui.views.ContentOutline - persp.viewSC:org.eclipse.ui.views.ProblemView - persp.viewSC:org.eclipse.ui.views.TaskList - persp.viewSC:org.eclipse.ui.views.ProgressView - persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer - persp.viewSC:org.eclipse.ui.texteditor.TemplatesView - persp.viewSC:org.eclipse.pde.runtime.LogView - persp.newWizSC:org.eclipse.jdt.ui.wizards.JavaProjectWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewPackageCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewClassCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewInterfaceCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewEnumCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewRecordCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewAnnotationCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSourceFolderCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSnippetFileCreationWizard - persp.newWizSC:org.eclipse.jdt.ui.wizards.NewJavaWorkingSetWizard - persp.newWizSC:org.eclipse.ui.wizards.new.folder - persp.newWizSC:org.eclipse.ui.wizards.new.file - persp.newWizSC:org.eclipse.ui.editors.wizards.UntitledTextFileWizard - persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective - persp.perspSC:org.eclipse.debug.ui.DebugPerspective - persp.showIn:org.eclipse.jdt.ui.PackageExplorer - persp.showIn:org.eclipse.team.ui.GenericHistoryView - persp.showIn:org.eclipse.ui.navigator.ProjectExplorer - persp.viewSC:org.eclipse.mylyn.tasks.ui.views.tasks - persp.newWizSC:org.eclipse.mylyn.tasks.ui.wizards.new.repository.task - persp.viewSC:org.eclipse.terminal.view.ui.TerminalsView - persp.showIn:org.eclipse.terminal.view.ui.TerminalsView - persp.actionSet:org.eclipse.debug.ui.breakpointActionSet - persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet - persp.newWizSC:org.eclipse.m2e.core.wizards.Maven2ProjectWizard - persp.newWizSC:org.springsource.ide.eclipse.commons.gettingstarted.wizard.boot.NewSpringBootWizard - persp.newWizSC:org.springsource.ide.eclipse.gettingstarted.wizards.import.generic.newalias - persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet - persp.showIn:org.eclipse.eclemma.ui.CoverageView - persp.viewSC:org.eclipse.jdt.bcoview.views.BytecodeOutlineView - persp.showIn:org.eclipse.egit.ui.RepositoriesView - persp.newWizSC:org.eclipse.jdt.junit.wizards.NewTestCaseCreationWizard - persp.actionSet:org.eclipse.jdt.junit.JUnitActionSet - persp.viewSC:org.eclipse.ant.ui.views.AntView - persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.jdt.ui/$nl$/icons/full/onboarding_jperspective.svg - persp.editorOnboardingText:Open a file or drop files here to open them. - persp.editorOnboardingCommand:Find Actions$$$Ctrl+3 - persp.editorOnboardingCommand:Show Key Assist$$$Shift+Ctrl+L - persp.editorOnboardingCommand:New$$$Ctrl+N - persp.editorOnboardingCommand:Open Type$$$Shift+Ctrl+T - - - - - org.eclipse.e4.primaryNavigationStack - - View - categoryTag:Java - - - View - categoryTag:Java - - - View - categoryTag:General - - - View - categoryTag:Java - - - - - View - categoryTag:Spring - - - - - - View - categoryTag:Git - - - - - - - - org.eclipse.e4.secondaryNavigationStack - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:Mylyn - - - View - categoryTag:Java - - - View - categoryTag:Ant - - - - - org.eclipse.e4.secondaryDataStack - Oomph - Gradle - - View - categoryTag:General - - - View - categoryTag:Java - - - View - categoryTag:Java - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:General - - - View - categoryTag:Terminal - - - View - categoryTag:Gradle - - - View - categoryTag:Gradle - - - View - categoryTag:Oomph - NoRestore - - - View - categoryTag:Oomph - NoRestore - - - - - - - - - View - categoryTag:Help - - - View - categoryTag:General - - - View - categoryTag:Help - - - - - - - View - categoryTag:Help - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Help - - - - EditorStack - org.eclipse.e4.primaryDataStack - active - noFocus - - - Editor - removeOnHide - org.eclipse.buildship.ui.gradlebuildscripteditor - - - - Editor - removeOnHide - org.eclipse.jdt.ui.CompilationUnitEditor - - - - Editor - removeOnHide - org.eclipse.jdt.ui.CompilationUnitEditor - active - - - - - - - - View - categoryTag:Java - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Java - - - - - View - categoryTag:General - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:General - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - - View - categoryTag:General - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Terminal - - - - - View - categoryTag:Java - - - - - View - categoryTag:Git - - - - - View - categoryTag:Java - - - - - - View - categoryTag:Spring - - ViewMenu - menuContribution:menu - - - - - - - View - categoryTag:Ant - - - - - - View - categoryTag:Gradle - - ViewMenu - menuContribution:menu - - - - - - - - View - categoryTag:Gradle - - ViewMenu - menuContribution:menu - - - - - - - - View - categoryTag:Oomph - NoRestore - - ViewMenu - menuContribution:menu - - - - - - - - View - categoryTag:Oomph - NoRestore - - ViewMenu - menuContribution:menu - - - - - - toolbarSeparator - - - - Draggable - - - - toolbarSeparator - - - - Draggable - - - - - toolbarSeparator - - - - Draggable - - - Draggable - - - Draggable - - - Draggable - - - toolbarSeparator - - - - Draggable - - - - Draggable - - - toolbarSeparator - - - - toolbarSeparator - - - - Draggable - - - stretch - SHOW_RESTORE_MENU - - - Draggable - HIDEABLE - SHOW_RESTORE_MENU - - - - - stretch - - - Draggable - - - Draggable - - - - - TrimStack - Draggable - - - - - - - - - - - - - - - - - - - platform:gtk - - - - - - platform:gtk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - platform:gtk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Editor - removeOnHide - - - - - View - categoryTag:Ant - - - - - View - categoryTag:Gradle - - - - - View - categoryTag:Gradle - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Debug - - - View - categoryTag:Debug - - - - - View - categoryTag:Java - - - - - View - categoryTag:Git - - - - - View - categoryTag:Git - - - - - View - categoryTag:Git - - - - - View - categoryTag:Git - NoRestore - - - - - View - categoryTag:Git - - - - - View - categoryTag:Help - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Debug - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java Browsing - - - - - View - categoryTag:Java - - - - - View - categoryTag:General - - - - - View - categoryTag:Java - - - - - View - categoryTag:Java - - - - - View - categoryTag:Docker - - - - - View - categoryTag:Docker - - - - - View - categoryTag:Docker - - - - - View - categoryTag:Docker - - - - - View - categoryTag:Language Servers - - - - - View - categoryTag:Language Servers - - - - - View - categoryTag:Language Servers - - - - - View - categoryTag:Maven - - - - - View - categoryTag:Maven - - - - - View - categoryTag:Maven - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Mylyn - - - - - View - categoryTag:Oomph - - - - - View - categoryTag:Oomph - NoRestore - - - - - View - categoryTag:Plug-in Development - - - - - View - categoryTag:General - - - - - View - categoryTag:Version Control (Team) - - - - - View - categoryTag:Version Control (Team) - - - - - View - categoryTag:Terminal - - - View - categoryTag:Help - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:Help - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:General - - - - - View - categoryTag:Spring - - - - - View - categoryTag:Spring - - - - - View - categoryTag:Spring - - - - glue - move_after:PerspectiveSpacer - SHOW_RESTORE_MENU - - - move_after:Spacer Glue - HIDEABLE - SHOW_RESTORE_MENU - - - glue - move_after:SearchField - SHOW_RESTORE_MENU - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration + + + + + + + + topLevel + shellMinimized + + + + + persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation + persp.actionSet:org.eclipse.ui.cheatsheets.actionSet + persp.actionSet:org.eclipse.search.searchActionSet + persp.actionSet:org.eclipse.text.quicksearch.actionSet + persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation + persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation + persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo + persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet + persp.actionSet:org.eclipse.ui.actionSet.keyBindings + persp.actionSet:org.eclipse.ui.actionSet.openFiles + persp.actionSet:org.springsource.ide.eclipse.commons.launch.actionSet + persp.actionSet:org.eclipse.debug.ui.launchActionSet + persp.actionSet:org.eclipse.jdt.ui.JavaActionSet + persp.actionSet:org.eclipse.jdt.ui.JavaElementCreationActionSet + persp.actionSet:org.eclipse.ui.NavigateActionSet + persp.viewSC:org.eclipse.jdt.ui.PackageExplorer + persp.viewSC:org.eclipse.jdt.ui.TypeHierarchy + persp.viewSC:org.eclipse.jdt.ui.SourceView + persp.viewSC:org.eclipse.jdt.ui.JavadocView + persp.viewSC:org.eclipse.search.ui.views.SearchView + persp.viewSC:org.eclipse.ui.console.ConsoleView + persp.viewSC:org.eclipse.ui.views.ContentOutline + persp.viewSC:org.eclipse.ui.views.ProblemView + persp.viewSC:org.eclipse.ui.views.TaskList + persp.viewSC:org.eclipse.ui.views.ProgressView + persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer + persp.viewSC:org.eclipse.ui.texteditor.TemplatesView + persp.viewSC:org.eclipse.pde.runtime.LogView + persp.newWizSC:org.eclipse.jdt.ui.wizards.JavaProjectWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewPackageCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewClassCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewInterfaceCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewEnumCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewRecordCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewAnnotationCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSourceFolderCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSnippetFileCreationWizard + persp.newWizSC:org.eclipse.jdt.ui.wizards.NewJavaWorkingSetWizard + persp.newWizSC:org.eclipse.ui.wizards.new.folder + persp.newWizSC:org.eclipse.ui.wizards.new.file + persp.newWizSC:org.eclipse.ui.editors.wizards.UntitledTextFileWizard + persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective + persp.perspSC:org.eclipse.debug.ui.DebugPerspective + persp.showIn:org.eclipse.jdt.ui.PackageExplorer + persp.showIn:org.eclipse.team.ui.GenericHistoryView + persp.showIn:org.eclipse.ui.navigator.ProjectExplorer + persp.viewSC:org.eclipse.mylyn.tasks.ui.views.tasks + persp.newWizSC:org.eclipse.mylyn.tasks.ui.wizards.new.repository.task + persp.viewSC:org.eclipse.terminal.view.ui.TerminalsView + persp.showIn:org.eclipse.terminal.view.ui.TerminalsView + persp.actionSet:org.eclipse.debug.ui.breakpointActionSet + persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet + persp.newWizSC:org.eclipse.m2e.core.wizards.Maven2ProjectWizard + persp.newWizSC:org.springsource.ide.eclipse.commons.gettingstarted.wizard.boot.NewSpringBootWizard + persp.newWizSC:org.springsource.ide.eclipse.gettingstarted.wizards.import.generic.newalias + persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet + persp.showIn:org.eclipse.eclemma.ui.CoverageView + persp.viewSC:org.eclipse.jdt.bcoview.views.BytecodeOutlineView + persp.showIn:org.eclipse.egit.ui.RepositoriesView + persp.newWizSC:org.eclipse.jdt.junit.wizards.NewTestCaseCreationWizard + persp.actionSet:org.eclipse.jdt.junit.JUnitActionSet + persp.viewSC:org.eclipse.ant.ui.views.AntView + persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.jdt.ui/$nl$/icons/full/onboarding_jperspective.svg + persp.editorOnboardingText:Open a file or drop files here to open them. + persp.editorOnboardingCommand:Find Actions$$$Ctrl+3 + persp.editorOnboardingCommand:Show Key Assist$$$Ctrl+Shift+L + persp.editorOnboardingCommand:New$$$Ctrl+N + persp.editorOnboardingCommand:Open Type$$$Ctrl+Shift+T + + + + + org.eclipse.e4.primaryNavigationStack + active + noFocus + + View + categoryTag:Java + + + View + categoryTag:Java + + + View + categoryTag:General + + + View + categoryTag:Java + + + + + View + categoryTag:Spring + + + + + + View + categoryTag:Git + + + + + + + + org.eclipse.e4.secondaryNavigationStack + Minimized + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Mylyn + + + View + categoryTag:Java + + + View + categoryTag:Ant + + + + + org.eclipse.e4.secondaryDataStack + Oomph + Gradle + + View + categoryTag:General + + + View + categoryTag:Java + + + View + categoryTag:Java + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:General + + + View + categoryTag:Terminal + + + View + categoryTag:Gradle + + + View + categoryTag:Gradle + + + + + + + + + View + categoryTag:Help + + + View + categoryTag:General + + + View + categoryTag:Help + + + + + + + View + categoryTag:Help + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Help + + + + EditorStack + org.eclipse.e4.primaryDataStack + + + Editor + removeOnHide + org.eclipse.buildship.ui.gradlebuildscripteditor + + + + Editor + removeOnHide + org.eclipse.buildship.ui.gradlebuildscripteditor + + + + + + + + View + categoryTag:Java + active + activeOnClose + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Java + + + + + View + categoryTag:General + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:General + + + + + + View + categoryTag:General + highlighted + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:General + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:General + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Terminal + + + + + View + categoryTag:Java + + + + + View + categoryTag:Git + + + + + View + categoryTag:Java + + + + + + View + categoryTag:Spring + + ViewMenu + menuContribution:menu + + + + + + + View + categoryTag:Ant + + + + + + View + categoryTag:Gradle + + ViewMenu + menuContribution:menu + + + + + + + + View + categoryTag:Gradle + + ViewMenu + menuContribution:menu + + + + + + toolbarSeparator + + + + Draggable + + + + toolbarSeparator + + + + Draggable + + + + + toolbarSeparator + + + + Draggable + + + Draggable + + + Draggable + + + Draggable + + + toolbarSeparator + + + + Draggable + + + + toolbarSeparator + + + + toolbarSeparator + + + + Draggable + + + stretch + SHOW_RESTORE_MENU + + + Draggable + HIDEABLE + SHOW_RESTORE_MENU + + + + + stretch + + + Draggable + + + Draggable + + + + + TrimStack + Draggable + + + TrimStack + Draggable + + + TrimStack + Draggable + + + + + TrimStack + Draggable + + + TrimStack + Draggable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + platform:win32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + locale:de + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + platform:win32 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Editor + removeOnHide + + + + + View + categoryTag:Ant + + + + + View + categoryTag:Gradle + + + + + View + categoryTag:Gradle + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Debug + + + View + categoryTag:Debug + + + + + View + categoryTag:Java + + + + + View + categoryTag:Git + + + + + View + categoryTag:Git + + + + + View + categoryTag:Git + + + + + View + categoryTag:Git + NoRestore + + + + + View + categoryTag:Git + + + + + View + categoryTag:Help + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java Browsing + + + + + View + categoryTag:Java + + + + + View + categoryTag:General + + + + + View + categoryTag:Java + + + + + View + categoryTag:Java + + + + + View + categoryTag:Docker + + + + + View + categoryTag:Docker + + + + + View + categoryTag:Docker + + + + + View + categoryTag:Docker + + + + + View + categoryTag:Language Servers + + + + + View + categoryTag:Language Servers + + + + + View + categoryTag:Language Servers + + + + + View + categoryTag:Maven + + + + + View + categoryTag:Maven + + + + + View + categoryTag:Maven + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Mylyn + + + + + View + categoryTag:Oomph + + + + + View + categoryTag:Oomph + NoRestore + + + + + View + categoryTag:Plug-in Development + + + + + View + categoryTag:General + + + + + View + categoryTag:Version Control (Team) + + + + + View + categoryTag:Version Control (Team) + + + + + View + categoryTag:Terminal + + + View + categoryTag:Help + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:Help + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:General + + + + + View + categoryTag:Spring + + + + + View + categoryTag:Spring + + + + + View + categoryTag:Spring + + + + + View + categoryTag:Data Management + + + + + View + categoryTag:Data Management + + + + + View + categoryTag:Data Management + + + + + View + categoryTag:General + + + + + View + categoryTag:JPA + + + + + View + categoryTag:JPA + + + + + View + categoryTag:JavaServer Faces + + + + + View + categoryTag:JavaServer Faces + + + + + View + categoryTag:Web Services + + + + + View + categoryTag:API Tools + + + + + View + categoryTag:OSGi + + + + + View + categoryTag:OSGi + + + + + View + categoryTag:Plug-in Development + + + + + View + categoryTag:Plug-in Development + + + + + View + categoryTag:Plug-in Development + + + + + View + categoryTag:Plug-in Development + + + + + View + categoryTag:Plug-in Development + + + + + View + categoryTag:General + + + + + View + categoryTag:Debug + + + + + View + categoryTag:Other + + + + + View + categoryTag:Other + + + + + View + categoryTag:Other + + + + + View + categoryTag:Server + + + + + View + categoryTag:XML + + + + + View + categoryTag:XML + + + + + View + categoryTag:XML + + + + + View + categoryTag:XML + + + + + View + categoryTag:XML + + + + glue + move_after:PerspectiveSpacer + SHOW_RESTORE_MENU + + + move_after:Spacer Glue + HIDEABLE + SHOW_RESTORE_MENU + + + glue + move_after:SearchField + SHOW_RESTORE_MENU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1007707583.index b/.metadata/.plugins/org.eclipse.jdt.core/1007707583.index deleted file mode 100644 index ec5f85a..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1007707583.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/104128081.index b/.metadata/.plugins/org.eclipse.jdt.core/104128081.index deleted file mode 100644 index 2f6e1c1..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/104128081.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1124510679.index b/.metadata/.plugins/org.eclipse.jdt.core/1124510679.index deleted file mode 100644 index 2d2095c..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1124510679.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1136998059.index b/.metadata/.plugins/org.eclipse.jdt.core/1136998059.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1136998059.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1305835383.index b/.metadata/.plugins/org.eclipse.jdt.core/1305835383.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1305835383.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1309346846.index b/.metadata/.plugins/org.eclipse.jdt.core/1309346846.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1309346846.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1317406562.index b/.metadata/.plugins/org.eclipse.jdt.core/1317406562.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1317406562.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1350656539.index b/.metadata/.plugins/org.eclipse.jdt.core/1350656539.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1350656539.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1351491558.index b/.metadata/.plugins/org.eclipse.jdt.core/1351491558.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1351491558.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1462452114.index b/.metadata/.plugins/org.eclipse.jdt.core/1462452114.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1462452114.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1490162601.index b/.metadata/.plugins/org.eclipse.jdt.core/1490162601.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1490162601.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1545642898.index b/.metadata/.plugins/org.eclipse.jdt.core/1545642898.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1545642898.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1550771283.index b/.metadata/.plugins/org.eclipse.jdt.core/1550771283.index deleted file mode 100644 index 48c52ed..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1550771283.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1578672902.index b/.metadata/.plugins/org.eclipse.jdt.core/1578672902.index deleted file mode 100644 index fa97256..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1578672902.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1585095811.index b/.metadata/.plugins/org.eclipse.jdt.core/1585095811.index deleted file mode 100644 index 08502ef..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1585095811.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1608883781.index b/.metadata/.plugins/org.eclipse.jdt.core/1608883781.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1608883781.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/16257995.index b/.metadata/.plugins/org.eclipse.jdt.core/16257995.index deleted file mode 100644 index 97e5408..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/16257995.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1703316476.index b/.metadata/.plugins/org.eclipse.jdt.core/1703316476.index deleted file mode 100644 index 5b5a5b7..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1703316476.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1738064530.index b/.metadata/.plugins/org.eclipse.jdt.core/1738064530.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1738064530.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index b/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index index 01d2b95..0ed5948 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index and b/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1989349782.index b/.metadata/.plugins/org.eclipse.jdt.core/1989349782.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1989349782.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2356684838.index b/.metadata/.plugins/org.eclipse.jdt.core/2356684838.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2356684838.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2392911294.index b/.metadata/.plugins/org.eclipse.jdt.core/2392911294.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2392911294.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/24151692.index b/.metadata/.plugins/org.eclipse.jdt.core/24151692.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/24151692.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2479985475.index b/.metadata/.plugins/org.eclipse.jdt.core/2479985475.index deleted file mode 100644 index d9983f7..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2479985475.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2497820567.index b/.metadata/.plugins/org.eclipse.jdt.core/2497820567.index deleted file mode 100644 index 067d0fd..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2497820567.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/262304292.index b/.metadata/.plugins/org.eclipse.jdt.core/262304292.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/262304292.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2696219173.index b/.metadata/.plugins/org.eclipse.jdt.core/2696219173.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2696219173.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2722916612.index b/.metadata/.plugins/org.eclipse.jdt.core/2722916612.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2722916612.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2783277246.index b/.metadata/.plugins/org.eclipse.jdt.core/2783277246.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2783277246.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2865784985.index b/.metadata/.plugins/org.eclipse.jdt.core/2865784985.index deleted file mode 100644 index ce9ef3d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2865784985.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2893713661.index b/.metadata/.plugins/org.eclipse.jdt.core/2893713661.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2893713661.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3011908894.index b/.metadata/.plugins/org.eclipse.jdt.core/3011908894.index deleted file mode 100644 index 5161ca2..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3011908894.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3040618168.index b/.metadata/.plugins/org.eclipse.jdt.core/3040618168.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3040618168.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3075372551.index b/.metadata/.plugins/org.eclipse.jdt.core/3075372551.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3075372551.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3096076814.index b/.metadata/.plugins/org.eclipse.jdt.core/3096076814.index deleted file mode 100644 index 6c9585d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3096076814.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3190887717.index b/.metadata/.plugins/org.eclipse.jdt.core/3190887717.index deleted file mode 100644 index 6f6ca96..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3190887717.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3282007833.index b/.metadata/.plugins/org.eclipse.jdt.core/3282007833.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3282007833.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3331268705.index b/.metadata/.plugins/org.eclipse.jdt.core/3331268705.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3331268705.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3390708485.index b/.metadata/.plugins/org.eclipse.jdt.core/3390708485.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3390708485.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3432172071.index b/.metadata/.plugins/org.eclipse.jdt.core/3432172071.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3432172071.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3534421922.index b/.metadata/.plugins/org.eclipse.jdt.core/3534421922.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3534421922.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3591837599.index b/.metadata/.plugins/org.eclipse.jdt.core/3591837599.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3591837599.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3791361740.index b/.metadata/.plugins/org.eclipse.jdt.core/3791361740.index deleted file mode 100644 index 5219b23..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3791361740.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3901683754.index b/.metadata/.plugins/org.eclipse.jdt.core/3901683754.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3901683754.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3945373644.index b/.metadata/.plugins/org.eclipse.jdt.core/3945373644.index deleted file mode 100644 index a908c6b..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3945373644.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3950534511.index b/.metadata/.plugins/org.eclipse.jdt.core/3950534511.index deleted file mode 100644 index 70a1baa..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3950534511.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3965639856.index b/.metadata/.plugins/org.eclipse.jdt.core/3965639856.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3965639856.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/4153997317.index b/.metadata/.plugins/org.eclipse.jdt.core/4153997317.index deleted file mode 100644 index c136a22..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/4153997317.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/4255926859.index b/.metadata/.plugins/org.eclipse.jdt.core/4255926859.index deleted file mode 100644 index 7429d2a..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/4255926859.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/436234689.index b/.metadata/.plugins/org.eclipse.jdt.core/436234689.index deleted file mode 100644 index 4dd6727..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/436234689.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/458837781.index b/.metadata/.plugins/org.eclipse.jdt.core/458837781.index deleted file mode 100644 index 1075d7d..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/458837781.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/606255621.index b/.metadata/.plugins/org.eclipse.jdt.core/606255621.index deleted file mode 100644 index 9762973..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/606255621.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/734632761.index b/.metadata/.plugins/org.eclipse.jdt.core/734632761.index deleted file mode 100644 index c1fc277..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/734632761.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/797385254.index b/.metadata/.plugins/org.eclipse.jdt.core/797385254.index deleted file mode 100644 index 6519dca..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/797385254.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/860469874.index b/.metadata/.plugins/org.eclipse.jdt.core/860469874.index deleted file mode 100644 index 59c4d33..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/860469874.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/890886771.index b/.metadata/.plugins/org.eclipse.jdt.core/890886771.index deleted file mode 100644 index 8c8b386..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/890886771.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/900586112.index b/.metadata/.plugins/org.eclipse.jdt.core/900586112.index deleted file mode 100644 index fad63ef..0000000 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/900586112.index and /dev/null differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache index ee55e59..015c674 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache and b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps b/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps index 97e9323..731353d 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps and b/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache b/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache index d62d0eb..08dc8e7 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache and b/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache differ diff --git a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt index 1da94e9..c3b92e9 100644 --- a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt +++ b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt @@ -1,58 +1,65 @@ -INDEX VERSION 1.134+/home/mario/Workspaces/blight/.metadata/.plugins/org.eclipse.jdt.core -3190887717.index -3965639856.index -262304292.index -3534421922.index -1585095811.index -16257995.index -1350656539.index -1136998059.index -458837781.index -860469874.index -1305835383.index -4153997317.index -1550771283.index -900586112.index -3950534511.index -1462452114.index -3331268705.index -1989349782.index -2722916612.index -1738064530.index -1007707583.index -2497820567.index -734632761.index +INDEX VERSION 1.134+C:\Projekte\blight\.metadata\.plugins\org.eclipse.jdt.core +1730180473.index +1560896368.index +3434360348.index +463881636.index +2160052426.index +2563355238.index +280283746.index +4260524346.index +3103640776.index +342961285.index +1271954631.index +2357666455.index +52047090.index +2034054389.index +330211381.index +2878081145.index +842794024.index +3981805811.index +1210571632.index +3496211806.index +1110902487.index +274839682.index +563269584.index +1307459789.index +2565315493.index +447892417.index +746211526.index +1734210249.index +1531656712.index +2352162652.index +3278945159.index +1724804096.index +1680596161.index +1660620864.index +993751451.index +55438146.index +1228247778.index +770656589.index +1962463332.index +3147751859.index +1431966402.index +2109219904.index 1865797976.index -3591837599.index -2392911294.index -1490162601.index -1124510679.index -3011908894.index -2356684838.index -3390708485.index -436234689.index -2865784985.index -1545642898.index -24151692.index -1578672902.index -2783277246.index -606255621.index -797385254.index -3901683754.index -2696219173.index -2479985475.index -4255926859.index -1608883781.index -890886771.index -104128081.index -1703316476.index -3075372551.index -3791361740.index -1351491558.index -1317406562.index -3096076814.index -1309346846.index -3282007833.index -3432172071.index -2893713661.index -3040618168.index +1340110446.index +2032725075.index +2788838607.index +4079747914.index +3296889058.index +633025986.index +3697082729.index +2913097348.index +306663419.index +404070292.index +1839710279.index +3002461991.index +3721901883.index +3822973035.index +887038847.index +2999631420.index +2615701196.index +2049044952.index +3035175596.index +1639262426.index +13457938.index diff --git a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat index b2cd4d5..6c69286 100644 Binary files a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat and b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat differ diff --git a/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml b/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml index 2b4cd2c..ec98ae6 100644 --- a/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml +++ b/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml @@ -1,5 +1,8 @@ - - - - - + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml b/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml index ff55ec7..8a97594 100644 --- a/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml +++ b/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml @@ -1,5 +1,28 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml index a4ee3cb..6cd9562 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml @@ -1,2 +1,2 @@ - - + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml index 9e390f5..8c365b7 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml @@ -1,2 +1,2 @@ - - + + diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml index 1618bcd..312100c 100644 --- a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml +++ b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml @@ -1,16 +1,16 @@ - -
-
- - - - - -
-
-
-
-
-
-
-
+ +
+
+ + + + + +
+
+
+
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml b/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml index 96a5807..5c579a2 100644 --- a/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml +++ b/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml @@ -1,4 +1,4 @@ - - - - + + + + diff --git a/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml b/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml index 9effde7..5fa1b75 100644 --- a/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml +++ b/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml @@ -1,41 +1,41 @@ - - - - %date [%thread] %-5level %logger{35} - %msg%n - - - ${org.eclipse.m2e.log.console.threshold:-OFF} - - - - - ${org.eclipse.m2e.log.dir}/0.log - - ${org.eclipse.m2e.log.dir}/%i.log - 1 - 10 - - - 10MB - - - %date [%thread] %-5level %logger{35} - %msg%n - - - - - - WARN - - - - - - - - - - - - - + + + + %date [%thread] %-5level %logger{35} - %msg%n + + + ${org.eclipse.m2e.log.console.threshold:-OFF} + + + + + ${org.eclipse.m2e.log.dir}/0.log + + ${org.eclipse.m2e.log.dir}/%i.log + 1 + 10 + + + 10MB + + + %date [%thread] %-5level %logger{35} - %msg%n + + + + + + WARN + + + + + + + + + + + + + diff --git a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup index 1f73e14..a1ef8f5 100644 --- a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup +++ b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup @@ -1,6 +1,6 @@ - - + + diff --git a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml index 5ca0b77..1ef2b05 100644 --- a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml +++ b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml @@ -1,3 +1,3 @@ - -
-
+ +
+
diff --git a/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml b/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml index 50f1edb..e4f30a7 100644 --- a/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml +++ b/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml @@ -1,5 +1,5 @@ - -
-
-
-
+ +
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.ui.intro/introstate b/.metadata/.plugins/org.eclipse.ui.intro/introstate index 02f134f..1cc22f5 100644 --- a/.metadata/.plugins/org.eclipse.ui.intro/introstate +++ b/.metadata/.plugins/org.eclipse.ui.intro/introstate @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml index 8e2e4c5..a201ff6 100644 --- a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml +++ b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml @@ -1,19 +1,19 @@ - -
-
- - - - - -
-
- - - - - -
-
-
-
+ +
+
+ + + + + +
+
+ + + + + +
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml index c9635ed..1e6320b 100644 --- a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml +++ b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties b/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties index cd7f694..90990f0 100644 --- a/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties +++ b/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties @@ -1,3 +1,3 @@ -# -#Wed May 06 22:30:24 CEST 2026 -task-tag-projects-already-scanned=blight-game +# +#Sun May 10 09:46:42 CEST 2026 +task-tag-projects-already-scanned=blight-common,blight-editor,blight,blight-game diff --git a/.metadata/version.ini b/.metadata/version.ini index ac8f4f3..a9bc14c 100644 --- a/.metadata/version.ini +++ b/.metadata/version.ini @@ -1,3 +1,3 @@ -#Thu May 07 06:27:32 CEST 2026 -org.eclipse.core.runtime=2 -org.eclipse.platform=4.39.0.v20260226-0420 +#Sun May 10 10:13:47 CEST 2026 +org.eclipse.core.runtime=2 +org.eclipse.platform=4.39.0.v20260226-0420 diff --git a/blight-assets/build.gradle b/blight-assets/build.gradle new file mode 100644 index 0000000..1e2dcc1 --- /dev/null +++ b/blight-assets/build.gradle @@ -0,0 +1,5 @@ +// Gemeinsame Spiel-Assets – kein Java-Quellcode, nur Ressourcen. +// Alle Projekte binden dieses Subprojekt ein, um Zugang zu den +// geteilten Assets (MatDefs, Shaders, Textures) auf dem Classpath zu erhalten. +// +// Editor-spezifische Assets (Tool-Icons etc.) verbleiben in blight-editor. diff --git a/blight-assets/src/main/resources/MatDefs/Grass.j3md b/blight-assets/src/main/resources/MatDefs/Grass.j3md new file mode 100644 index 0000000..19f61be --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/Grass.j3md @@ -0,0 +1,28 @@ +MaterialDef Grass { + + MaterialParameters { + Color Color (Color) : 1.0 1.0 1.0 1.0 + Texture2D ColorMap + Float WindSpeed : 0.5 + Float WindStrength : 0.12 + } + + Technique { + VertexShader GLSL150: Shaders/Grass.vert + FragmentShader GLSL150: Shaders/Grass.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + Time + } + + RenderState { + FaceCull Off + } + + Defines { + HAS_COLORMAP : ColorMap + } + } +} diff --git a/blight-assets/src/main/resources/MatDefs/Tree.j3md b/blight-assets/src/main/resources/MatDefs/Tree.j3md new file mode 100644 index 0000000..25e4729 --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/Tree.j3md @@ -0,0 +1,21 @@ +MaterialDef Tree { + + MaterialParameters { + Color Diffuse (Color) : 0.42 0.26 0.10 1.0 + Float WindStrength : 0.15 + Float WindSpeed : 0.5 + Texture2D BarkMap + Boolean HasBarkMap : false + } + + Technique { + VertexShader GLSL150 : Shaders/Tree.vert + FragmentShader GLSL150 : Shaders/Tree.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + Time + } + } +} diff --git a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md new file mode 100644 index 0000000..d57e4bd --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md @@ -0,0 +1,83 @@ +MaterialDef TreeLeaf { + + MaterialParameters { + Color Diffuse (Color) : 0.18 0.60 0.10 1.0 + Float WindStrength : 0.30 + Float WindSpeed : 0.7 + Texture2D LeafMap + Boolean HasLeafMap : false + + // Vom Shadow-Renderer befüllt (PostShadow-Pass) — vollständige Liste aus PostShadow.j3md + Int BoundDrawBuffer + Int FilterMode + Boolean HardwareShadows + Texture2D ShadowMap0 + Texture2D ShadowMap1 + Texture2D ShadowMap2 + Texture2D ShadowMap3 + Texture2D ShadowMap4 + Texture2D ShadowMap5 + Float ShadowIntensity : 1.0 + Vector4 Splits + Vector2 FadeInfo + Matrix4 LightViewProjectionMatrix0 + Matrix4 LightViewProjectionMatrix1 + Matrix4 LightViewProjectionMatrix2 + Matrix4 LightViewProjectionMatrix3 + Matrix4 LightViewProjectionMatrix4 + Matrix4 LightViewProjectionMatrix5 + Vector3 LightPos + Vector3 LightDir + Float PCFEdge + Float ShadowMapSize + Boolean BackfaceShadows : false + } + + Technique { + VertexShader GLSL150 : Shaders/Tree.vert + FragmentShader GLSL150 : Shaders/TreeLeaf.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + Time + } + + RenderState { + FaceCull Off + } + } + + Technique PostShadow { + VertexShader GLSL150 : Shaders/LeafPostShadow.vert + FragmentShader GLSL150 : Shaders/LeafPostShadow.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + } + + ForcedRenderState { + Blend Modulate + FaceCull Off + DepthWrite Off + } + } + + Technique PreShadow { + VertexShader GLSL150 : Shaders/LeafPreShadow.vert + FragmentShader GLSL150 : Shaders/LeafPreShadow.frag + + WorldParameters { + WorldViewProjectionMatrix + } + + ForcedRenderState { + FaceCull Off + DepthTest On + DepthWrite On + PolyOffset 5 3 + ColorWrite Off + } + } +} diff --git a/blight-assets/src/main/resources/Shaders/Grass.frag b/blight-assets/src/main/resources/Shaders/Grass.frag new file mode 100644 index 0000000..3fbc02f --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Grass.frag @@ -0,0 +1,18 @@ +uniform vec4 m_Color; + +#ifdef HAS_COLORMAP +uniform sampler2D m_ColorMap; +#endif + +in vec2 texCoord; +out vec4 outFragColor; + +void main() { + vec4 color = m_Color; +#ifdef HAS_COLORMAP + color *= texture(m_ColorMap, texCoord); +#endif + // Alpha-Discard: transparente Pixel sofort verwerfen, Z-Buffer bleibt sauber + if (color.a < 0.5) discard; + outFragColor = color; +} diff --git a/blight-assets/src/main/resources/Shaders/Grass.vert b/blight-assets/src/main/resources/Shaders/Grass.vert new file mode 100644 index 0000000..49ba07d --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Grass.vert @@ -0,0 +1,33 @@ +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; +uniform float g_Time; + +uniform float m_WindSpeed; +uniform float m_WindStrength; + +in vec3 inPosition; +in vec2 inTexCoord; + +out vec2 texCoord; + +void main() { + vec4 pos = vec4(inPosition, 1.0); + + // Nur obere Hälfte des Halms bewegen (inTexCoord.y = 0 unten, 1 oben) + if (inTexCoord.y > 0.05) { + vec2 worldXZ = (g_WorldMatrix * pos).xz; + float t = g_Time * m_WindSpeed; + + // Zwei überlagerte Sinuswellen für organische Bewegung + float sway = sin(t * 2.1 + worldXZ.x * 0.08 + worldXZ.y * 0.06) * 0.6 + + sin(t * 1.4 - worldXZ.x * 0.05 + worldXZ.y * 0.09) * 0.4; + + // Quadratische Gewichtung: Spitze bewegt sich mehr als Basis + float bend = sway * m_WindStrength * inTexCoord.y * inTexCoord.y; + pos.x += bend; + pos.z += bend * 0.3; + } + + texCoord = inTexCoord; + gl_Position = g_WorldViewProjectionMatrix * pos; +} diff --git a/blight-assets/src/main/resources/Shaders/LeafPostShadow.frag b/blight-assets/src/main/resources/Shaders/LeafPostShadow.frag new file mode 100644 index 0000000..f87574b --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/LeafPostShadow.frag @@ -0,0 +1,20 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform sampler2D m_ShadowMap0; +uniform float m_ShadowIntensity; +uniform sampler2D m_LeafMap; +uniform bool m_HasLeafMap; + +in vec4 shadowCoord; +in vec2 texCoord; + +void main() { + // Transparente Blattbereiche empfangen keinen Schatten + if (m_HasLeafMap && texture2D(m_LeafMap, texCoord).a < 0.5) discard; + + vec3 coord = shadowCoord.xyz / shadowCoord.w; + float mapDepth = texture2D(m_ShadowMap0, coord.xy).r; + float lit = (coord.z > mapDepth + 0.001) ? (1.0 - m_ShadowIntensity) : 1.0; + + gl_FragColor = vec4(lit, lit, lit, 1.0); +} diff --git a/blight-assets/src/main/resources/Shaders/LeafPostShadow.vert b/blight-assets/src/main/resources/Shaders/LeafPostShadow.vert new file mode 100644 index 0000000..5b13bb8 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/LeafPostShadow.vert @@ -0,0 +1,18 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; +uniform mat4 m_LightViewProjectionMatrix0; + +in vec3 inPosition; +in vec2 inTexCoord; + +out vec4 shadowCoord; +out vec2 texCoord; + +void main() { + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); + vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); + shadowCoord = m_LightViewProjectionMatrix0 * worldPos; + texCoord = inTexCoord; +} diff --git a/blight-assets/src/main/resources/Shaders/LeafPreShadow.frag b/blight-assets/src/main/resources/Shaders/LeafPreShadow.frag new file mode 100644 index 0000000..59a31e7 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/LeafPreShadow.frag @@ -0,0 +1,15 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform sampler2D m_LeafMap; +uniform bool m_HasLeafMap; + +in vec2 texCoord; + +void main() { + if (m_HasLeafMap) { + vec4 tex = texture2D(m_LeafMap, texCoord); + if (tex.a < 0.5) discard; + } + // Nur Tiefe schreiben — ColorWrite ist per ForcedRenderState deaktiviert + gl_FragColor = vec4(1.0); +} diff --git a/blight-assets/src/main/resources/Shaders/LeafPreShadow.vert b/blight-assets/src/main/resources/Shaders/LeafPreShadow.vert new file mode 100644 index 0000000..8b60506 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/LeafPreShadow.vert @@ -0,0 +1,13 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform mat4 g_WorldViewProjectionMatrix; + +in vec3 inPosition; +in vec2 inTexCoord; + +out vec2 texCoord; + +void main() { + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); + texCoord = inTexCoord; +} diff --git a/blight-assets/src/main/resources/Shaders/PreShadow.frag b/blight-assets/src/main/resources/Shaders/PreShadow.frag new file mode 100644 index 0000000..48e9312 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/PreShadow.frag @@ -0,0 +1,17 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform sampler2D m_LeafMap; +uniform float m_AlphaDiscardThreshold; +varying vec2 texCoord; + +void main() { + #ifdef HAS_LEAFMAP + float alpha = texture2D(m_LeafMap, texCoord).a; + if (alpha < m_AlphaDiscardThreshold) { + discard; + } + #endif + + // Wir schreiben nur die Tiefe, die Farbe ist egal. + gl_FragColor = vec4(1.0); +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Shaders/Tree.frag b/blight-assets/src/main/resources/Shaders/Tree.frag new file mode 100644 index 0000000..4232df7 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Tree.frag @@ -0,0 +1,29 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform vec4 m_Diffuse; +uniform sampler2D m_BarkMap; +uniform bool m_HasBarkMap; + +in vec2 texCoord; +in vec3 worldNormal; + +void main() { + vec3 n = normalize(worldNormal); + + // Sun at ~45° elevation from SE — good contrast on vertical cylinders + vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4)); + vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4)); + vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7)); + + float sun = max(dot(n, sunDir), 0.0); + float fill = max(dot(n, fillDir), 0.0) * 0.22; + float rim = max(dot(n, rimDir), 0.0) * 0.14; + float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; // [0.0, 0.8] + float light = sun * 0.75 + fill + rim + sky * 0.09 + 0.05; + + vec3 baseColor = m_HasBarkMap + ? texture2D(m_BarkMap, texCoord).rgb * m_Diffuse.rgb + : m_Diffuse.rgb; + + gl_FragColor = vec4(baseColor * clamp(light, 0.0, 1.0), m_Diffuse.a); +} diff --git a/blight-assets/src/main/resources/Shaders/Tree.vert b/blight-assets/src/main/resources/Shaders/Tree.vert new file mode 100644 index 0000000..a8da904 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/Tree.vert @@ -0,0 +1,33 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + + +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; +uniform float g_Time; +uniform float m_WindStrength; +uniform float m_WindSpeed; + +in vec3 inPosition; +in vec3 inNormal; +in vec2 inTexCoord; +in vec4 inColor; // R = Wind-Gewicht (0 = Wurzel, 1 = Spitze) + +out vec2 texCoord; +out vec3 worldNormal; + +void main() { +float windW = inColor.r; +float t = g_Time * m_WindSpeed; + +// Welt-Position für orts-abhängige Phase (verhindert synchrones Schwingen) +vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); +float phase = worldPos.x * 0.08 + worldPos.z * 0.06; +float swayX = sin(t + phase) * windW * m_WindStrength; +float swayZ = cos(t * 0.73 + phase) * windW * m_WindStrength * 0.55; + +vec3 animPos = inPosition + vec3(swayX, 0.0, swayZ); + +gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0); +texCoord = inTexCoord; +worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz); +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Shaders/TreeLeaf.frag b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag new file mode 100644 index 0000000..d805691 --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag @@ -0,0 +1,27 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform vec4 m_Diffuse; +uniform sampler2D m_LeafMap; +uniform bool m_HasLeafMap; + +in vec2 texCoord; +in vec3 worldNormal; + +void main() { + vec3 baseColor; + + if (m_HasLeafMap) { + vec4 tex = texture2D(m_LeafMap, texCoord); + if (tex.a < 0.5) discard; + baseColor = tex.rgb * m_Diffuse.rgb; + } else { + // Fallback: kreisförmiger Clip + vec2 uv = texCoord * 2.0 - 1.0; + if (dot(uv, uv) > 0.95) discard; + float edge = 1.0 - dot(uv, uv); + baseColor = m_Diffuse.rgb * (0.7 + 0.3 * edge); + } + + // Leaves transmit light — no directional shading, uniform brightness + gl_FragColor = vec4(baseColor, 1.0); +} diff --git a/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg new file mode 100644 index 0000000..6277b94 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg differ diff --git a/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg new file mode 100644 index 0000000..b2525e9 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg differ diff --git a/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg new file mode 100644 index 0000000..15ed21b Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg differ diff --git a/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg new file mode 100644 index 0000000..43ee658 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg differ diff --git a/blight-game/assets/Textures/gras.png b/blight-assets/src/main/resources/Textures/gras.png similarity index 100% rename from blight-game/assets/Textures/gras.png rename to blight-assets/src/main/resources/Textures/gras.png diff --git a/blight-assets/src/main/resources/Textures/leaves/ash.png b/blight-assets/src/main/resources/Textures/leaves/ash.png new file mode 100644 index 0000000..7f7c844 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/ash.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/aspen.png b/blight-assets/src/main/resources/Textures/leaves/aspen.png new file mode 100644 index 0000000..89265a9 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/aspen.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/oak.png b/blight-assets/src/main/resources/Textures/leaves/oak.png new file mode 100644 index 0000000..061fbf6 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/oak.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/palm.png b/blight-assets/src/main/resources/Textures/leaves/palm.png new file mode 100644 index 0000000..b6db0bb Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/palm.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/palm2.png b/blight-assets/src/main/resources/Textures/leaves/palm2.png new file mode 100644 index 0000000..3ca0657 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/palm2.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/pine.png b/blight-assets/src/main/resources/Textures/leaves/pine.png new file mode 100644 index 0000000..478dca3 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/pine.png differ diff --git a/blight-common/.gitignore b/blight-common/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/blight-common/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/blight-common/build.gradle b/blight-common/build.gradle new file mode 100644 index 0000000..340ff37 --- /dev/null +++ b/blight-common/build.gradle @@ -0,0 +1,20 @@ +// Gemeinsames Datenmodell — wird von blight-editor und blight-game eingebunden. +// Selbst-ständig konfiguriert, damit es auch funktioniert wenn blight-editor +// oder blight-game standalone in Eclipse importiert werden (ohne Root-Build). +plugins { + id 'java' +} + +group = 'de.blight' +version = '0.1.0' + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +compileJava.options.encoding = 'UTF-8' + +repositories { + mavenCentral() +} diff --git a/blight-common/src/main/java/de/blight/common/MapData.java b/blight-common/src/main/java/de/blight/common/MapData.java new file mode 100644 index 0000000..76fe94d --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/MapData.java @@ -0,0 +1,68 @@ +package de.blight.common; + +/** + * Serialisierbarer Zustand einer Blight-Weltkarte. + * + * Basis-Terrain : 4097 × 4097 Vertices (= 4096 × 4096 Zellen), + * 8 Welteinheiten pro Zelle → Welt −2048 .. +2048. + * Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung. + * Splatmap : 513 × 513 Pixel (passt auf Spiel-Terrain 1:1). + * Kanäle R/G/B = Gewicht für Tex2/Tex3/Tex4; Tex1 füllt den Rest. + */ +public final class MapData { + + // ── Terrain-Konstanten ──────────────────────────────────────────────────── + + /** Vertices pro Achse des Basis-Terrains (muss 2^n + 1 sein). */ + public static final int TERRAIN_VERTS = 4097; + + // ── Upper-Layer-Konstanten ──────────────────────────────────────────────── + + /** Zellen pro Achse der oberen Schicht. */ + public static final int UPPER_CELLS = 512; + + /** Vertices pro Achse der oberen Schicht (UPPER_CELLS + 1). */ + public static final int UPPER_VERTS = 513; + + // ── Splatmap-Konstanten ──────────────────────────────────────────────────── + + /** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */ + public static final int SPLAT_SIZE = 513; + + // ── Daten ───────────────────────────────────────────────────────────────── + + /** Y-Höhe jedes Vertex im Basis-Terrain [TERRAIN_VERTS²]. */ + public final float[] terrainHeight; + + /** Y der oberen Oberfläche der Bergschicht [UPPER_VERTS²]. */ + public final float[] upperTop; + + /** Y der Höhlendecke [UPPER_VERTS²]. */ + public final float[] upperBottom; + + /** 1 = Loch (offen), 0 = massiv [UPPER_CELLS²]. */ + public final byte[] upperHole; + + /** Splatmap Rot-Kanal: Tex1-Helligkeit (Alpha.R), immer 255 [SPLAT_SIZE²]. */ + public final byte[] splatR; + + /** Splatmap Grün-Kanal: Tex2 (Fels) mix-Faktor (Alpha.G) [SPLAT_SIZE²], Bytes 0–255. */ + public final byte[] splatG; + + /** Splatmap Blau-Kanal: Tex3 (Erde) mix-Faktor (Alpha.B) [SPLAT_SIZE²], Bytes 0–255. */ + public final byte[] splatB; + + /** Gras-Dichte [SPLAT_SIZE²], Bytes 0–255 (0=kein Gras, 255=max Dichte). */ + public final byte[] grassDensity; + + public MapData() { + terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS]; + upperTop = new float[UPPER_VERTS * UPPER_VERTS]; + upperBottom = new float[UPPER_VERTS * UPPER_VERTS]; + upperHole = new byte [UPPER_CELLS * UPPER_CELLS]; + splatR = new byte [SPLAT_SIZE * SPLAT_SIZE]; + splatG = new byte [SPLAT_SIZE * SPLAT_SIZE]; + splatB = new byte [SPLAT_SIZE * SPLAT_SIZE]; + grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE]; + } +} diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java new file mode 100644 index 0000000..941c7cf --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -0,0 +1,102 @@ +package de.blight.common; + +import java.io.*; +import java.nio.*; +import java.nio.file.*; +import java.util.zip.*; + +/** + * Liest und schreibt {@link MapData} als komprimierte Binärdatei. + * + * Speicherort: {@code world/blight_map.blm} relativ zum Arbeitsverzeichnis. + * Beide Projekte setzen {@code workingDir = rootDir} im Gradle-Run-Task, + * zeigen also auf dasselbe Verzeichnis. + * + * Versionen: + * 1 – Basis-Terrain + Obere Schicht (kein Splatmap) + * 2 – wie 1 + Splatmap (R/G/B je 513×513 Bytes) + * 3 – wie 2 + Gras-Dichte (513×513 Bytes) + */ +public final class MapIO { + + private static final Path MAP_PATH = Paths.get("world", "blight_map.blm"); + + private static final int MAGIC = 0x424C4947; // "BLIG" + private static final int VERSION = 3; + + private MapIO() {} + + // ── Public API ──────────────────────────────────────────────────────────── + + public static boolean exists() { + return Files.exists(MAP_PATH); + } + + public static Path getMapPath() { + return MAP_PATH.toAbsolutePath(); + } + + public static void save(MapData data) throws IOException { + Files.createDirectories(MAP_PATH.getParent()); + try (DataOutputStream out = new DataOutputStream( + new BufferedOutputStream( + new GZIPOutputStream(Files.newOutputStream(MAP_PATH))))) { + out.writeInt(MAGIC); + out.writeInt(VERSION); + writeFloats(out, data.terrainHeight); + writeFloats(out, data.upperTop); + writeFloats(out, data.upperBottom); + out.write(data.upperHole); + // v2: splatmap + out.write(data.splatR); + out.write(data.splatG); + out.write(data.splatB); + // v3: gras-dichte + out.write(data.grassDensity); + } + } + + public static MapData load() throws IOException { + MapData data = new MapData(); + try (DataInputStream in = new DataInputStream( + new BufferedInputStream( + new GZIPInputStream(Files.newInputStream(MAP_PATH))))) { + int magic = in.readInt(); + int version = in.readInt(); + if (magic != MAGIC) throw new IOException("Ungültige Map-Datei (falscher Magic)"); + if (version < 1 || version > VERSION) + throw new IOException("Unbekannte Map-Version: " + version); + + readFloats(in, data.terrainHeight); + readFloats(in, data.upperTop); + readFloats(in, data.upperBottom); + in.readFully(data.upperHole); + + if (version >= 2) { + in.readFully(data.splatR); + in.readFully(data.splatG); + in.readFully(data.splatB); + } + if (version >= 3) { + in.readFully(data.grassDensity); + } + // version 1/2: grassDensity stays all-zeros (= kein Gras) + } + return data; + } + + // ── Hilfsmethoden ───────────────────────────────────────────────────────── + + private static void writeFloats(DataOutputStream out, float[] arr) throws IOException { + ByteBuffer buf = ByteBuffer.allocate(arr.length * Float.BYTES) + .order(ByteOrder.BIG_ENDIAN); + buf.asFloatBuffer().put(arr); + out.write(buf.array()); + } + + private static void readFloats(DataInputStream in, float[] arr) throws IOException { + byte[] bytes = new byte[arr.length * Float.BYTES]; + in.readFully(bytes); + ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(arr); + } +} diff --git a/blight-editor/.gitignore b/blight-editor/.gitignore new file mode 100644 index 0000000..abc78ba --- /dev/null +++ b/blight-editor/.gitignore @@ -0,0 +1,2 @@ +/.gradle/ +/build/ diff --git a/blight-editor/build.gradle b/blight-editor/build.gradle index 57c0856..87e06e8 100644 --- a/blight-editor/build.gradle +++ b/blight-editor/build.gradle @@ -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 + } +} diff --git a/blight-editor/gradle/wrapper/gradle-wrapper.properties b/blight-editor/gradle/wrapper/gradle-wrapper.properties index b82aa23..a034286 100644 --- a/blight-editor/gradle/wrapper/gradle-wrapper.properties +++ b/blight-editor/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/blight-editor/gradlew b/blight-editor/gradlew index 97de990..30553ae 100755 --- a/blight-editor/gradlew +++ b/blight-editor/gradlew @@ -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" "$@" diff --git a/blight-editor/gradlew.bat b/blight-editor/gradlew.bat index 16e26a1..66f1aa7 100644 --- a/blight-editor/gradlew.bat +++ b/blight-editor/gradlew.bat @@ -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 diff --git a/blight-editor/settings.gradle b/blight-editor/settings.gradle index 485407c..5f92c16 100644 --- a/blight-editor/settings.gradle +++ b/blight-editor/settings.gradle @@ -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') diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 000b4a7..0ece3bd 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -1,22 +1,42 @@ package de.blight.editor; +import de.blight.editor.tool.ChoiceToolParameter; +import de.blight.editor.tool.EditorTool; +import de.blight.editor.tool.ToolParameter; +import de.blight.editor.tree.PalmOptions; +import de.blight.editor.tree.TreeParams; +import de.blight.eztree.Billboard; +import de.blight.eztree.TreeOptions; +import de.blight.eztree.TreePresets; +import de.blight.eztree.TreeType; import javafx.application.Application; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Orientation; +import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.WritableImage; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.Dragboard; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; +import javafx.scene.input.TransferMode; import javafx.scene.layout.*; import javafx.stage.FileChooser; import javafx.stage.Stage; import java.io.File; import java.io.IOException; +import java.net.URL; import java.nio.file.*; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; public class EditorApp extends Application { @@ -24,97 +44,1119 @@ public class EditorApp extends Application { static final int VP_WIDTH = 1024; static final int VP_HEIGHT = 640; - // ── Asset-Verzeichnis ──────────────────────────────────────────────────── private static final Path ASSET_ROOT = Paths.get("editor-assets"); - private final SharedInput input = new SharedInput(); + private final SharedInput input = new SharedInput(); private WritableImage jfxImage; private ImageView viewport; private Label statusLabel; + private VBox toolPanel; + private BorderPane root; + private VBox assetPanel; + private StackPane worldViewport; + private VBox topBar; // MenuBar + aktuelle Toolbar + private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons) + private final TextField treeNameField = new TextField("Baum1"); + private ImageView treePreviewView; // aktualisiert wenn JME3 Bild neu erstellt + private Stage primaryStage; + + // Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt) + private TreeParams treeParams = TreeParams.oak(); + + // EZ-Tree-Zustand + private TreeOptions ezTreeOptions = TreePresets.oakMedium(); + private final TextField ezTreeNameField = new TextField("EzBaum1"); + + // Palmen-Generator-Zustand + private PalmOptions palmOptions = new PalmOptions(); + private final TextField palmNameField = new TextField("Palme1"); + + // Asset-Panel: Pfad-Map + DnD-Zustand + private final Map, Path> itemPaths = new HashMap<>(); + private TreeItem draggedItem = null; + + // Objekt-Werkzeug-Zustand + private Label objModelLabel; // zeigt den ausgewählten Modell-Pfad + private Label objPosLabel; // zeigt Position/Rotation + private CheckBox objSolidCB; // Solid-Flag des selektierten Objekts + private CheckBox multiPlaceCB; // Mehrfach-Platzierungs-Modus + + // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein) + private ToggleButton objPlaceBtn; + private ToggleButton objEditBtn; + + // Mesh-Primitiv-Auswahl (Platzieren-Modus) + private ToggleGroup meshToggleGroup; // Drag-Tracking für Kamerarotation (mittlere Taste) private double prevDragX, prevDragY; - // ── Asset-Tree-Items ───────────────────────────────────────────────────── + // Drag-Tracking für Gizmo im Objekt-Modus + private double objDragPrevX, objDragPrevY; + private boolean objDragging = false; + + // Edit-Tracking + private double editPressX, editPressY; + private int editPressAction; + private javafx.animation.Timeline editTimer; + + // Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein) private TreeItem modelsNode; private TreeItem texturesNode; private TreeItem audioNode; // ── JavaFX Entry-Point ─────────────────────────────────────────────────── - public static void main(String[] args) { - launch(args); - } + public static void main(String[] args) { launch(args); } @Override public void start(Stage stage) { - // WritableImage für den JME3-Frame-Export jfxImage = new WritableImage(VP_WIDTH, VP_HEIGHT); - - // JME3 starten (Hintergrund-Daemon-Thread) JmeEditorApp.launch(input, jfxImage, VP_WIDTH, VP_HEIGHT); - // UI zusammenbauen - BorderPane root = new BorderPane(); - root.setTop(buildTop()); - root.setLeft(buildAssetPanel()); - root.setCenter(buildViewport()); + assetPanel = buildAssetPanel(); + toolPanel = buildToolPanel(); + + root = new BorderPane(); + topBar = buildTop(); + root.setTop(topBar); + worldViewport = buildViewport(); + root.setLeft(assetPanel); + root.setCenter(worldViewport); + root.setRight(toolPanel); root.setBottom(buildStatusBar()); Scene scene = new Scene(root, 1280, 760); scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true)); scene.setOnKeyReleased(e -> handleKeyPress(e.getCode(), false)); + primaryStage = stage; stage.setTitle("Blight World Editor"); stage.setScene(scene); stage.setMinWidth(900); stage.setMinHeight(600); stage.setOnCloseRequest(e -> Platform.exit()); + stage.setMaximized(true); stage.show(); + + javafx.animation.Timeline statusPoller = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { + String saveMsg = input.saveStatusMsg; + if (saveMsg != null) { input.saveStatusMsg = null; setStatus(saveMsg); } + + String treeMsg = input.treeGenStatusMsg; + if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); } + + if (input.treePreviewResized) { + input.treePreviewResized = false; + if (treePreviewView != null) + treePreviewView.setImage(input.treePreviewImage); + } + + if (input.refreshAssets) { + input.refreshAssets = false; + refreshCategoryNode(modelsNode, + ".j3o", ".obj", ".fbx", ".gltf", ".glb"); + modelsNode.setExpanded(true); + } + + if (input.objectJustPlaced) { + input.objectJustPlaced = false; + if (multiPlaceCB == null || !multiPlaceCB.isSelected()) { + input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; + if (objEditBtn != null) objEditBtn.setSelected(true); + root.setRight(buildObjectEditPanel()); + } + } + + if (input.objectSelectionChanged) { + input.objectSelectionChanged = false; + updateObjectPanel(input.selectedObjectInfo); + } + }) + ); + statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE); + statusPoller.play(); + } + + // ── Modus-Wechsel ──────────────────────────────────────────────────────── + + private void switchToTreeGenerator() { + topBar.getChildren().set(1, buildTreeToolBar()); + root.setCenter(buildTreePreviewPanel()); + root.setRight(buildTreeParamsPanel()); + } + + private void switchToWorldEditor() { + topBar.getChildren().set(1, worldToolBar); + root.setCenter(worldViewport); + root.setRight(toolPanel); + } + + private void switchToEzTree() { + topBar.getChildren().set(1, buildEzTreeToolBar()); + root.setCenter(buildTreePreviewPanel()); // teilt Vorschau-Infrastruktur + root.setRight(buildEzTreeParamsPanel()); + } + + private void switchToPalm() { + topBar.getChildren().set(1, buildPalmToolBar()); + root.setCenter(buildTreePreviewPanel()); + root.setRight(buildPalmParamsPanel()); } // ── Oberer Bereich: MenuBar + ToolBar ──────────────────────────────────── private VBox buildTop() { - // Menüleiste MenuBar menuBar = new MenuBar(); + Menu fileMenu = new Menu("Datei"); MenuItem newItem = new MenuItem("Neue Karte"); MenuItem saveItem = new MenuItem("Speichern"); + saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S")); + saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); }); fileMenu.getItems().addAll(newItem, saveItem); + + Menu toolsMenu = new Menu("Werkzeuge"); + MenuItem treeGenItem = new MenuItem("Baum Generator (blight)"); + MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)"); + MenuItem palmItem = new MenuItem("Baum Generator (Palme)"); + MenuItem worldEditItem = new MenuItem("Welteneditor"); + treeGenItem.setOnAction(e -> switchToTreeGenerator()); + ezTreeItem.setOnAction(e -> switchToEzTree()); + palmItem.setOnAction(e -> switchToPalm()); + worldEditItem.setOnAction(e -> switchToWorldEditor()); + toolsMenu.getItems().addAll(treeGenItem, ezTreeItem, palmItem, worldEditItem); + Menu viewMenu = new Menu("Ansicht"); MenuItem resetCam = new MenuItem("Kamera zurücksetzen"); - resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); // noop, Kamera reset via SharedInput wäre aufwändiger + resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); viewMenu.getItems().add(resetCam); - menuBar.getMenus().addAll(fileMenu, viewMenu); - // Werkzeugleiste + menuBar.getMenus().addAll(fileMenu, toolsMenu, viewMenu); + ToolBar toolBar = new ToolBar(); - Button heightTool = new Button("▲▼ Höhe"); - heightTool.setStyle("-fx-font-weight:bold;"); - heightTool.setTooltip(new Tooltip( - "Linksklick: Terrain anheben\nRechtsklick: Terrain absenken")); + ToggleButton baseBtn = new ToggleButton("▲▼ Basis-Terrain"); + ToggleButton upperBtn = new ToggleButton("⛰ Obere Schicht"); + ToggleButton holesBtn = new ToggleButton("⬤ Höhlen/Löcher"); + ToggleButton grassBtn = new ToggleButton("🌿 Gras"); + ToggleButton textureBtn = new ToggleButton("🎨 Textur"); + objPlaceBtn = new ToggleButton("📦 Platzieren"); + objEditBtn = new ToggleButton("🔧 Bearbeiten"); + baseBtn.setStyle("-fx-font-weight:bold;"); + upperBtn.setStyle("-fx-font-weight:bold;"); + holesBtn.setStyle("-fx-font-weight:bold;"); + grassBtn.setStyle("-fx-font-weight:bold;"); + textureBtn.setStyle("-fx-font-weight:bold;"); + objPlaceBtn.setStyle("-fx-font-weight:bold;"); + objEditBtn.setStyle("-fx-font-weight:bold;"); - Separator sep1 = new Separator(Orientation.VERTICAL); + ToggleGroup layerGroup = new ToggleGroup(); + baseBtn.setToggleGroup(layerGroup); + upperBtn.setToggleGroup(layerGroup); + holesBtn.setToggleGroup(layerGroup); + grassBtn.setToggleGroup(layerGroup); + textureBtn.setToggleGroup(layerGroup); + objPlaceBtn.setToggleGroup(layerGroup); + objEditBtn.setToggleGroup(layerGroup); + baseBtn.setSelected(true); - Label brushLabel = new Label("Pinselstärke:"); - Slider brushSlider = new Slider(0.1, 1.0, 1.0); - brushSlider.setPrefWidth(100); - brushSlider.setMajorTickUnit(0.5); - brushSlider.setShowTickMarks(true); + baseBtn.setOnAction(e -> { + input.activeLayer = 0; input.activeTool = input.heightTool; + root.setRight(toolPanel); + showToolParameters(toolPanel, input.activeTool); + }); + upperBtn.setOnAction(e -> { + input.activeLayer = 1; input.activeTool = input.upperHeightTool; + root.setRight(toolPanel); + showToolParameters(toolPanel, input.activeTool); + }); + holesBtn.setOnAction(e -> { + input.activeLayer = 2; input.activeTool = input.holeTool; + root.setRight(toolPanel); + showToolParameters(toolPanel, input.activeTool); + }); + grassBtn.setOnAction(e -> { + input.activeLayer = 3; input.activeTool = input.grassTool; + root.setRight(toolPanel); + showToolParameters(toolPanel, input.activeTool); + }); + textureBtn.setOnAction(e -> { + input.activeLayer = 4; input.activeTool = input.textureTool; + root.setRight(toolPanel); + showToolParameters(toolPanel, input.activeTool); + }); + objPlaceBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_OBJECTS; + root.setRight(buildObjectPlacePanel()); + }); + objEditBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; + input.pendingModelPath = null; + root.setRight(buildObjectEditPanel()); + }); - Separator sep2 = new Separator(Orientation.VERTICAL); + CheckBox visibleCB = new CheckBox("Obere Schicht sichtbar"); + visibleCB.setSelected(true); + visibleCB.setOnAction(e -> input.upperLayerVisible = visibleCB.isSelected()); - Label hint = new Label("WASD/QE: Kamera | Mitte-Drag: Drehen | L-Klick: hoch | R-Klick: tief"); + Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief"); hint.setStyle("-fx-text-fill: #555;"); - toolBar.getItems().addAll(heightTool, sep1, brushLabel, brushSlider, sep2, hint); + toolBar.getItems().addAll(baseBtn, upperBtn, holesBtn, grassBtn, textureBtn, + new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn, + new Separator(Orientation.VERTICAL), visibleCB, + new Separator(Orientation.VERTICAL), hint); - VBox top = new VBox(menuBar, toolBar); - return top; + worldToolBar = toolBar; + return new VBox(menuBar, toolBar); } - // ── Linke Seite: Asset-Panel ───────────────────────────────────────────── + // ── Baum-Generator – Toolbar (ersetzt die Welt-Toolbar) ────────────────── + + private ToolBar buildTreeToolBar() { + Label presetLabel = new Label("Baumart:"); + presetLabel.setStyle("-fx-font-weight: bold;"); + ComboBox presetBox = new ComboBox<>(); + presetBox.getItems().addAll("Eiche", "Birke", "Kiefer", "Weide", "Busch"); + presetBox.setValue("Eiche"); + presetBox.setOnAction(e -> { + treeParams = presetFromName(presetBox.getValue()); + root.setRight(buildTreeParamsPanel()); + }); + + Label nameLabel = new Label("Export-Name:"); + nameLabel.setStyle("-fx-font-weight: bold;"); + treeNameField.setPrefWidth(130); + + Button previewBtn = new Button("▶ Vorschau"); + previewBtn.setStyle("-fx-font-weight: bold;"); + previewBtn.setOnAction(e -> { + input.treeGenQueue.offer( + new SharedInput.TreeGenRequest(treeParams.copy(), false, treeNameField.getText().trim())); + setStatus("Generiere Vorschau…"); + }); + + Button exportBtn = new Button("💾 Exportieren als .j3o"); + exportBtn.setOnAction(e -> { + input.treeGenQueue.offer( + new SharedInput.TreeGenRequest(treeParams.copy(), true, treeNameField.getText().trim())); + setStatus("Generiere und exportiere…"); + }); + + ToolBar bar = new ToolBar(); + bar.getItems().addAll( + presetLabel, presetBox, + new Separator(Orientation.VERTICAL), + nameLabel, treeNameField, + previewBtn, exportBtn + ); + return bar; + } + + // ── Baum-Generator – zentrales Vorschau-Panel (ersetzt den Welt-Viewport) ── + + private StackPane buildTreePreviewPanel() { + treePreviewView = new ImageView(input.treePreviewImage); + treePreviewView.setPreserveRatio(true); + treePreviewView.setSmooth(true); + + StackPane previewPane = new StackPane(treePreviewView); + previewPane.setStyle("-fx-background-color: #1e2433;"); + previewPane.widthProperty().addListener((o, ow, nw) -> { + treePreviewView.setFitWidth(nw.doubleValue()); + input.treePreviewW = Math.max(64, nw.intValue()); + }); + previewPane.heightProperty().addListener((o, oh, nh) -> { + treePreviewView.setFitHeight(nh.doubleValue()); + input.treePreviewH = Math.max(64, nh.intValue()); + }); + + double[] prevMX = {0}, prevMY = {0}; + previewPane.setOnMousePressed(e -> { + boolean mid = e.getButton() == MouseButton.MIDDLE; + boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); + if (mid || both) { prevMX[0] = e.getX(); prevMY[0] = e.getY(); } + }); + previewPane.setOnMouseDragged(e -> { + boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); + if (e.isMiddleButtonDown() || both) { + double dx = e.getX() - prevMX[0]; + double dy = e.getY() - prevMY[0]; + input.treePreviewRotY += (float)(dx * 0.4); + input.treePreviewRotX = (float) Math.max(5.0, Math.min(80.0, + input.treePreviewRotX - dy * 0.3)); + prevMX[0] = e.getX(); prevMY[0] = e.getY(); + } + }); + previewPane.setOnScroll(e -> { + float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f; + input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0, + input.treePreviewZoom * factor)); + }); + + return previewPane; + } + + // ── Baum-Generator – rechtes Parameter-Panel ───────────────────────────── + + private VBox buildTreeParamsPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + Label title = new Label("Baum-Parameter"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;"); + inner.getChildren().addAll(title, new Separator()); + + // Zufallssamen + inner.getChildren().add(bold("Zufallssamen:")); + Spinner seedSpinner = new Spinner<>(0, 99999, treeParams.seed); + seedSpinner.setEditable(true); + seedSpinner.setMaxWidth(Double.MAX_VALUE); + seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b); + inner.getChildren().add(seedSpinner); + + // Ast-Tiefe + inner.getChildren().add(bold("Ast-Tiefe:")); + Spinner levelSpinner = new Spinner<>(1, 4, treeParams.levels); + levelSpinner.setMaxWidth(Double.MAX_VALUE); + levelSpinner.valueProperty().addListener((o, a, b) -> treeParams.levels = b); + inner.getChildren().add(levelSpinner); + + // Gravitation + inner.getChildren().add(paramSlider("Gravitation:", -0.15, 0.20, treeParams.gravityStrength, + v -> treeParams.gravityStrength = v)); + + inner.getChildren().add(new Separator()); + + // Blätter + CheckBox leavesCheck = new CheckBox("Blätter generieren"); + leavesCheck.setSelected(treeParams.generateLeaves); + leavesCheck.setOnAction(e -> treeParams.generateLeaves = leavesCheck.isSelected()); + inner.getChildren().add(leavesCheck); + + inner.getChildren().add(paramSlider("Blattgröße:", 0.3, 3.0, treeParams.leafScale, + v -> treeParams.leafScale = v)); + + inner.getChildren().add(bold("Blattanzahl:")); + Spinner leafCountSpinner = new Spinner<>(1, 15, treeParams.leafCount); + leafCountSpinner.setMaxWidth(Double.MAX_VALUE); + leafCountSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafCount = b); + inner.getChildren().add(leafCountSpinner); + + inner.getChildren().add(bold("Ast-Verzweigungen:")); + Spinner leafBranchingsSpinner = new Spinner<>(0, 3, treeParams.leafBranchings); + leafBranchingsSpinner.setMaxWidth(Double.MAX_VALUE); + leafBranchingsSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafBranchings = b); + inner.getChildren().add(leafBranchingsSpinner); + + inner.getChildren().add(new Separator()); + + // Wind + inner.getChildren().add(bold("Wind:")); + inner.getChildren().add(paramSlider("Stamm:", 0.0, 0.5, treeParams.trunkFlexibility, + v -> treeParams.trunkFlexibility = v)); + inner.getChildren().add(paramSlider("Spitzen:", 0.2, 1.0, treeParams.branchFlexibility, + v -> treeParams.branchFlexibility = v)); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + // ── Kleine Helpers für Baum-Parameter-Panel ────────────────────────────── + + private static Label bold(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + return l; + } + + private boolean isObjectMode() { + return input.activeLayer == SharedInput.LAYER_OBJECTS + || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT; + } + + private static Label styledHint(String text) { + Label l = new Label(text); + l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;"); + return l; + } + + private static VBox paramSlider(String label, double min, double max, double init, + Consumer setter) { + Label name = new Label(label); + name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + Slider slider = new Slider(min, max, init); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit((max - min) / 2.0); + slider.setMaxWidth(Double.MAX_VALUE); + Label val = new Label(String.format("%.3f", init)); + val.setStyle("-fx-text-fill: #444; -fx-font-size: 11;"); + slider.valueProperty().addListener((o, a, b) -> { + setter.accept(b.floatValue()); + val.setText(String.format("%.3f", b.doubleValue())); + }); + return new VBox(3, name, slider, val); + } + + // ── EZ-Tree – Toolbar ──────────────────────────────────────────────────── + + private ToolBar buildEzTreeToolBar() { + Label presetLabel = new Label("Preset:"); + presetLabel.setStyle("-fx-font-weight: bold;"); + ComboBox presetBox = new ComboBox<>(); + presetBox.getItems().addAll(TreePresets.presetNames()); + presetBox.setValue("Oak Medium"); + presetBox.setOnAction(e -> { + ezTreeOptions = TreePresets.byName(presetBox.getValue()); + root.setRight(buildEzTreeParamsPanel()); + }); + + Label nameLabel = new Label("Export-Name:"); + nameLabel.setStyle("-fx-font-weight: bold;"); + ezTreeNameField.setPrefWidth(130); + + Button previewBtn = new Button("▶ Vorschau"); + previewBtn.setStyle("-fx-font-weight: bold;"); + previewBtn.setOnAction(e -> { + input.ezTreeGenQueue.offer( + new SharedInput.EzTreeGenRequest( + ezTreeOptions.copy(), false, ezTreeNameField.getText().trim())); + setStatus("EZ-Tree: generiere Vorschau…"); + }); + + Button exportBtn = new Button("💾 Export .j3o"); + exportBtn.setOnAction(e -> { + input.ezTreeGenQueue.offer( + new SharedInput.EzTreeGenRequest( + ezTreeOptions.copy(), true, ezTreeNameField.getText().trim())); + setStatus("EZ-Tree: generiere und exportiere…"); + }); + + ToolBar bar = new ToolBar(); + bar.getItems().addAll( + presetLabel, presetBox, + new Separator(Orientation.VERTICAL), + nameLabel, ezTreeNameField, + previewBtn, exportBtn + ); + return bar; + } + + // ── EZ-Tree – Parameter-Panel (alle Optionen) ──────────────────────────── + + private VBox buildEzTreeParamsPanel() { + VBox inner = new VBox(6); + inner.setPadding(new Insets(10)); + + // ── Allgemein ──────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); + + inner.getChildren().add(bold("Zufallssamen:")); + Spinner seedSp = intSpinner(0, 999999, ezTreeOptions.seed); + seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b); + inner.getChildren().add(seedSp); + + inner.getChildren().add(bold("Typ:")); + ChoiceBox typeCB = new ChoiceBox<>(); + typeCB.getItems().addAll("Laubbaum", "Immergrün"); + typeCB.setValue(ezTreeOptions.type == TreeType.EVERGREEN ? "Immergrün" : "Laubbaum"); + typeCB.setMaxWidth(Double.MAX_VALUE); + typeCB.setOnAction(e -> ezTreeOptions.type = + "Immergrün".equals(typeCB.getValue()) ? TreeType.EVERGREEN : TreeType.DECIDUOUS); + inner.getChildren().add(typeCB); + + inner.getChildren().add(bold("Ast-Ebenen:")); + Spinner levelsSp = intSpinner(1, 5, ezTreeOptions.branch.levels); + levelsSp.valueProperty().addListener((o, a, b) -> { + ezTreeOptions.branch.levels = b; + root.setRight(buildEzTreeParamsPanel()); // rebuild für neue Level-Sektionen + }); + inner.getChildren().add(levelsSp); + + // ── Rinde ──────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Rinde"), new Separator()); + + inner.getChildren().add(bold("Textur:")); + String[] barkTexNames = {"Keine", "Bark001", "Bark002", "Bark003", "Bark008"}; + String[] barkTexPaths = {null, + "Textures/bark/Bark001_Color.jpg", "Textures/bark/Bark002_Color.jpg", + "Textures/bark/Bark003_Color.jpg", "Textures/bark/Bark008_Color.jpg"}; + ChoiceBox barkTexCB = new ChoiceBox<>(); + barkTexCB.getItems().addAll(barkTexNames); + barkTexCB.setValue(pathToName(ezTreeOptions.bark.textureFile, barkTexPaths, barkTexNames)); + barkTexCB.setMaxWidth(Double.MAX_VALUE); + barkTexCB.setOnAction(e -> { + int idx = barkTexCB.getSelectionModel().getSelectedIndex(); + ezTreeOptions.bark.textureFile = barkTexPaths[idx]; + }); + inner.getChildren().add(barkTexCB); + + inner.getChildren().add(ezFloat("UV-Skala X:", 0.1, 5.0, ezTreeOptions.bark.textureScaleX, + v -> ezTreeOptions.bark.textureScaleX = v)); + inner.getChildren().add(ezFloat("UV-Skala Y:", 0.1, 5.0, ezTreeOptions.bark.textureScaleY, + v -> ezTreeOptions.bark.textureScaleY = v)); + inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.bark.r, + v -> ezTreeOptions.bark.r = v)); + inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.bark.g, + v -> ezTreeOptions.bark.g = v)); + inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.bark.b, + v -> ezTreeOptions.bark.b = v)); + + // ── Wachstumskraft ─────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Wachstumskraft"), new Separator()); + inner.getChildren().add(ezFloat("Richtung X:", -1, 1, ezTreeOptions.branch.force.direction.x, + v -> ezTreeOptions.branch.force.direction.x = v)); + inner.getChildren().add(ezFloat("Richtung Y:", -1, 1, ezTreeOptions.branch.force.direction.y, + v -> ezTreeOptions.branch.force.direction.y = v)); + inner.getChildren().add(ezFloat("Richtung Z:", -1, 1, ezTreeOptions.branch.force.direction.z, + v -> ezTreeOptions.branch.force.direction.z = v)); + inner.getChildren().add(ezFloat("Stärke:", 0, 0.2, ezTreeOptions.branch.force.strength, + v -> ezTreeOptions.branch.force.strength = v)); + + // ── Pro-Ebene-Sektionen ────────────────────────────────────────────── + int maxLevel = ezTreeOptions.branch.levels; + for (int lv = 0; lv <= maxLevel; lv++) { + buildLevelSection(inner, lv, maxLevel); + } + + // ── Blätter ────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Blätter"), new Separator()); + + inner.getChildren().add(bold("Textur:")); + String[] leafTexNames = {"Keine", "Eiche", "Esche", "Espe", "Kiefer", "Palme"}; + String[] leafTexPaths = {null, + "Textures/leaves/oak.png", "Textures/leaves/ash.png", + "Textures/leaves/aspen.png", "Textures/leaves/pine.png", + "Textures/leaves/palm.png"}; + ChoiceBox leafTexCB = new ChoiceBox<>(); + leafTexCB.getItems().addAll(leafTexNames); + leafTexCB.setValue(pathToName(ezTreeOptions.leaves.textureFile, leafTexPaths, leafTexNames)); + leafTexCB.setMaxWidth(Double.MAX_VALUE); + leafTexCB.setOnAction(e -> { + int idx = leafTexCB.getSelectionModel().getSelectedIndex(); + ezTreeOptions.leaves.textureFile = leafTexPaths[idx]; + }); + inner.getChildren().add(leafTexCB); + + inner.getChildren().add(bold("Billboard:")); + ChoiceBox billCB = new ChoiceBox<>(); + billCB.getItems().addAll("Kein", "Kreuz", "X-Drehung"); + billCB.setValue(switch (ezTreeOptions.leaves.billboard) { + case CROSS -> "Kreuz"; + case ROTATE_X -> "X-Drehung"; + default -> "Kein"; + }); + billCB.setMaxWidth(Double.MAX_VALUE); + billCB.setOnAction(e -> ezTreeOptions.leaves.billboard = switch (billCB.getValue()) { + case "Kreuz" -> Billboard.CROSS; + case "X-Drehung" -> Billboard.ROTATE_X; + default -> Billboard.NONE; + }); + inner.getChildren().add(billCB); + + inner.getChildren().add(bold("Anzahl:")); + Spinner leafCountSp = intSpinner(0, 60, ezTreeOptions.leaves.count); + leafCountSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.leaves.count = b); + inner.getChildren().add(leafCountSp); + + inner.getChildren().add(ezFloat("Start-Position:", 0, 1, ezTreeOptions.leaves.start, + v -> ezTreeOptions.leaves.start = v)); + inner.getChildren().add(ezFloat("Größe:", 0.05, 4, ezTreeOptions.leaves.size, + v -> ezTreeOptions.leaves.size = v)); + inner.getChildren().add(ezFloat("Größen-Varianz:", 0, 1, ezTreeOptions.leaves.sizeVariance, + v -> ezTreeOptions.leaves.sizeVariance = v)); + inner.getChildren().add(ezFloat("Alpha-Schwellwert:", 0, 1, ezTreeOptions.leaves.alphaTest, + v -> ezTreeOptions.leaves.alphaTest = v)); + inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.leaves.r, + v -> ezTreeOptions.leaves.r = v)); + inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.leaves.g, + v -> ezTreeOptions.leaves.g = v)); + inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.leaves.b, + v -> ezTreeOptions.leaves.b = v)); + + // ── Spalier ────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Spalier (Trellis)"), new Separator()); + + CheckBox trellisEnCB = new CheckBox("Aktiviert"); + trellisEnCB.setSelected(ezTreeOptions.trellis.enabled); + trellisEnCB.setOnAction(e -> ezTreeOptions.trellis.enabled = trellisEnCB.isSelected()); + inner.getChildren().add(trellisEnCB); + + inner.getChildren().add(bold("Streben:")); + Spinner trellisSecSp = intSpinner(3, 12, ezTreeOptions.trellis.sections); + trellisSecSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.sections = b); + inner.getChildren().add(trellisSecSp); + + inner.getChildren().add(ezFloat("Radius:", 0.1, 8, ezTreeOptions.trellis.radius, + v -> ezTreeOptions.trellis.radius = v)); + inner.getChildren().add(ezFloat("Höhe:", 0.5, 15, ezTreeOptions.trellis.length, + v -> ezTreeOptions.trellis.length = v)); + inner.getChildren().add(ezFloat("Streben-Radius:", 0.01, 0.3, ezTreeOptions.trellis.memberRadius, + v -> ezTreeOptions.trellis.memberRadius = v)); + + inner.getChildren().add(bold("Querstreben:")); + Spinner crossSp = intSpinner(1, 8, ezTreeOptions.trellis.crossMembers); + crossSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.crossMembers = b); + inner.getChildren().add(crossSp); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + // ── Palmen-Generator – Toolbar ─────────────────────────────────────────── + + private ToolBar buildPalmToolBar() { + Label nameLabel = new Label("Export-Name:"); + nameLabel.setStyle("-fx-font-weight: bold;"); + palmNameField.setPrefWidth(130); + + Button previewBtn = new Button("▶ Vorschau"); + previewBtn.setStyle("-fx-font-weight: bold;"); + previewBtn.setOnAction(e -> { + input.palmGenQueue.offer( + new SharedInput.PalmGenRequest( + palmOptions.copy(), false, palmNameField.getText().trim())); + setStatus("Palme: generiere Vorschau…"); + }); + + Button exportBtn = new Button("💾 Export .j3o"); + exportBtn.setOnAction(e -> { + input.palmGenQueue.offer( + new SharedInput.PalmGenRequest( + palmOptions.copy(), true, palmNameField.getText().trim())); + setStatus("Palme: generiere und exportiere…"); + }); + + ToolBar bar = new ToolBar(); + bar.getItems().addAll( + nameLabel, palmNameField, + previewBtn, exportBtn + ); + return bar; + } + + // ── Palmen-Generator – Parameter-Panel ────────────────────────────────── + + private VBox buildPalmParamsPanel() { + VBox inner = new VBox(6); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); + inner.getChildren().add(bold("Zufallssamen:")); + Spinner seedSp = intSpinner(0, 999999, palmOptions.seed); + seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b); + inner.getChildren().add(seedSp); + + inner.getChildren().addAll(sectionTitle("Stamm"), new Separator()); + inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight, + v -> palmOptions.trunkHeight = v)); + inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom, + v -> palmOptions.trunkRadiusBottom = v)); + inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop, + v -> palmOptions.trunkRadiusTop = v)); + + inner.getChildren().addAll(sectionTitle("Wedel"), new Separator()); + inner.getChildren().add(bold("Anzahl:")); + Spinner frondCountSp = intSpinner(3, 20, palmOptions.frondCount); + frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b); + inner.getChildren().add(frondCountSp); + inner.getChildren().add(ezFloat("Winkel min (° von oben):", 60, 95, palmOptions.frondAngleMin, + v -> palmOptions.frondAngleMin = v)); + inner.getChildren().add(ezFloat("Winkel max (° von oben):", 85, 130, palmOptions.frondAngleMax, + v -> palmOptions.frondAngleMax = v)); + inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength, + v -> palmOptions.frondLength = v)); + + inner.getChildren().addAll(sectionTitle("Blätter (Fiederblättchen)"), new Separator()); + inner.getChildren().add(bold("Paare pro Wedel:")); + Spinner pairsSp = intSpinner(3, 16, palmOptions.frondLeafletPairs); + pairsSp.valueProperty().addListener((o, a, b) -> palmOptions.frondLeafletPairs = b); + inner.getChildren().add(pairsSp); + inner.getChildren().add(ezFloat("Wedel-Breite:", 0.2, 4.0, palmOptions.frondWidth, + v -> palmOptions.frondWidth = v)); + + inner.getChildren().addAll(sectionTitle("Farben"), new Separator()); + inner.getChildren().add(ezFloat("Stamm R:", 0, 1, palmOptions.barkR, v -> palmOptions.barkR = v)); + inner.getChildren().add(ezFloat("Stamm G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v)); + inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = v)); + inner.getChildren().add(ezFloat("Blatt R:", 0, 1, palmOptions.leafR, v -> palmOptions.leafR = v)); + inner.getChildren().add(ezFloat("Blatt G:", 0, 1, palmOptions.leafG, v -> palmOptions.leafG = v)); + inner.getChildren().add(ezFloat("Blatt B:", 0, 1, palmOptions.leafB, v -> palmOptions.leafB = v)); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + private void buildLevelSection(VBox container, int lv, int maxLevel) { + String title = lv == 0 ? "Stamm (Ebene 0)" : "Ast-Ebene " + lv; + container.getChildren().addAll(sectionTitle(title), new Separator()); + + var bo = ezTreeOptions.branch; + + // Winkel + Start nur ab Ebene 1 (da Kind-Eigenschaften) + if (lv > 0) { + container.getChildren().add(ezFloat("Winkel (°):", 0, 90, + bo.getAngle(lv), v -> bo.angle.put(lv, v))); + container.getChildren().add(ezFloat("Start-Position:", 0, 1, + bo.getStart(lv), v -> bo.start.put(lv, v))); + } + + // Kinder nur wenn noch weitere Ebenen folgen + if (lv < maxLevel) { + container.getChildren().add(bold("Kinder:")); + Spinner childSp = intSpinner(0, 20, bo.getChildren(lv)); + childSp.valueProperty().addListener((o, a, b) -> bo.children.put(lv, b)); + container.getChildren().add(childSp); + } + + container.getChildren().add(ezFloat("Gnarliness:", 0, 1.5, + bo.getGnarliness(lv), v -> bo.gnarliness.put(lv, v))); + + String lenLabel = lv == 0 ? "Länge (absolut):" : "Länge (Faktor):"; + double lenMax = lv == 0 ? 60 : 2; + container.getChildren().add(ezFloat(lenLabel, 0.1, lenMax, + bo.getLength(lv), v -> bo.length.put(lv, v))); + + String radLabel = lv == 0 ? "Radius (absolut):" : "Radius (Faktor):"; + double radMax = lv == 0 ? 3 : 1; + container.getChildren().add(ezFloat(radLabel, 0.01, radMax, + bo.getRadius(lv), v -> bo.radius.put(lv, v))); + + container.getChildren().add(bold("Sektionen:")); + Spinner secSp = intSpinner(2, 24, bo.getSections(lv)); + secSp.valueProperty().addListener((o, a, b) -> bo.sections.put(lv, b)); + container.getChildren().add(secSp); + + container.getChildren().add(bold("Segmente (Polygon):")); + Spinner segSp = intSpinner(3, 12, bo.getSegments(lv)); + segSp.valueProperty().addListener((o, a, b) -> bo.segments.put(lv, b)); + container.getChildren().add(segSp); + + container.getChildren().add(ezFloat("Verjüngung:", 0, 1, + bo.getTaper(lv), v -> bo.taper.put(lv, v))); + container.getChildren().add(ezFloat("Drall (°/Sektion):", -180, 180, + bo.getTwist(lv), v -> bo.twist.put(lv, v))); + } + + // ── EZ-Tree UI-Helpers ─────────────────────────────────────────────────── + + private static Label sectionTitle(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;"); + VBox.setMargin(l, new Insets(8, 0, 0, 0)); + return l; + } + + private static Spinner intSpinner(int min, int max, int init) { + Spinner sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init))); + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + return sp; + } + + private static VBox ezFloat(String label, double min, double max, double init, + java.util.function.Consumer setter) { + Label name = new Label(label); + name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init))); + slider.setShowTickMarks(false); + slider.setMaxWidth(Double.MAX_VALUE); + Label val = new Label(String.format("%.3f", init)); + val.setStyle("-fx-text-fill: #444; -fx-font-size: 11;"); + slider.valueProperty().addListener((o, a, b) -> { + setter.accept(b.floatValue()); + val.setText(String.format("%.3f", b.doubleValue())); + }); + HBox row = new HBox(6, slider, val); + HBox.setHgrow(slider, Priority.ALWAYS); + return new VBox(2, name, row); + } + + private static String pathToName(String path, String[] paths, String[] names) { + if (path == null) return names[0]; + for (int i = 0; i < paths.length; i++) { + if (path.equals(paths[i])) return names[i]; + } + return names[0]; + } + + private static TreeParams presetFromName(String name) { + return switch (name) { + case "Birke" -> TreeParams.birch(); + case "Kiefer" -> TreeParams.pine(); + case "Weide" -> TreeParams.willow(); + case "Busch" -> TreeParams.bush(); + default -> TreeParams.oak(); + }; + } + + // ── Objekt-Werkzeug – Platzieren-Panel ────────────────────────────────── + + private VBox buildObjectPlacePanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + // ── Modell aus Asset-Baum ───────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Modell"), new Separator()); + objModelLabel = new Label(input.pendingModelPath != null + && !input.pendingModelPath.startsWith("@") + ? input.pendingModelPath : "(Doppelklick im Asset-Baum)"); + objModelLabel.setWrapText(true); + objModelLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); + inner.getChildren().add(objModelLabel); + inner.getChildren().add(styledHint("Doppelklick auf Modell links → auswählen")); + inner.getChildren().add(styledHint("Linksklick ins Terrain → platzieren")); + + // ── Platziermodus ───────────────────────────────────────────────────── + boolean prevMulti = multiPlaceCB != null && multiPlaceCB.isSelected(); + multiPlaceCB = new CheckBox("Mehrfach platzieren"); + multiPlaceCB.setSelected(prevMulti); + multiPlaceCB.setStyle("-fx-text-fill: #111111;"); + multiPlaceCB.setTooltip(new Tooltip( + "Aktiv: Objekte nacheinander platzieren.\n" + + "Inaktiv: Nach dem Platzieren automatisch in den Bearbeiten-Modus wechseln.")); + inner.getChildren().add(multiPlaceCB); + + // ── Meshes (Primitive) ──────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Meshes"), new Separator()); + + meshToggleGroup = new ToggleGroup(); + String[][] shapes = { + {"Box", "@box"}, + {"Kugel", "@sphere"}, + {"Zylinder", "@cylinder"}, + {"Ebene", "@plane"}, + }; + + // 2-spaltige Gitteranordnung + javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); + grid.setHgap(6); + grid.setVgap(6); + grid.setMaxWidth(Double.MAX_VALUE); + + for (int i = 0; i < shapes.length; i++) { + String label = shapes[i][0]; + String marker = shapes[i][1]; + ToggleButton btn = new ToggleButton(label); + btn.setToggleGroup(meshToggleGroup); + btn.setMaxWidth(Double.MAX_VALUE); + btn.setStyle("-fx-font-size: 12;"); + btn.setOnAction(e -> { + if (btn.isSelected()) { + input.pendingModelPath = marker; + if (objModelLabel != null) + objModelLabel.setText("(Doppelklick im Asset-Baum)"); + setStatus("Mesh: " + label + " | Linksklick ins Terrain zum Platzieren"); + } else { + input.pendingModelPath = null; + } + }); + // Vorauswahl wiederherstellen + if (marker.equals(input.pendingModelPath)) btn.setSelected(true); + grid.add(btn, i % 2, i / 2); + javafx.scene.layout.GridPane.setHgrow(btn, Priority.ALWAYS); + } + inner.getChildren().add(grid); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + // ── Objekt-Werkzeug – Bearbeiten-Panel ────────────────────────────────── + + private VBox buildObjectEditPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll(sectionTitle("Gewähltes Objekt"), new Separator()); + + objSolidCB = new CheckBox("Solid (Charakter-Kollision)"); + objSolidCB.setDisable(true); + objSolidCB.setOnAction(e -> input.pendingSolidChange = objSolidCB.isSelected()); + inner.getChildren().add(objSolidCB); + + objPosLabel = new Label("Position: –"); + objPosLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + objPosLabel.setWrapText(true); + inner.getChildren().add(objPosLabel); + + inner.getChildren().add(new Separator()); + inner.getChildren().add(styledHint("Linksklick auf Objekt → auswählen")); + inner.getChildren().add(styledHint("Pfeile ziehen → X/Y/Z bewegen")); + inner.getChildren().add(styledHint("Ring ziehen → Y-Achse drehen")); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + private void updateObjectPanel(String info) { + if (objSolidCB == null || objPosLabel == null) return; + if (info == null) { + objSolidCB.setDisable(true); + objSolidCB.setSelected(false); + objPosLabel.setText("Position: –"); + return; + } + // info format: "modelPath|solid|x|y|z|rotY|scale" + String[] parts = info.split("\\|", 7); + if (parts.length < 7) return; + objSolidCB.setDisable(false); + objSolidCB.setSelected(Boolean.parseBoolean(parts[1])); + try { + float x = Float.parseFloat(parts[2]); + float y = Float.parseFloat(parts[3]); + float z = Float.parseFloat(parts[4]); + float rotY = (float) Math.toDegrees(Float.parseFloat(parts[5])); + objPosLabel.setText(String.format( + "X: %.1f Y: %.1f Z: %.1f%nRot Y: %.1f°", x, y, z, rotY)); + } catch (NumberFormatException ignored) {} + } + + // ── Rechte Seite: Tool-Parameter-Panel (Welteneditor) ──────────────────── + + private VBox buildToolPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + showToolParameters(panel, input.activeTool); + return panel; + } + + private void showToolParameters(VBox panel, EditorTool tool) { + panel.getChildren().clear(); + + Label toolTitle = new Label(tool.getName()); + toolTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;"); + panel.getChildren().addAll(toolTitle, new Separator()); + + for (ChoiceToolParameter param : tool.getChoiceParameters()) { + Label nameLabel = new Label(param.getName()); + nameLabel.setStyle("-fx-text-fill: #111111;"); + + if (param.getImagePaths() != null) { + String[] paths = param.getImagePaths(); + String[] labels = param.getChoices(); + ToggleGroup tg = new ToggleGroup(); + ToggleButton[] buttons = new ToggleButton[paths.length]; + HBox row = new HBox(3); + + for (int j = 0; j < paths.length; j++) { + final int index = j; + ToggleButton btn = new ToggleButton(); + btn.setToggleGroup(tg); + btn.setTooltip(new Tooltip(labels[j])); + btn.setMinSize(50, 50); + btn.setMaxSize(50, 50); + + String imgUrlStr = null; + URL resUrl = EditorApp.class.getResource("/" + paths[j]); + if (resUrl != null) { + imgUrlStr = resUrl.toString(); + } else { + File imgFile = new File(paths[j]).getAbsoluteFile(); + if (imgFile.exists()) imgUrlStr = imgFile.toURI().toString(); + } + if (imgUrlStr != null) { + Image img = new Image(imgUrlStr, 38, 38, true, true); + if (!img.isError()) btn.setGraphic(new ImageView(img)); + else btn.setText(labels[j].substring(0, 2)); + } else { + btn.setText(labels[j].substring(0, 2)); + } + + boolean initially = (j == param.getSelectedIndex()); + btn.setSelected(initially); + applyModeButtonStyle(btn, initially); + btn.selectedProperty().addListener((obs, was, isNow) -> { + applyModeButtonStyle(btn, isNow); + if (isNow) param.setSelectedIndex(index); + }); + + buttons[j] = btn; + row.getChildren().add(btn); + } + + tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { + if (newT == null) tg.selectToggle(oldT); + }); + panel.getChildren().addAll(nameLabel, row); + } else { + ChoiceBox choiceBox = new ChoiceBox<>(); + choiceBox.getItems().addAll(param.getChoices()); + choiceBox.getSelectionModel().select(param.getSelectedIndex()); + choiceBox.setMaxWidth(Double.MAX_VALUE); + choiceBox.getSelectionModel().selectedIndexProperty().addListener( + (obs, oldV, newV) -> param.setSelectedIndex(newV.intValue())); + panel.getChildren().addAll(nameLabel, choiceBox); + } + } + + for (ToolParameter param : tool.getParameters()) { + Label name = new Label(param.getName()); + name.setStyle("-fx-text-fill: #111111;"); + + Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue()); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2); + slider.setMaxWidth(Double.MAX_VALUE); + + Label valueLabel = new Label(String.format("%.3f", param.getValue())); + valueLabel.setStyle("-fx-text-fill: #444; -fx-font-size: 11;"); + + slider.valueProperty().addListener((obs, oldV, newV) -> { + param.setValue(newV.doubleValue()); + valueLabel.setText(String.format("%.3f", newV.doubleValue())); + }); + + panel.getChildren().add(new VBox(3, name, slider, valueLabel)); + } + } + + // ── Linke Seite: Asset-Panel (Welteneditor) ────────────────────────────── private VBox buildAssetPanel() { VBox panel = new VBox(6); @@ -125,30 +1167,54 @@ public class EditorApp extends Application { Label title = new Label("Assets"); title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - // Baum - TreeItem root = new TreeItem<>("Projekt"); - root.setExpanded(true); + TreeItem assetRoot = new TreeItem<>("Projekt"); + assetRoot.setExpanded(true); modelsNode = new TreeItem<>("Models"); texturesNode = new TreeItem<>("Texturen"); audioNode = new TreeItem<>("Audio"); - root.getChildren().addAll(modelsNode, texturesNode, audioNode); + assetRoot.getChildren().addAll(modelsNode, texturesNode, audioNode); - // Bestehende Assets laden - loadAssetsInto(modelsNode, "models", ".j3o", ".obj", ".fbx", ".gltf", ".glb"); - loadAssetsInto(texturesNode, "textures", ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds"); - loadAssetsInto(audioNode, "audio", ".ogg", ".wav", ".mp3"); + itemPaths.put(modelsNode, ASSET_ROOT.resolve("models")); + itemPaths.put(texturesNode, ASSET_ROOT.resolve("textures")); + itemPaths.put(audioNode, ASSET_ROOT.resolve("audio")); - TreeView tree = new TreeView<>(root); + loadAssetsRecursive(modelsNode, ASSET_ROOT.resolve("models"), + ".j3o", ".obj", ".fbx", ".gltf", ".glb"); + loadAssetsRecursive(texturesNode, ASSET_ROOT.resolve("textures"), + ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds"); + loadAssetsRecursive(audioNode, ASSET_ROOT.resolve("audio"), + ".ogg", ".wav", ".mp3"); + + TreeView tree = new TreeView<>(assetRoot); tree.setShowRoot(false); VBox.setVgrow(tree, Priority.ALWAYS); + tree.setCellFactory(tv -> buildAssetCell()); - // Kontextmenü im Baum (Datei öffnen im Dateimanager) - ContextMenu ctx = new ContextMenu(); - MenuItem showItem = new MenuItem("Im Dateisystem anzeigen"); - ctx.getItems().add(showItem); - tree.setContextMenu(ctx); + // F2 → Umbenennen + tree.setOnKeyPressed(e -> { + if (e.getCode() != KeyCode.F2) return; + TreeItem sel = tree.getSelectionModel().getSelectedItem(); + if (sel == null || sel == modelsNode || sel == texturesNode || sel == audioNode) return; + renameAsset(sel); + e.consume(); + }); + + // Doppelklick auf ein Modell → als aktives Objekt-Platzierungs-Modell wählen + tree.setOnMouseClicked(e -> { + if (e.getClickCount() != 2 || input.activeLayer != SharedInput.LAYER_OBJECTS) return; + TreeItem sel = tree.getSelectionModel().getSelectedItem(); + if (sel == null || isAssetFolder(sel)) return; + if (getCategoryRoot(sel) != modelsNode) return; + Path p = itemPaths.get(sel); + if (p == null) return; + String path = ASSET_ROOT.relativize(p).toString().replace('\\', '/'); + input.pendingModelPath = path; + // Mesh-Primitiv-Auswahl aufheben + if (meshToggleGroup != null) meshToggleGroup.selectToggle(null); + if (objModelLabel != null) objModelLabel.setText(path); + setStatus("Objekt-Modell: " + path + " | Linksklick ins Terrain zum Platzieren"); + }); - // Import-Button Button importBtn = new Button("⊕ Import…"); importBtn.setMaxWidth(Double.MAX_VALUE); importBtn.setOnAction(e -> handleImport(tree.getScene().getWindow())); @@ -157,19 +1223,324 @@ public class EditorApp extends Application { return panel; } - private void loadAssetsInto(TreeItem parent, String subDir, String... exts) { - Path dir = ASSET_ROOT.resolve(subDir); + // ── Asset-Tree-Cell (DnD + Kontextmenü) ────────────────────────────────── + + private TreeCell buildAssetCell() { + TreeCell cell = new TreeCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { setText(null); setStyle(""); return; } + setText(item); + setCellStyle(this, false); + } + }; + + // ── Drag-Quelle: nur Dateien (keine Ordner) ────────────────────────── + cell.setOnDragDetected(e -> { + TreeItem item = cell.getTreeItem(); + if (item == null || isAssetFolder(item)) return; + draggedItem = item; + Dragboard db = cell.startDragAndDrop(TransferMode.MOVE); + ClipboardContent cc = new ClipboardContent(); + cc.putString(item.getValue()); + db.setContent(cc); + e.consume(); + }); + + // ── Drag-Ziel: nur Ordner der gleichen Kategorie ───────────────────── + cell.setOnDragOver(e -> { + if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem())) + e.acceptTransferModes(TransferMode.MOVE); + e.consume(); + }); + + cell.setOnDragEntered(e -> { + if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem())) + setCellStyle(cell, true); + e.consume(); + }); + + cell.setOnDragExited(e -> { setCellStyle(cell, false); e.consume(); }); + + cell.setOnDragDropped(e -> { + boolean ok = false; + TreeItem target = cell.getTreeItem(); + if (draggedItem != null && target != null && isValidDropTarget(target)) { + Path src = itemPaths.get(draggedItem); + Path destDir = itemPaths.get(target); + Path dest = destDir.resolve(src.getFileName()); + if (!src.equals(dest)) { + if (Files.exists(dest)) { + setStatus("Fehler: Datei mit diesem Namen existiert bereits im Zielordner."); + } else { + try { + Files.move(src, dest); + draggedItem.getParent().getChildren().remove(draggedItem); + itemPaths.remove(draggedItem); + TreeItem moved = new TreeItem<>(src.getFileName().toString()); + itemPaths.put(moved, dest); + target.getChildren().add(moved); + target.setExpanded(true); + setStatus("Verschoben: " + src.getFileName()); + ok = true; + } catch (IOException ex) { + setStatus("Verschieben fehlgeschlagen: " + ex.getMessage()); + } + } + } + } + draggedItem = null; + e.setDropCompleted(ok); + e.consume(); + }); + + cell.setOnDragDone(e -> { draggedItem = null; e.consume(); }); + + // ── Kontextmenü ────────────────────────────────────────────────────── + cell.setOnContextMenuRequested(e -> { + TreeItem item = cell.getTreeItem(); + if (item == null) return; + ContextMenu ctx = new ContextMenu(); + + if (isAssetFolder(item)) { + MenuItem newSub = new MenuItem("📁 Neuer Unterordner…"); + newSub.setOnAction(ev -> createSubfolder(item)); + ctx.getItems().add(newSub); + + boolean isCatRoot = (item == modelsNode || item == texturesNode || item == audioNode); + Path dir = itemPaths.get(item); + + if (!isCatRoot && dir != null) { + MenuItem rename = new MenuItem("✏ Umbenennen…"); + rename.setOnAction(ev -> renameAsset(item)); + ctx.getItems().add(rename); + + // Ordner löschen nur wenn leer + boolean empty; + try (var s = Files.list(dir)) { empty = s.findFirst().isEmpty(); } + catch (IOException ex) { empty = false; } + MenuItem del = new MenuItem("🗑 Ordner löschen"); + del.setDisable(!empty); + if (!empty) del.setStyle("-fx-text-fill: grey;"); + del.setOnAction(ev -> { + try { + Files.deleteIfExists(dir); + item.getParent().getChildren().remove(item); + itemPaths.remove(item); + setStatus("Ordner gelöscht: " + dir.getFileName()); + } catch (IOException ex) { + setStatus("Fehler: " + ex.getMessage()); + } + }); + ctx.getItems().addAll(new SeparatorMenuItem(), del); + } + } else { + // Datei: Umbenennen + Im Dateisystem anzeigen + Löschen + Path p = itemPaths.get(item); + if (p != null) { + MenuItem rename = new MenuItem("✏ Umbenennen…"); + rename.setOnAction(ev -> renameAsset(item)); + MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); + reveal.setOnAction(ev -> { + try { Runtime.getRuntime().exec( + new String[]{"xdg-open", p.getParent().toString()}); + } catch (IOException ignored) {} + }); + MenuItem del = new MenuItem("🗑 Löschen"); + del.setStyle("-fx-text-fill: #c0392b;"); + del.setOnAction(ev -> { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + "Datei wirklich löschen?\n" + p.getFileName(), + ButtonType.OK, ButtonType.CANCEL); + confirm.setHeaderText(null); + confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { + try { + Files.deleteIfExists(p); + item.getParent().getChildren().remove(item); + itemPaths.remove(item); + setStatus("Gelöscht: " + p.getFileName()); + } catch (IOException ex) { + setStatus("Fehler: " + ex.getMessage()); + } + }); + }); + ctx.getItems().addAll(rename, reveal, new SeparatorMenuItem(), del); + } + } + + if (!ctx.getItems().isEmpty()) + ctx.show(cell, e.getScreenX(), e.getScreenY()); + e.consume(); + }); + + return cell; + } + + private void setCellStyle(TreeCell cell, boolean highlighted) { + String base = isAssetFolder(cell.getTreeItem()) ? "-fx-font-weight:bold;" : ""; + cell.setStyle(highlighted ? base + "-fx-background-color:#cce5ff;" : base); + } + + // ── Asset-Baum Hilfsmethoden ────────────────────────────────────────────── + + /** Gibt true zurück wenn das Item ein Verzeichnis ist (inkl. Kategoriewurzeln). */ + private boolean isAssetFolder(TreeItem item) { + if (item == null) return false; + Path p = itemPaths.get(item); + return p != null && Files.isDirectory(p); + } + + /** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode). */ + private TreeItem getCategoryRoot(TreeItem item) { + for (TreeItem cur = item; cur != null; cur = cur.getParent()) + if (cur == modelsNode || cur == texturesNode || cur == audioNode) return cur; + return null; + } + + /** Gibt true zurück wenn draggedItem auf dieses target verschoben werden darf. */ + private boolean isValidDropTarget(TreeItem target) { + if (draggedItem == null || target == null) return false; + if (!isAssetFolder(target)) return false; + if (target == draggedItem.getParent()) return false; // schon dort + TreeItem dragCat = getCategoryRoot(draggedItem); + TreeItem dropCat = getCategoryRoot(target); + return dragCat != null && dragCat == dropCat; + } + + /** Erstellt einen neuen Unterordner unter parentItem (Dialog + Filesystem). */ + private void createSubfolder(TreeItem parentItem) { + Path parentPath = itemPaths.get(parentItem); + if (parentPath == null) return; + TextInputDialog dlg = new TextInputDialog("Neuer Ordner"); + dlg.setTitle("Ordner erstellen"); + dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:"); + dlg.setContentText("Name:"); + dlg.showAndWait().ifPresent(raw -> { + String name = raw.trim(); + if (name.isEmpty()) return; + Path newDir = parentPath.resolve(name); + try { + Files.createDirectories(newDir); + TreeItem newItem = new TreeItem<>(name); + itemPaths.put(newItem, newDir); + parentItem.getChildren().add(0, newItem); // Ordner an erster Stelle + parentItem.setExpanded(true); + setStatus("Ordner erstellt: " + name); + } catch (IOException ex) { + setStatus("Fehler beim Erstellen: " + ex.getMessage()); + } + }); + } + + /** Benennt eine Datei oder einen Ordner um (Filesystem + TreeItem + itemPaths). */ + private void renameAsset(TreeItem item) { + Path oldPath = itemPaths.get(item); + if (oldPath == null) return; + + boolean isFile = !Files.isDirectory(oldPath); + String oldFileName = oldPath.getFileName().toString(); + + // For files: extract extension and pre-fill dialog with base name only + String ext = ""; + String baseName = oldFileName; + if (isFile) { + int dot = oldFileName.lastIndexOf('.'); + if (dot > 0) { + ext = oldFileName.substring(dot); // e.g. ".j3o" + baseName = oldFileName.substring(0, dot); + } + } + + String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)"; + TextInputDialog dlg = new TextInputDialog(baseName); + dlg.setTitle("Umbenennen"); + dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt); + dlg.setContentText("Neuer Name:"); + + final String fixedExt = ext; + dlg.showAndWait().ifPresent(raw -> { + String newBase = raw.trim(); + if (newBase.isEmpty()) return; + // Append original extension for files + String newName = isFile ? newBase + fixedExt : newBase; + if (newName.equals(oldFileName)) return; + if (newName.contains("/") || newName.contains("\\")) { + setStatus("Ungültiger Name: keine Pfadtrennzeichen erlaubt."); + return; + } + Path newPath = oldPath.getParent().resolve(newName); + if (Files.exists(newPath)) { + setStatus("Fehler: " + newName + " existiert bereits."); + return; + } + try { + Files.move(oldPath, newPath); + itemPaths.put(item, newPath); + // Bei Ordnern alle Kind-Pfade anpassen + if (Files.isDirectory(newPath)) { + updateDescendantPaths(item, oldPath, newPath); + } + item.setValue(newName); + setStatus("Umbenannt: " + oldFileName + " → " + newName); + } catch (IOException ex) { + setStatus("Umbenennen fehlgeschlagen: " + ex.getMessage()); + } + }); + } + + /** Aktualisiert itemPaths aller Kinder nach einer Ordner-Umbenennung. */ + private void updateDescendantPaths(TreeItem parent, Path oldBase, Path newBase) { + for (TreeItem child : parent.getChildren()) { + Path p = itemPaths.get(child); + if (p != null) itemPaths.put(child, newBase.resolve(oldBase.relativize(p))); + updateDescendantPaths(child, oldBase, newBase); + } + } + + /** Lädt Assets rekursiv: Ordner zuerst (alphabetisch), dann Dateien. */ + private void loadAssetsRecursive(TreeItem parent, Path dir, String... exts) { if (!Files.exists(dir)) return; try (var stream = Files.list(dir)) { - stream.filter(p -> { - String name = p.getFileName().toString().toLowerCase(); - for (String ext : exts) if (name.endsWith(ext)) return true; - return false; - }).map(p -> new TreeItem<>(p.getFileName().toString())) - .forEach(parent.getChildren()::add); + List paths = stream + .sorted(Comparator.comparing((Path p) -> Files.isDirectory(p) ? 0 : 1) + .thenComparing(p -> p.getFileName().toString().toLowerCase())) + .toList(); + for (Path p : paths) { + if (Files.isDirectory(p)) { + TreeItem sub = new TreeItem<>(p.getFileName().toString()); + itemPaths.put(sub, p); + parent.getChildren().add(sub); + loadAssetsRecursive(sub, p, exts); + } else { + String lo = p.getFileName().toString().toLowerCase(); + for (String ext : exts) { + if (lo.endsWith(ext)) { + TreeItem file = new TreeItem<>(p.getFileName().toString()); + itemPaths.put(file, p); + parent.getChildren().add(file); + break; + } + } + } + } } catch (IOException ignored) {} } + /** Baut den Teilbaum einer Kategorie komplett neu auf (nach Export/Import). */ + private void refreshCategoryNode(TreeItem catNode, String... exts) { + clearItemPathsFor(catNode); + catNode.getChildren().clear(); + Path dir = itemPaths.get(catNode); + if (dir != null) loadAssetsRecursive(catNode, dir, exts); + } + + private void clearItemPathsFor(TreeItem item) { + for (TreeItem child : item.getChildren()) clearItemPathsFor(child); + if (item != modelsNode && item != texturesNode && item != audioNode) + itemPaths.remove(item); + } + private void handleImport(javafx.stage.Window owner) { FileChooser fc = new FileChooser(); fc.setTitle("Assets importieren"); @@ -186,24 +1557,36 @@ public class EditorApp extends Application { if (files == null) return; for (File file : files) { - String name = file.getName().toLowerCase(); - String subDir; - TreeItem parent; - if (name.matches(".*\\.(j3o|obj|fbx|gltf|glb)")) { - subDir = "models"; parent = modelsNode; - } else if (name.matches(".*\\.(ogg|wav|mp3)")) { - subDir = "audio"; parent = audioNode; - } else { - subDir = "textures"; parent = texturesNode; - } + String name = file.getName().toLowerCase(); + boolean isNativeModel = name.matches(".*\\.(obj|fbx|gltf|glb)"); + boolean isJ3o = name.endsWith(".j3o"); + boolean isAudio = name.matches(".*\\.(ogg|wav|mp3)"); + boolean isModel = isNativeModel || isJ3o; + + String subDir = isModel ? "models" : isAudio ? "audio" : "textures"; + TreeItem parent = isModel ? modelsNode : isAudio ? audioNode : texturesNode; + try { - Path dest = ASSET_ROOT.resolve(subDir); - Files.createDirectories(dest); - Path target = dest.resolve(file.getName()); - Files.copy(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING); - parent.getChildren().add(new TreeItem<>(file.getName())); - parent.setExpanded(true); - setStatus("Importiert: " + file.getName()); + Path destDir = ASSET_ROOT.resolve(subDir); + Files.createDirectories(destDir); + Path destFile = destDir.resolve(file.getName()); + Files.copy(file.toPath(), destFile, StandardCopyOption.REPLACE_EXISTING); + + if (isNativeModel) { + // Asynchron via JME3 zu .j3o konvertieren + String assetPath = subDir + "/" + file.getName(); + String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); + Path destJ3o = destDir.resolve(baseName + ".j3o"); + input.modelConvertQueue.offer( + new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile)); + setStatus("Konvertiere " + file.getName() + " → .j3o …"); + } else { + TreeItem newItem = new TreeItem<>(file.getName()); + itemPaths.put(newItem, destFile); + parent.getChildren().add(newItem); + parent.setExpanded(true); + setStatus("Importiert: " + file.getName()); + } } catch (IOException ex) { setStatus("Fehler beim Import: " + ex.getMessage()); } @@ -220,7 +1603,6 @@ public class EditorApp extends Application { StackPane pane = new StackPane(viewport); pane.setStyle("-fx-background-color: #1a1a2e;"); - // ImageView auf Pane-Größe binden → JME3-Pixelskalierung aktualisieren pane.widthProperty().addListener((o, oldW, newW) -> { viewport.setFitWidth(newW.doubleValue()); input.viewportScaleX = VP_WIDTH / newW.doubleValue(); @@ -230,43 +1612,81 @@ public class EditorApp extends Application { input.viewportScaleY = VP_HEIGHT / newH.doubleValue(); }); - // ── Maus-Events ────────────────────────────────────────────────────── viewport.setOnMousePressed(e -> { viewport.requestFocus(); if (e.getButton() == MouseButton.MIDDLE) { - prevDragX = e.getX(); - prevDragY = e.getY(); + prevDragX = e.getX(); prevDragY = e.getY(); } - if (e.getButton() == MouseButton.PRIMARY) { - submitEdit(e.getX(), e.getY(), +1); + boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); + + if (isObjectMode()) { + if (e.getButton() == MouseButton.MIDDLE || bothDown) { + prevDragX = e.getX(); prevDragY = e.getY(); + } else if (e.getButton() == MouseButton.PRIMARY) { + input.objectClickQueue.offer( + new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false)); + objDragPrevX = e.getX(); objDragPrevY = e.getY(); + objDragging = true; + } + return; } - if (e.getButton() == MouseButton.SECONDARY) { - submitEdit(e.getX(), e.getY(), -1); + + if (bothDown) { + stopEditTimer(); + prevDragX = e.getX(); prevDragY = e.getY(); + } else if (e.getButton() == MouseButton.PRIMARY) { + editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1; + submitEdit(editPressX, editPressY, editPressAction); + startEditTimer(); + } else if (e.getButton() == MouseButton.SECONDARY) { + editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1; + submitEdit(editPressX, editPressY, editPressAction); + startEditTimer(); } }); viewport.setOnMouseDragged(e -> { - if (e.isMiddleButtonDown()) { + boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); + + if (isObjectMode()) { + if (e.isMiddleButtonDown() || bothDown) { + double dx = e.getX() - prevDragX; + double dy = e.getY() - prevDragY; + input.addMouseDelta((int) dx, (int) dy); + prevDragX = e.getX(); prevDragY = e.getY(); + } else if (objDragging && e.isPrimaryButtonDown()) { + float dx = (float)(e.getX() - objDragPrevX); + float dy = (float)(e.getY() - objDragPrevY); + input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy)); + objDragPrevX = e.getX(); objDragPrevY = e.getY(); + } + return; + } + + if (e.isMiddleButtonDown() || bothDown) { + stopEditTimer(); double dx = e.getX() - prevDragX; double dy = e.getY() - prevDragY; input.addMouseDelta((int) dx, (int) dy); - prevDragX = e.getX(); - prevDragY = e.getY(); - } - if (e.isPrimaryButtonDown()) { - submitEdit(e.getX(), e.getY(), +1); - } - if (e.isSecondaryButtonDown()) { - submitEdit(e.getX(), e.getY(), -1); + prevDragX = e.getX(); prevDragY = e.getY(); } }); + viewport.setOnMouseReleased(e -> { + objDragging = false; + if (!isObjectMode()) stopEditTimer(); + }); + + pane.setOnMouseMoved(e -> { + input.mouseScreenX = (float) e.getX(); + input.mouseScreenY = (float) e.getY(); + }); + pane.setOnMouseExited(e -> input.mouseScreenX = -1f); + viewport.setOnScroll(e -> { - // Scrollen = Kamera vorwärts/rückwärts double delta = e.getDeltaY(); input.forward = delta > 0; input.backward = delta < 0; - // Nach kurzem Delay zurücksetzen (kein physischer Key-Release) javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.millis(150)); pause.setOnFinished(ev -> { input.forward = false; input.backward = false; }); @@ -276,14 +1696,43 @@ public class EditorApp extends Application { return pane; } + private static void applyModeButtonStyle(ToggleButton btn, boolean active) { + if (active) { + btn.setStyle("-fx-border-color: #2277cc; -fx-border-width: 3; -fx-border-radius: 5;" + + "-fx-background-color: #d4e8ff; -fx-padding: 2;"); + } else { + btn.setStyle("-fx-border-color: #bbbbbb; -fx-border-width: 1; -fx-border-radius: 5;" + + "-fx-background-color: #f4f4f4; -fx-padding: 4;"); + } + } + + private void startEditTimer() { + stopEditTimer(); + editTimer = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(50), + ev -> submitEdit(editPressX, editPressY, editPressAction)) + ); + editTimer.setCycleCount(javafx.animation.Timeline.INDEFINITE); + editTimer.play(); + } + + private void stopEditTimer() { + if (editTimer != null) { editTimer.stop(); editTimer = null; } + } + private void submitEdit(double x, double y, int action) { - input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action)); + switch (input.activeLayer) { + case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action)); + case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action)); + case 4 -> input.textureEditQueue.offer(new SharedInput.TextureEdit((float) x, (float) y, action)); + default -> input.upperLayerEditQueue.offer(new SharedInput.UpperLayerEdit((float) x, (float) y, action)); + } } // ── Statusleiste ───────────────────────────────────────────────────────── private HBox buildStatusBar() { - statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag: Drehen"); + statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag / L+R-Drag: Drehen"); statusLabel.setPadding(new Insets(3, 8, 3, 8)); statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #333;"); HBox bar = new HBox(statusLabel); diff --git a/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java b/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java index 81c05a1..35f5b6e 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java @@ -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); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/FrameTransfer.class b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.class new file mode 100644 index 0000000..4a54da2 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java index 6b133d4..6ab0312 100644 --- a/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java +++ b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java @@ -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 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); } diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index 0749f91..38f1790 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -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) {} +} diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$GrassEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$GrassEdit.class new file mode 100644 index 0000000..ec00d7f Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$GrassEdit.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$TerrainEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$TerrainEdit.class new file mode 100644 index 0000000..a15aa23 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$TerrainEdit.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$TextureEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$TextureEdit.class new file mode 100644 index 0000000..f9458c8 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$TextureEdit.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$TreeGenRequest.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$TreeGenRequest.class new file mode 100644 index 0000000..a1d4c10 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$TreeGenRequest.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$UpperLayerEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$UpperLayerEdit.class new file mode 100644 index 0000000..df89e34 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$UpperLayerEdit.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.class b/blight-editor/src/main/java/de/blight/editor/SharedInput.class new file mode 100644 index 0000000..affed90 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 6f6ebd7..f42dd1b 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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 editQueue = new ConcurrentLinkedQueue<>(); + // ── Upper-Layer-Edits ───────────────────────────────────────────────────── + public record UpperLayerEdit(float screenX, float screenY, int action) {} + public final ConcurrentLinkedQueue 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 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 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 treeGenQueue = new ConcurrentLinkedQueue<>(); + + // ── EZ-Tree-Generator ───────────────────────────────────────────────────── + public record EzTreeGenRequest(de.blight.eztree.TreeOptions options, boolean exportAfter, String exportName) {} + public final ConcurrentLinkedQueue ezTreeGenQueue = new ConcurrentLinkedQueue<>(); + + // ── Palmen-Generator ────────────────────────────────────────────────────── + public record PalmGenRequest(PalmOptions options, boolean exportAfter, String exportName) {} + public final ConcurrentLinkedQueue 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 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 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 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 modelConvertQueue = + new ConcurrentLinkedQueue<>(); } diff --git a/blight-editor/src/main/java/de/blight/editor/object/GrassObject.java b/blight-editor/src/main/java/de/blight/editor/object/GrassObject.java new file mode 100644 index 0000000..56d3c9c --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/object/GrassObject.java @@ -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"; } +} diff --git a/blight-editor/src/main/java/de/blight/editor/object/PlacedObject.java b/blight-editor/src/main/java/de/blight/editor/object/PlacedObject.java new file mode 100644 index 0000000..4e30fb8 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/object/PlacedObject.java @@ -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; } +} diff --git a/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java b/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java new file mode 100644 index 0000000..f7f7a91 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java @@ -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; } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java new file mode 100644 index 0000000..be3f10a --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -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(); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java new file mode 100644 index 0000000..196bcb2 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java @@ -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(); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState$GrassVisibilityControl.class b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState$GrassVisibilityControl.class new file mode 100644 index 0000000..c04ad5d Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState$GrassVisibilityControl.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.class b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.class new file mode 100644 index 0000000..9209db8 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java new file mode 100644 index 0000000..2992830 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java @@ -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 locs, List 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 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 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; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java new file mode 100644 index 0000000..26fa258 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -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 objects = new ArrayList<>(); + private final List 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 + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.class b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.class new file mode 100644 index 0000000..57c15fe Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 6ce71f5..f4d2c6f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -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 locs = new ArrayList<>(); + List 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 locs = new ArrayList<>(); + List newHts = new ArrayList<>(); + List 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; } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$1.class b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$1.class new file mode 100644 index 0000000..11dc71f Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$1.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$TreeLodControl.class b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$TreeLodControl.class new file mode 100644 index 0000000..de9e48b Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$TreeLodControl.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.class b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.class new file mode 100644 index 0000000..50b0196 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java new file mode 100644 index 0000000..afee952 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java @@ -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); + } + + @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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.class b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.class new file mode 100644 index 0000000..ff77789 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.java new file mode 100644 index 0000000..7d3feba --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.java @@ -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))); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.class b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.class new file mode 100644 index 0000000..7a30e1d Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.java new file mode 100644 index 0000000..94be3a4 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.java @@ -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 pos = new ArrayList<>(CPP * CPP * 24 * 3); + ArrayList nor = new ArrayList<>(CPP * CPP * 24 * 3); + ArrayList uv = new ArrayList<>(CPP * CPP * 24 * 2); + ArrayList 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 pos, ArrayList nor, ArrayList uv, + ArrayList 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 pos, ArrayList nor, ArrayList uv, + ArrayList 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 pos, ArrayList nor, ArrayList 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 pos, + ArrayList nor, + ArrayList uv, + ArrayList 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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.class b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.class new file mode 100644 index 0000000..f2c08a3 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java new file mode 100644 index 0000000..ce3fc69 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java @@ -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 worldXZ, List deltas) { + HashSet 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); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.class b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.class new file mode 100644 index 0000000..11eec49 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.java b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.java new file mode 100644 index 0000000..b3f18b0 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.java @@ -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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.class b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.class new file mode 100644 index 0000000..54c085d Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.java b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.java new file mode 100644 index 0000000..ff58b76 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.java @@ -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 getParameters(); + + /** Diskrete Auswahl-Parameter (werden als ChoiceBox gerendert). */ + public List getChoiceParameters() { + return List.of(); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.class b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.class new file mode 100644 index 0000000..55692c1 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java new file mode 100644 index 0000000..adb803d --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java @@ -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 getChoiceParameters() { return List.of(); } + + @Override + public List getParameters() { return List.of(brushRadius, grassHeight, density); } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.class b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.class new file mode 100644 index 0000000..2e92287 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java new file mode 100644 index 0000000..25dfbcc --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java @@ -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 getChoiceParameters() { + return List.of(mode); + } + + @Override + public List getParameters() { + return List.of(brushRadius, brushStrength); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class new file mode 100644 index 0000000..c4b4798 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.java b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.java new file mode 100644 index 0000000..92bdae4 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.java @@ -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 getChoiceParameters() { + return List.of(mode); + } + + @Override + public List getParameters() { + return List.of(brushRadius); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.class b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.class new file mode 100644 index 0000000..5663e2b Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java new file mode 100644 index 0000000..512ee95 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java @@ -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 getChoiceParameters() { return List.of(textureIndex); } + + @Override + public List getParameters() { return List.of(brushRadius, brushStrength); } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.class b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.class new file mode 100644 index 0000000..0571b5c Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.java b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.java new file mode 100644 index 0000000..3710959 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.java @@ -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)); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.class b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.class new file mode 100644 index 0000000..a3da38c Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.java b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.java new file mode 100644 index 0000000..b02f704 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.java @@ -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 getChoiceParameters() { + return List.of(mode); + } + + @Override + public List getParameters() { + return List.of(brushRadius, brushStrength); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java new file mode 100644 index 0000000..80e5b88 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java @@ -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 pos = new ArrayList<>(); + final List norm = new ArrayList<>(); + final List uv = new ArrayList<>(); + final List col = new ArrayList<>(); + final List 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; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java new file mode 100644 index 0000000..90734f3 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java @@ -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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog$FloatSetter.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog$FloatSetter.class new file mode 100644 index 0000000..3e7662e Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog$FloatSetter.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog.class new file mode 100644 index 0000000..52b04ee Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$1BranchTask.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$1BranchTask.class new file mode 100644 index 0000000..03f4872 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$1BranchTask.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$MeshResult.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$MeshResult.class new file mode 100644 index 0000000..72bf60c Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$MeshResult.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$VertexCollector.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$VertexCollector.class new file mode 100644 index 0000000..1727b04 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$VertexCollector.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.class new file mode 100644 index 0000000..cc890eb Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java new file mode 100644 index 0000000..946221d --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java @@ -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 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 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 pos = new ArrayList<>(); + final List norm = new ArrayList<>(); + final List uv = new ArrayList<>(); + final List col = new ArrayList<>(); + final List 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; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.class new file mode 100644 index 0000000..352ab86 Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.class differ diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java new file mode 100644 index 0000000..3b3258a --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java @@ -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)]; + } +} diff --git a/blight-editor/src/main/resources/img/editor/grasstool.png b/blight-editor/src/main/resources/img/editor/grasstool.png new file mode 100644 index 0000000..7f581fe Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/grasstool.png differ diff --git a/blight-editor/src/main/resources/img/editor/terraintool.png b/blight-editor/src/main/resources/img/editor/terraintool.png new file mode 100644 index 0000000..f33bc63 Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool.png differ diff --git a/blight-editor/src/main/resources/img/editor/terraintool_plateau.png b/blight-editor/src/main/resources/img/editor/terraintool_plateau.png new file mode 100644 index 0000000..f3f8e1c Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_plateau.png differ diff --git a/blight-editor/src/main/resources/img/editor/terraintool_sinus.png b/blight-editor/src/main/resources/img/editor/terraintool_sinus.png new file mode 100644 index 0000000..bb8dfc6 Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_sinus.png differ diff --git a/blight-editor/src/main/resources/img/editor/terraintool_smooth.png b/blight-editor/src/main/resources/img/editor/terraintool_smooth.png new file mode 100644 index 0000000..2eff4bd Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_smooth.png differ diff --git a/blight-editor/src/main/resources/img/editor/terraintool_spike.png b/blight-editor/src/main/resources/img/editor/terraintool_spike.png new file mode 100644 index 0000000..88f6c67 Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_spike.png differ diff --git a/blight-editor/src/main/resources/img/editor/textruretool.png b/blight-editor/src/main/resources/img/editor/textruretool.png new file mode 100644 index 0000000..11e4518 Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/textruretool.png differ diff --git a/blight-editor/world/blight_map.blm b/blight-editor/world/blight_map.blm new file mode 100644 index 0000000..8b5dd3c Binary files /dev/null and b/blight-editor/world/blight_map.blm differ diff --git a/blight-game/.gitignore b/blight-game/.gitignore new file mode 100644 index 0000000..da88288 --- /dev/null +++ b/blight-game/.gitignore @@ -0,0 +1 @@ +/.gradle/ diff --git a/blight-game/bin/main/Textures/gras.png b/blight-game/bin/main/Textures/gras.png deleted file mode 100644 index 2f131cb..0000000 Binary files a/blight-game/bin/main/Textures/gras.png and /dev/null differ diff --git a/blight-game/build.gradle b/blight-game/build.gradle index 6caa0d4..b78f1b1 100644 --- a/blight-game/build.gradle +++ b/blight-game/build.gradle @@ -1,81 +1,53 @@ -plugins { - id 'java' - id 'application' -} - -group = 'de.blight' -version = '0.1.0' - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -sourceSets { - main { - resources { - srcDirs = ['src/main/resources', 'assets'] - } - } -} - -application { - mainClass = 'de.blight.game.BlightApp' - // jMonkeyEngine benötigt den headless-Flag nicht; nativer Fenstermodus - 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 { - // jMonkeyEngine core - implementation "org.jmonkeyengine:jme3-core:${jmeVersion}" - implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}" - implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}" - - // Terrain, Effects, Plugins - implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}" - implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}" - - // Bullet-Physik (nativ) - implementation "org.jmonkeyengine:jme3-jbullet:${jmeVersion}" - - // Testdaten / eingebaute Assets (Primitiv-Modelle, Skybox etc.) - implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}" - - // JSON für Key-Binding-Konfiguration - implementation 'com.google.code.gson:gson:2.11.0' -} - -// Native Libraries automatisch entpacken -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' +} + +application { + mainClass = 'de.blight.game.BlightApp' + 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.7.0-stable' +} + +dependencies { + implementation project(':blight-common') + implementation project(':blight-assets') + + 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-jbullet:${jmeVersion}" + implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}" + implementation 'com.google.code.gson:gson:2.11.0' +} + +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 // gemeinsames Arbeitsverzeichnis = Projekt-Root +} + +jar { + manifest { + attributes 'Main-Class': application.mainClass + } +} diff --git a/blight-game/gradle/wrapper/gradle-wrapper.properties b/blight-game/gradle/wrapper/gradle-wrapper.properties index b82aa23..a034286 100644 --- a/blight-game/gradle/wrapper/gradle-wrapper.properties +++ b/blight-game/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/blight-game/gradlew b/blight-game/gradlew index 97de990..30553ae 100755 --- a/blight-game/gradlew +++ b/blight-game/gradlew @@ -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" "$@" diff --git a/blight-game/gradlew.bat b/blight-game/gradlew.bat index 16e26a1..66f1aa7 100644 --- a/blight-game/gradlew.bat +++ b/blight-game/gradlew.bat @@ -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 diff --git a/blight-game/settings.gradle b/blight-game/settings.gradle index f95b0de..b3c08c2 100644 --- a/blight-game/settings.gradle +++ b/blight-game/settings.gradle @@ -1 +1,8 @@ -rootProject.name = 'blight-game' +rootProject.name = 'blight-game' + +// Sibling-Projekte einbinden. +include 'blight-common' +project(':blight-common').projectDir = new File(settingsDir, '../blight-common') + +include 'blight-assets' +project(':blight-assets').projectDir = new File(settingsDir, '../blight-assets') diff --git a/blight-game/src/main/java/de/blight/game/BlightApp.java b/blight-game/src/main/java/de/blight/game/BlightApp.java index 204b91f..3a50592 100644 --- a/blight-game/src/main/java/de/blight/game/BlightApp.java +++ b/blight-game/src/main/java/de/blight/game/BlightApp.java @@ -1,92 +1,92 @@ -package de.blight.game; - -import com.jme3.app.SimpleApplication; -import com.jme3.input.KeyInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.KeyTrigger; -import com.jme3.system.AppSettings; -import de.blight.game.config.*; -import de.blight.game.scene.WorldScene; - -public class BlightApp extends SimpleApplication { - - private KeyBindings keyBindings; - private GraphicsSettings graphicsSettings; - private WorldScene worldScene; - private ConfigScreen configScreen; - private GraphicsScreen graphicsScreen; - private PauseMenu pauseMenu; - - public static void main(String[] args) { - BlightApp app = new BlightApp(); - - GraphicsSettings gs = GraphicsStore.load(); - AppSettings settings = new AppSettings(true); - settings.setTitle("Blight"); - settings.setResolution(gs.width, gs.height); - settings.setFullscreen(gs.fullscreen); - settings.setVSync(gs.vsync); - settings.setSamples(gs.samples); - - app.setSettings(settings); - app.setShowSettings(false); - app.start(); - } - - @Override - public void simpleInitApp() { - flyCam.setEnabled(false); - inputManager.deleteMapping(INPUT_MAPPING_EXIT); - - keyBindings = KeyBindingStore.load(); - graphicsSettings = GraphicsStore.load(); - - worldScene = new WorldScene(keyBindings); - stateManager.attach(worldScene); - - configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings)); - configScreen.setOnClose(() -> pauseMenu.setEnabled(true)); - stateManager.attach(configScreen); - configScreen.setEnabled(false); - - graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true)); - stateManager.attach(graphicsScreen); - graphicsScreen.setEnabled(false); - - pauseMenu = new PauseMenu( - () -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); }, - () -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); } - ); - stateManager.attach(pauseMenu); - pauseMenu.setEnabled(false); - - inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE)); - inputManager.addListener((ActionListener) (name, isPressed, tpf) -> { - if (!isPressed) return; - - if (graphicsScreen.isEnabled()) { - // GraphicsScreen wird nur über seine eigenen Buttons geschlossen - return; - } - if (configScreen.isEnabled()) { - if (configScreen.isWaiting()) { - configScreen.cancelWaiting(); - } else { - configScreen.setEnabled(false); - pauseMenu.setEnabled(true); - } - return; - } - if (pauseMenu.isEnabled()) { - pauseMenu.setEnabled(false); - worldScene.setPaused(false); - return; - } - pauseMenu.setEnabled(true); - worldScene.setPaused(true); - }, "ToggleMenu"); - } - - @Override - public void simpleUpdate(float tpf) {} -} +package de.blight.game; + +import com.jme3.app.SimpleApplication; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.system.AppSettings; +import de.blight.game.config.*; +import de.blight.game.scene.WorldScene; + +public class BlightApp extends SimpleApplication { + + private KeyBindings keyBindings; + private GraphicsSettings graphicsSettings; + private WorldScene worldScene; + private ConfigScreen configScreen; + private GraphicsScreen graphicsScreen; + private PauseMenu pauseMenu; + + public static void main(String[] args) { + BlightApp app = new BlightApp(); + + GraphicsSettings gs = GraphicsStore.load(); + AppSettings settings = new AppSettings(true); + settings.setTitle("Blight"); + settings.setResolution(gs.width, gs.height); + settings.setFullscreen(gs.fullscreen); + settings.setVSync(gs.vsync); + settings.setSamples(gs.samples); + + app.setSettings(settings); + app.setShowSettings(false); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setEnabled(false); + inputManager.deleteMapping(INPUT_MAPPING_EXIT); + + keyBindings = KeyBindingStore.load(); + graphicsSettings = GraphicsStore.load(); + + worldScene = new WorldScene(keyBindings); + stateManager.attach(worldScene); + + configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings)); + configScreen.setOnClose(() -> pauseMenu.setEnabled(true)); + stateManager.attach(configScreen); + configScreen.setEnabled(false); + + graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true)); + stateManager.attach(graphicsScreen); + graphicsScreen.setEnabled(false); + + pauseMenu = new PauseMenu( + () -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); }, + () -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); } + ); + stateManager.attach(pauseMenu); + pauseMenu.setEnabled(false); + + inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE)); + inputManager.addListener((ActionListener) (name, isPressed, tpf) -> { + if (!isPressed) return; + + if (graphicsScreen.isEnabled()) { + // GraphicsScreen wird nur über seine eigenen Buttons geschlossen + return; + } + if (configScreen.isEnabled()) { + if (configScreen.isWaiting()) { + configScreen.cancelWaiting(); + } else { + configScreen.setEnabled(false); + pauseMenu.setEnabled(true); + } + return; + } + if (pauseMenu.isEnabled()) { + pauseMenu.setEnabled(false); + worldScene.setPaused(false); + return; + } + pauseMenu.setEnabled(true); + worldScene.setPaused(true); + }, "ToggleMenu"); + } + + @Override + public void simpleUpdate(float tpf) {} +} diff --git a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java index fd13c6a..db51a4c 100644 --- a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java @@ -1,309 +1,309 @@ -package de.blight.game.config; - -import java.util.ArrayList; -import java.util.List; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.KeyInput; -import com.jme3.input.MouseInput; -import com.jme3.input.RawInputListener; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.input.event.JoyAxisEvent; -import com.jme3.input.event.JoyButtonEvent; -import com.jme3.input.event.KeyInputEvent; -import com.jme3.input.event.MouseButtonEvent; -import com.jme3.input.event.MouseMotionEvent; -import com.jme3.input.event.TouchEvent; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; - -/** - * Overlay-AppState der die Tastenbelegungs-Maske anzeigt. - * - * ESC → Schließen (ohne Speichern) - * Klick auf Row → wartet auf neue Taste - * ESC während Warten → bricht nur die Zuweisung ab - * Speichern → schreibt JSON, ruft onSave-Callback - */ -public class ConfigScreen extends BaseAppState implements RawInputListener { - - // Farben - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f); - private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f); - private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); - private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); - - // ----------------------------------------------------------------------- - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - - private KeyBindings liveBindings; // geteilt mit der ganzen App - private KeyBindings editCopy; // wird beim Öffnen geklont - - private Runnable onSave; // Callback → PlayerInputControl.reloadBindings - private Runnable onClose; // Callback → PauseMenu wiederherstellen - - private Node panel; - private List rows = new ArrayList<>(); - private int waitingRow = -1; // -1 = keine Zuweisung aktiv - - // UI-Elemente für Buttons (Bounds in Screen-Koordinaten) - private float saveBtnX, saveBtnY, saveBtnW, saveBtnH; - private float cancelBtnX, cancelBtnY; - - // ----------------------------------------------------------------------- - - private static class Row { - String field; - String label; - BitmapText keyText; - Geometry bg; - float x, y, w, h; // Button-Bounds - } - - // ----------------------------------------------------------------------- - - public ConfigScreen(KeyBindings liveBindings, Runnable onSave) { - this.liveBindings = liveBindings; - this.onSave = onSave; - } - - public boolean isWaiting() { return waitingRow >= 0; } - - public void setOnClose(Runnable onClose) { this.onClose = onClose; } - - public void cancelWaiting() { - if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; } - } - - // ----------------------------------------------------------------------- - // Lifecycle - // ----------------------------------------------------------------------- - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - editCopy = liveBindings.copy(); - waitingRow = -1; - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addRawInputListener(this); - app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_CfgClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - rows.clear(); - waitingRow = -1; - app.getInputManager().removeRawInputListener(this); - app.getInputManager().deleteMapping("_CfgClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - // ----------------------------------------------------------------------- - // UI aufbauen - // ----------------------------------------------------------------------- - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - - panel = new Node("cfg-panel"); - - // Halbdurchsichtiger Overlay über dem Spiel - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 720, ph = 440; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - // Titel - BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT); - centerText(title, px, py + ph - 40, pw); - panel.attachChild(title); - - BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f)); - centerText(hint, px, py + ph - 70, pw); - panel.attachChild(hint); - - // Reihen - float rowX = px + 30; - float keyX = px + pw - 220; - float rowW = 180; - float rowH = 36; - float startY = py + ph - 110; - float stepY = 48; - - for (int i = 0; i < KeyBindings.ENTRIES.length; i++) { - String[] entry = KeyBindings.ENTRIES[i]; - float ry = startY - i * stepY; - - BitmapText lbl = text(entry[1], 16, COL_TEXT); - lbl.setLocalTranslation(rowX, ry + rowH - 8, 0); - panel.attachChild(lbl); - - Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0); - - BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY); - kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1); - panel.attachChild(kt); - - Row row = new Row(); - row.field = entry[0]; - row.label = entry[1]; - row.keyText = kt; - row.bg = bg; - row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH; - rows.add(row); - } - - // Buttons - float btnW = 160, btnH = 42; - float btnY = py + 25; - saveBtnX = px + pw / 2f - btnW - 15; - saveBtnY = btnY; - saveBtnW = btnW; - saveBtnH = btnH; - cancelBtnX = px + pw / 2f + 15; - cancelBtnY = btnY; - - addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0); - BitmapText saveLabel = text("Speichern", 16, COL_TEXT); - centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW); - panel.attachChild(saveLabel); - - addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0); - BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT); - centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW); - panel.attachChild(cancelLabel); - - guiNode.attachChild(panel); - } - - // ----------------------------------------------------------------------- - // Mausklick - // ----------------------------------------------------------------------- - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f cursor = app.getInputManager().getCursorPosition(); - - // Reihen prüfen - for (int i = 0; i < rows.size(); i++) { - Row r = rows.get(i); - if (hits(cursor, r.x, r.y, r.w, r.h)) { - waitingRow = i; - r.bg.getMaterial().setColor("Color", COL_ROW_WAIT); - r.keyText.setText("..."); - return; - } - } - - // Speichern - if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) { - liveBindings.copyFrom(editCopy); - KeyBindingStore.save(liveBindings); - if (onSave != null) onSave.run(); - setEnabled(false); - if (onClose != null) onClose.run(); - return; - } - - // Abbrechen - if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) { - setEnabled(false); - if (onClose != null) onClose.run(); - } - }; - - // ----------------------------------------------------------------------- - // Tastendruck beim Warten auf Zuweisung (RawInputListener) - // ----------------------------------------------------------------------- - - @Override - public void onKeyEvent(KeyInputEvent evt) { - if (!evt.isPressed() || waitingRow < 0) return; - if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen - - Row r = rows.get(waitingRow); - editCopy.set(r.field, evt.getKeyCode()); - r.keyText.setText(KeyNames.of(evt.getKeyCode())); - resetRowColor(waitingRow); - waitingRow = -1; - } - - private void resetRowColor(int idx) { - rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW); - } - - // RawInputListener-Pflichtmethoden - @Override public void beginInput() {} - @Override public void endInput() {} - @Override public void onMouseMotionEvent(MouseMotionEvent evt) {} - @Override public void onMouseButtonEvent(MouseButtonEvent evt) {} - @Override public void onJoyAxisEvent(JoyAxisEvent evt) {} - @Override public void onJoyButtonEvent(JoyButtonEvent evt) {} - @Override public void onTouchEvent(TouchEvent evt) {} - - // ----------------------------------------------------------------------- - // Hilfsmethoden - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText text(String content, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font, false); - t.setSize(size); - t.setColor(color); - t.setText(content); - return t; - } - - private void centerText(BitmapText t, float x, float y, float width) { - t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); - } - - private boolean hits(Vector2f p, float x, float y, float w, float h) { - return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; - } -} +package de.blight.game.config; + +import java.util.ArrayList; +import java.util.List; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListener; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.input.event.MouseButtonEvent; +import com.jme3.input.event.MouseMotionEvent; +import com.jme3.input.event.TouchEvent; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; + +/** + * Overlay-AppState der die Tastenbelegungs-Maske anzeigt. + * + * ESC → Schließen (ohne Speichern) + * Klick auf Row → wartet auf neue Taste + * ESC während Warten → bricht nur die Zuweisung ab + * Speichern → schreibt JSON, ruft onSave-Callback + */ +public class ConfigScreen extends BaseAppState implements RawInputListener { + + // Farben + private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); + private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); + private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); + private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f); + private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f); + private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); + private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); + private static final ColorRGBA COL_TEXT = ColorRGBA.White; + private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); + + // ----------------------------------------------------------------------- + + private SimpleApplication app; + private Node guiNode; + private BitmapFont font; + + private KeyBindings liveBindings; // geteilt mit der ganzen App + private KeyBindings editCopy; // wird beim Öffnen geklont + + private Runnable onSave; // Callback → PlayerInputControl.reloadBindings + private Runnable onClose; // Callback → PauseMenu wiederherstellen + + private Node panel; + private List rows = new ArrayList<>(); + private int waitingRow = -1; // -1 = keine Zuweisung aktiv + + // UI-Elemente für Buttons (Bounds in Screen-Koordinaten) + private float saveBtnX, saveBtnY, saveBtnW, saveBtnH; + private float cancelBtnX, cancelBtnY; + + // ----------------------------------------------------------------------- + + private static class Row { + String field; + String label; + BitmapText keyText; + Geometry bg; + float x, y, w, h; // Button-Bounds + } + + // ----------------------------------------------------------------------- + + public ConfigScreen(KeyBindings liveBindings, Runnable onSave) { + this.liveBindings = liveBindings; + this.onSave = onSave; + } + + public boolean isWaiting() { return waitingRow >= 0; } + + public void setOnClose(Runnable onClose) { this.onClose = onClose; } + + public void cancelWaiting() { + if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; } + } + + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.guiNode = this.app.getGuiNode(); + this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + protected void onEnable() { + editCopy = liveBindings.copy(); + waitingRow = -1; + buildUI(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addRawInputListener(this); + app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, "_CfgClick"); + } + + @Override + protected void onDisable() { + if (panel != null) { guiNode.detachChild(panel); panel = null; } + rows.clear(); + waitingRow = -1; + app.getInputManager().removeRawInputListener(this); + app.getInputManager().deleteMapping("_CfgClick"); + app.getInputManager().setCursorVisible(false); + } + + @Override protected void cleanup(Application app) {} + + // ----------------------------------------------------------------------- + // UI aufbauen + // ----------------------------------------------------------------------- + + private void buildUI() { + float sw = app.getCamera().getWidth(); + float sh = app.getCamera().getHeight(); + + panel = new Node("cfg-panel"); + + // Halbdurchsichtiger Overlay über dem Spiel + addQuad(panel, 0, 0, sw, sh, COL_BG, -2); + + float pw = 720, ph = 440; + float px = (sw - pw) / 2f, py = (sh - ph) / 2f; + addQuad(panel, px, py, pw, ph, COL_PANEL, -1); + + // Titel + BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT); + centerText(title, px, py + ph - 40, pw); + panel.attachChild(title); + + BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f)); + centerText(hint, px, py + ph - 70, pw); + panel.attachChild(hint); + + // Reihen + float rowX = px + 30; + float keyX = px + pw - 220; + float rowW = 180; + float rowH = 36; + float startY = py + ph - 110; + float stepY = 48; + + for (int i = 0; i < KeyBindings.ENTRIES.length; i++) { + String[] entry = KeyBindings.ENTRIES[i]; + float ry = startY - i * stepY; + + BitmapText lbl = text(entry[1], 16, COL_TEXT); + lbl.setLocalTranslation(rowX, ry + rowH - 8, 0); + panel.attachChild(lbl); + + Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0); + + BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY); + kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1); + panel.attachChild(kt); + + Row row = new Row(); + row.field = entry[0]; + row.label = entry[1]; + row.keyText = kt; + row.bg = bg; + row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH; + rows.add(row); + } + + // Buttons + float btnW = 160, btnH = 42; + float btnY = py + 25; + saveBtnX = px + pw / 2f - btnW - 15; + saveBtnY = btnY; + saveBtnW = btnW; + saveBtnH = btnH; + cancelBtnX = px + pw / 2f + 15; + cancelBtnY = btnY; + + addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0); + BitmapText saveLabel = text("Speichern", 16, COL_TEXT); + centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW); + panel.attachChild(saveLabel); + + addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0); + BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT); + centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW); + panel.attachChild(cancelLabel); + + guiNode.attachChild(panel); + } + + // ----------------------------------------------------------------------- + // Mausklick + // ----------------------------------------------------------------------- + + private final ActionListener clickListener = (name, isPressed, tpf) -> { + if (!isPressed) return; + Vector2f cursor = app.getInputManager().getCursorPosition(); + + // Reihen prüfen + for (int i = 0; i < rows.size(); i++) { + Row r = rows.get(i); + if (hits(cursor, r.x, r.y, r.w, r.h)) { + waitingRow = i; + r.bg.getMaterial().setColor("Color", COL_ROW_WAIT); + r.keyText.setText("..."); + return; + } + } + + // Speichern + if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) { + liveBindings.copyFrom(editCopy); + KeyBindingStore.save(liveBindings); + if (onSave != null) onSave.run(); + setEnabled(false); + if (onClose != null) onClose.run(); + return; + } + + // Abbrechen + if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) { + setEnabled(false); + if (onClose != null) onClose.run(); + } + }; + + // ----------------------------------------------------------------------- + // Tastendruck beim Warten auf Zuweisung (RawInputListener) + // ----------------------------------------------------------------------- + + @Override + public void onKeyEvent(KeyInputEvent evt) { + if (!evt.isPressed() || waitingRow < 0) return; + if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen + + Row r = rows.get(waitingRow); + editCopy.set(r.field, evt.getKeyCode()); + r.keyText.setText(KeyNames.of(evt.getKeyCode())); + resetRowColor(waitingRow); + waitingRow = -1; + } + + private void resetRowColor(int idx) { + rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW); + } + + // RawInputListener-Pflichtmethoden + @Override public void beginInput() {} + @Override public void endInput() {} + @Override public void onMouseMotionEvent(MouseMotionEvent evt) {} + @Override public void onMouseButtonEvent(MouseButtonEvent evt) {} + @Override public void onJoyAxisEvent(JoyAxisEvent evt) {} + @Override public void onJoyButtonEvent(JoyButtonEvent evt) {} + @Override public void onTouchEvent(TouchEvent evt) {} + + // ----------------------------------------------------------------------- + // Hilfsmethoden + // ----------------------------------------------------------------------- + + private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { + Geometry geo = new Geometry("q", new Quad(w, h)); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color.clone()); + if (color.a < 1f) { + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + } + geo.setMaterial(mat); + geo.setLocalTranslation(x, y, z); + parent.attachChild(geo); + return geo; + } + + private BitmapText text(String content, int size, ColorRGBA color) { + BitmapText t = new BitmapText(font, false); + t.setSize(size); + t.setColor(color); + t.setText(content); + return t; + } + + private void centerText(BitmapText t, float x, float y, float width) { + t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); + } + + private boolean hits(Vector2f p, float x, float y, float w, float h) { + return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java index fb884a5..917bed7 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java @@ -1,290 +1,290 @@ -package de.blight.game.config; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; -import com.jme3.system.AppSettings; - -public class GraphicsScreen extends BaseAppState { - - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_ARROW = new ColorRGBA(0.28f, 0.28f, 0.44f, 1.00f); - private static final ColorRGBA COL_BTN_OK = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); - private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); - - private static final int[][] RESOLUTIONS = { - {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160} - }; - private static final int[] SAMPLES = {0, 2, 4, 8}; - - private static final int ROW_RES = 0; - private static final int ROW_FULL = 1; - private static final int ROW_VSYNC = 2; - private static final int ROW_AA = 3; - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - private Node panel; - - private final GraphicsSettings live; - private GraphicsSettings edit; - private final Runnable onClose; - - private int resIdx; - private int samplesIdx; - - // Per-row layout (indexed by ROW_*) - private final float[] cellX = new float[4]; - private final float[] cellY = new float[4]; - private final float[] cellW = new float[4]; - private final float cellH = 36; - private final float arrW = 30; - private final float[] leftX = new float[4]; - private final float[] rightX = new float[4]; - private final BitmapText[] valTexts = new BitmapText[4]; - - private float okX, okY, okW, okH; - private float cancelX, cancelY; - - public GraphicsScreen(GraphicsSettings live, Runnable onClose) { - this.live = live; - this.onClose = onClose; - } - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - edit = new GraphicsSettings(); - edit.width = live.width; edit.height = live.height; - edit.fullscreen = live.fullscreen; - edit.vsync = live.vsync; - edit.samples = live.samples; - - resIdx = 0; - for (int i = 0; i < RESOLUTIONS.length; i++) { - if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) { - resIdx = i; break; - } - } - samplesIdx = 0; - for (int i = 0; i < SAMPLES.length; i++) { - if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; } - } - - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_GfxClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - app.getInputManager().deleteMapping("_GfxClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - panel = new Node("gfx-panel"); - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 640, ph = 400; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT); - centerText(title, px, py + ph - 42, pw); - panel.attachChild(title); - - String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"}; - float lblX = px + 30; - float vx = px + pw - 270; - float vw = 190; - float startY = py + ph - 100; - float step = 60; - - for (int i = 0; i < 4; i++) { - float ry = startY - i * step; - - BitmapText lbl = txt(labels[i], 16, COL_TEXT); - lbl.setLocalTranslation(lblX, ry + cellH - 8, 0); - panel.attachChild(lbl); - - // Left arrow - addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0); - BitmapText lt = txt("<", 16, COL_TEXT); - lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1); - panel.attachChild(lt); - - // Value cell - addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0); - - // Right arrow - addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0); - BitmapText rt = txt(">", 16, COL_TEXT); - rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1); - panel.attachChild(rt); - - BitmapText vt = txt("", 16, COL_TEXT_VAL); - panel.attachChild(vt); - valTexts[i] = vt; - - cellX[i] = vx; cellY[i] = ry; cellW[i] = vw; - leftX[i] = vx - arrW - 6; - rightX[i] = vx + vw + 6; - } - - for (int i = 0; i < 4; i++) refreshText(i); - - float bw = 160, bh = 42; - okW = bw; okH = bh; - okX = px + pw / 2f - bw - 10; - okY = py + 22; - cancelX = px + pw / 2f + 10; - cancelY = py + 22; - - addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0); - BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT); - centerText(okLbl, okX, okY + bh - 10, bw); - panel.attachChild(okLbl); - - addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0); - BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT); - centerText(cancelLbl, cancelX, cancelY + bh - 10, bw); - panel.attachChild(cancelLbl); - - guiNode.attachChild(panel); - } - - private void refreshText(int row) { - String val = switch (row) { - case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1]; - case ROW_FULL -> edit.fullscreen ? "An" : "Aus"; - case ROW_VSYNC -> edit.vsync ? "An" : "Aus"; - case ROW_AA -> SAMPLES[samplesIdx] == 0 ? "Aus" : SAMPLES[samplesIdx] + "x MSAA"; - default -> ""; - }; - BitmapText vt = valTexts[row]; - vt.setText(val); - vt.setLocalTranslation( - cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f, - cellY[row] + cellH - 8, - 1 - ); - } - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f c = app.getInputManager().getCursorPosition(); - - for (int i = 0; i < 4; i++) { - if (hits(c, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; } - if (hits(c, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; } - } - if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; } - if (hits(c, cancelX, cancelY, okW, okH)) { close(); } - }; - - private void cycleRow(int row, int dir) { - switch (row) { - case ROW_RES: - resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length; - edit.width = RESOLUTIONS[resIdx][0]; - edit.height = RESOLUTIONS[resIdx][1]; - break; - case ROW_FULL: - edit.fullscreen = !edit.fullscreen; - break; - case ROW_VSYNC: - edit.vsync = !edit.vsync; - break; - case ROW_AA: - samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length; - edit.samples = SAMPLES[samplesIdx]; - break; - } - refreshText(row); - } - - private void applyAndSave() { - live.width = edit.width; live.height = edit.height; - live.fullscreen = edit.fullscreen; - live.vsync = edit.vsync; - live.samples = edit.samples; - - GraphicsStore.save(live); - - AppSettings s = app.getContext().getSettings(); - s.setResolution(live.width, live.height); - s.setFullscreen(live.fullscreen); - s.setVSync(live.vsync); - s.setSamples(live.samples); - app.setSettings(s); - - close(); - app.restart(); - } - - private void close() { - setEnabled(false); - if (onClose != null) onClose.run(); - } - - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText txt(String s, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font, false); - t.setSize(size); t.setColor(color); t.setText(s); - return t; - } - - private void centerText(BitmapText t, float x, float y, float width) { - t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); - } - - private boolean hits(Vector2f p, float x, float y, float w, float h) { - return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; - } -} +package de.blight.game.config; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; +import com.jme3.system.AppSettings; + +public class GraphicsScreen extends BaseAppState { + + private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); + private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); + private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); + private static final ColorRGBA COL_ARROW = new ColorRGBA(0.28f, 0.28f, 0.44f, 1.00f); + private static final ColorRGBA COL_BTN_OK = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); + private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); + private static final ColorRGBA COL_TEXT = ColorRGBA.White; + private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); + + private static final int[][] RESOLUTIONS = { + {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160} + }; + private static final int[] SAMPLES = {0, 2, 4, 8}; + + private static final int ROW_RES = 0; + private static final int ROW_FULL = 1; + private static final int ROW_VSYNC = 2; + private static final int ROW_AA = 3; + + private SimpleApplication app; + private Node guiNode; + private BitmapFont font; + private Node panel; + + private final GraphicsSettings live; + private GraphicsSettings edit; + private final Runnable onClose; + + private int resIdx; + private int samplesIdx; + + // Per-row layout (indexed by ROW_*) + private final float[] cellX = new float[4]; + private final float[] cellY = new float[4]; + private final float[] cellW = new float[4]; + private final float cellH = 36; + private final float arrW = 30; + private final float[] leftX = new float[4]; + private final float[] rightX = new float[4]; + private final BitmapText[] valTexts = new BitmapText[4]; + + private float okX, okY, okW, okH; + private float cancelX, cancelY; + + public GraphicsScreen(GraphicsSettings live, Runnable onClose) { + this.live = live; + this.onClose = onClose; + } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.guiNode = this.app.getGuiNode(); + this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + protected void onEnable() { + edit = new GraphicsSettings(); + edit.width = live.width; edit.height = live.height; + edit.fullscreen = live.fullscreen; + edit.vsync = live.vsync; + edit.samples = live.samples; + + resIdx = 0; + for (int i = 0; i < RESOLUTIONS.length; i++) { + if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) { + resIdx = i; break; + } + } + samplesIdx = 0; + for (int i = 0; i < SAMPLES.length; i++) { + if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; } + } + + buildUI(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, "_GfxClick"); + } + + @Override + protected void onDisable() { + if (panel != null) { guiNode.detachChild(panel); panel = null; } + app.getInputManager().deleteMapping("_GfxClick"); + app.getInputManager().setCursorVisible(false); + } + + @Override protected void cleanup(Application app) {} + + private void buildUI() { + float sw = app.getCamera().getWidth(); + float sh = app.getCamera().getHeight(); + panel = new Node("gfx-panel"); + addQuad(panel, 0, 0, sw, sh, COL_BG, -2); + + float pw = 640, ph = 400; + float px = (sw - pw) / 2f, py = (sh - ph) / 2f; + addQuad(panel, px, py, pw, ph, COL_PANEL, -1); + + BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT); + centerText(title, px, py + ph - 42, pw); + panel.attachChild(title); + + String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"}; + float lblX = px + 30; + float vx = px + pw - 270; + float vw = 190; + float startY = py + ph - 100; + float step = 60; + + for (int i = 0; i < 4; i++) { + float ry = startY - i * step; + + BitmapText lbl = txt(labels[i], 16, COL_TEXT); + lbl.setLocalTranslation(lblX, ry + cellH - 8, 0); + panel.attachChild(lbl); + + // Left arrow + addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0); + BitmapText lt = txt("<", 16, COL_TEXT); + lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1); + panel.attachChild(lt); + + // Value cell + addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0); + + // Right arrow + addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0); + BitmapText rt = txt(">", 16, COL_TEXT); + rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1); + panel.attachChild(rt); + + BitmapText vt = txt("", 16, COL_TEXT_VAL); + panel.attachChild(vt); + valTexts[i] = vt; + + cellX[i] = vx; cellY[i] = ry; cellW[i] = vw; + leftX[i] = vx - arrW - 6; + rightX[i] = vx + vw + 6; + } + + for (int i = 0; i < 4; i++) refreshText(i); + + float bw = 160, bh = 42; + okW = bw; okH = bh; + okX = px + pw / 2f - bw - 10; + okY = py + 22; + cancelX = px + pw / 2f + 10; + cancelY = py + 22; + + addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0); + BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT); + centerText(okLbl, okX, okY + bh - 10, bw); + panel.attachChild(okLbl); + + addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0); + BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT); + centerText(cancelLbl, cancelX, cancelY + bh - 10, bw); + panel.attachChild(cancelLbl); + + guiNode.attachChild(panel); + } + + private void refreshText(int row) { + String val = switch (row) { + case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1]; + case ROW_FULL -> edit.fullscreen ? "An" : "Aus"; + case ROW_VSYNC -> edit.vsync ? "An" : "Aus"; + case ROW_AA -> SAMPLES[samplesIdx] == 0 ? "Aus" : SAMPLES[samplesIdx] + "x MSAA"; + default -> ""; + }; + BitmapText vt = valTexts[row]; + vt.setText(val); + vt.setLocalTranslation( + cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f, + cellY[row] + cellH - 8, + 1 + ); + } + + private final ActionListener clickListener = (name, isPressed, tpf) -> { + if (!isPressed) return; + Vector2f c = app.getInputManager().getCursorPosition(); + + for (int i = 0; i < 4; i++) { + if (hits(c, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; } + if (hits(c, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; } + } + if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; } + if (hits(c, cancelX, cancelY, okW, okH)) { close(); } + }; + + private void cycleRow(int row, int dir) { + switch (row) { + case ROW_RES: + resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length; + edit.width = RESOLUTIONS[resIdx][0]; + edit.height = RESOLUTIONS[resIdx][1]; + break; + case ROW_FULL: + edit.fullscreen = !edit.fullscreen; + break; + case ROW_VSYNC: + edit.vsync = !edit.vsync; + break; + case ROW_AA: + samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length; + edit.samples = SAMPLES[samplesIdx]; + break; + } + refreshText(row); + } + + private void applyAndSave() { + live.width = edit.width; live.height = edit.height; + live.fullscreen = edit.fullscreen; + live.vsync = edit.vsync; + live.samples = edit.samples; + + GraphicsStore.save(live); + + AppSettings s = app.getContext().getSettings(); + s.setResolution(live.width, live.height); + s.setFullscreen(live.fullscreen); + s.setVSync(live.vsync); + s.setSamples(live.samples); + app.setSettings(s); + + close(); + app.restart(); + } + + private void close() { + setEnabled(false); + if (onClose != null) onClose.run(); + } + + // ----------------------------------------------------------------------- + + private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { + Geometry geo = new Geometry("q", new Quad(w, h)); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color.clone()); + if (color.a < 1f) { + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + } + geo.setMaterial(mat); + geo.setLocalTranslation(x, y, z); + parent.attachChild(geo); + return geo; + } + + private BitmapText txt(String s, int size, ColorRGBA color) { + BitmapText t = new BitmapText(font, false); + t.setSize(size); t.setColor(color); t.setText(s); + return t; + } + + private void centerText(BitmapText t, float x, float y, float width) { + t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); + } + + private boolean hits(Vector2f p, float x, float y, float w, float h) { + return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java index 9f72757..fbc9ea9 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java @@ -1,9 +1,9 @@ -package de.blight.game.config; - -public class GraphicsSettings { - public int width = 1280; - public int height = 720; - public boolean fullscreen = false; - public boolean vsync = false; - public int samples = 4; -} +package de.blight.game.config; + +public class GraphicsSettings { + public int width = 1280; + public int height = 720; + public boolean fullscreen = false; + public boolean vsync = false; + public int samples = 4; +} diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java index 9f24c44..f1377bc 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java @@ -1,35 +1,35 @@ -package de.blight.game.config; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.*; -import java.nio.file.*; - -public class GraphicsStore { - - private static final Path FILE = Paths.get("config", "graphics.json"); - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - public static GraphicsSettings load() { - if (Files.exists(FILE)) { - try (Reader r = Files.newBufferedReader(FILE)) { - return GSON.fromJson(r, GraphicsSettings.class); - } catch (IOException e) { - System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage()); - } - } - return new GraphicsSettings(); - } - - public static void save(GraphicsSettings gs) { - try { - Files.createDirectories(FILE.getParent()); - try (Writer w = Files.newBufferedWriter(FILE)) { - GSON.toJson(gs, w); - } - } catch (IOException e) { - System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage()); - } - } -} +package de.blight.game.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.*; +import java.nio.file.*; + +public class GraphicsStore { + + private static final Path FILE = Paths.get("config", "graphics.json"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public static GraphicsSettings load() { + if (Files.exists(FILE)) { + try (Reader r = Files.newBufferedReader(FILE)) { + return GSON.fromJson(r, GraphicsSettings.class); + } catch (IOException e) { + System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage()); + } + } + return new GraphicsSettings(); + } + + public static void save(GraphicsSettings gs) { + try { + Files.createDirectories(FILE.getParent()); + try (Writer w = Files.newBufferedWriter(FILE)) { + GSON.toJson(gs, w); + } + } catch (IOException e) { + System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage()); + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java index a15e8bf..23cfbdb 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java @@ -1,35 +1,35 @@ -package de.blight.game.config; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.*; -import java.nio.file.*; - -public class KeyBindingStore { - - private static final Path FILE = Paths.get("config", "keybindings.json"); - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - public static KeyBindings load() { - if (Files.exists(FILE)) { - try (Reader r = Files.newBufferedReader(FILE)) { - return GSON.fromJson(r, KeyBindings.class); - } catch (IOException e) { - System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage()); - } - } - return new KeyBindings(); - } - - public static void save(KeyBindings kb) { - try { - Files.createDirectories(FILE.getParent()); - try (Writer w = Files.newBufferedWriter(FILE)) { - GSON.toJson(kb, w); - } - } catch (IOException e) { - System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage()); - } - } -} +package de.blight.game.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.*; +import java.nio.file.*; + +public class KeyBindingStore { + + private static final Path FILE = Paths.get("config", "keybindings.json"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public static KeyBindings load() { + if (Files.exists(FILE)) { + try (Reader r = Files.newBufferedReader(FILE)) { + return GSON.fromJson(r, KeyBindings.class); + } catch (IOException e) { + System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage()); + } + } + return new KeyBindings(); + } + + public static void save(KeyBindings kb) { + try { + Files.createDirectories(FILE.getParent()); + try (Writer w = Files.newBufferedWriter(FILE)) { + GSON.toJson(kb, w); + } + } catch (IOException e) { + System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage()); + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java index 797a201..a64eb2e 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java @@ -1,44 +1,44 @@ -package de.blight.game.config; - -import com.jme3.input.KeyInput; - -/** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */ -public class KeyBindings { - - public int forward = KeyInput.KEY_W; - public int backward = KeyInput.KEY_S; - public int left = KeyInput.KEY_A; - public int right = KeyInput.KEY_D; - public int jump = KeyInput.KEY_SPACE; - public int sprint = KeyInput.KEY_LSHIFT; - - /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */ - public static final String[][] ENTRIES = { - {"forward", "Vorwärts"}, - {"backward", "Rückwärts"}, - {"left", "Links"}, - {"right", "Rechts"}, - {"jump", "Springen"}, - {"sprint", "Rennen"}, - }; - - public int get(String fieldName) { - try { return (int) KeyBindings.class.getField(fieldName).get(this); } - catch (Exception e) { return 0; } - } - - public void set(String fieldName, int keyCode) { - try { KeyBindings.class.getField(fieldName).setInt(this, keyCode); } - catch (Exception ignored) {} - } - - public KeyBindings copy() { - KeyBindings c = new KeyBindings(); - for (String[] e : ENTRIES) c.set(e[0], get(e[0])); - return c; - } - - public void copyFrom(KeyBindings src) { - for (String[] e : ENTRIES) set(e[0], src.get(e[0])); - } -} +package de.blight.game.config; + +import com.jme3.input.KeyInput; + +/** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */ +public class KeyBindings { + + public int forward = KeyInput.KEY_W; + public int backward = KeyInput.KEY_S; + public int left = KeyInput.KEY_A; + public int right = KeyInput.KEY_D; + public int jump = KeyInput.KEY_SPACE; + public int sprint = KeyInput.KEY_LSHIFT; + + /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */ + public static final String[][] ENTRIES = { + {"forward", "Vorwärts"}, + {"backward", "Rückwärts"}, + {"left", "Links"}, + {"right", "Rechts"}, + {"jump", "Springen"}, + {"sprint", "Rennen"}, + }; + + public int get(String fieldName) { + try { return (int) KeyBindings.class.getField(fieldName).get(this); } + catch (Exception e) { return 0; } + } + + public void set(String fieldName, int keyCode) { + try { KeyBindings.class.getField(fieldName).setInt(this, keyCode); } + catch (Exception ignored) {} + } + + public KeyBindings copy() { + KeyBindings c = new KeyBindings(); + for (String[] e : ENTRIES) c.set(e[0], get(e[0])); + return c; + } + + public void copyFrom(KeyBindings src) { + for (String[] e : ENTRIES) set(e[0], src.get(e[0])); + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/KeyNames.java b/blight-game/src/main/java/de/blight/game/config/KeyNames.java index ce48361..ab91299 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyNames.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyNames.java @@ -1,51 +1,51 @@ -package de.blight.game.config; - -import com.jme3.input.KeyInput; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; - -/** Bildet KeyInput-Codes auf lesbare Namen ab (via Reflection auf KeyInput-Konstanten). */ -public final class KeyNames { - - private static final Map NAMES = new HashMap<>(); - - static { - for (Field f : KeyInput.class.getFields()) { - if (f.getName().startsWith("KEY_") && f.getType() == int.class) { - try { - NAMES.put(f.getInt(null), pretty(f.getName().substring(4))); - } catch (IllegalAccessException ignored) {} - } - } - } - - public static String of(int keyCode) { - return NAMES.getOrDefault(keyCode, "Key#" + keyCode); - } - - private static String pretty(String raw) { - // "LSHIFT" → "L-Shift", "SPACE" → "Space", "W" → "W" - return switch (raw) { - case "SPACE" -> "Space"; - case "LSHIFT" -> "L-Shift"; - case "RSHIFT" -> "R-Shift"; - case "LCONTROL"-> "L-Ctrl"; - case "RCONTROL"-> "R-Ctrl"; - case "LMENU" -> "L-Alt"; - case "RMENU" -> "R-Alt"; - case "RETURN" -> "Enter"; - case "BACK" -> "Backspace"; - case "UP" -> "Pfeil-Hoch"; - case "DOWN" -> "Pfeil-Runter"; - case "LEFT" -> "Pfeil-Links"; - case "RIGHT" -> "Pfeil-Rechts"; - default -> raw.length() == 1 ? raw : capitalize(raw); - }; - } - - private static String capitalize(String s) { - return s.isEmpty() ? s : s.charAt(0) + s.substring(1).toLowerCase(); - } -} +package de.blight.game.config; + +import com.jme3.input.KeyInput; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +/** Bildet KeyInput-Codes auf lesbare Namen ab (via Reflection auf KeyInput-Konstanten). */ +public final class KeyNames { + + private static final Map NAMES = new HashMap<>(); + + static { + for (Field f : KeyInput.class.getFields()) { + if (f.getName().startsWith("KEY_") && f.getType() == int.class) { + try { + NAMES.put(f.getInt(null), pretty(f.getName().substring(4))); + } catch (IllegalAccessException ignored) {} + } + } + } + + public static String of(int keyCode) { + return NAMES.getOrDefault(keyCode, "Key#" + keyCode); + } + + private static String pretty(String raw) { + // "LSHIFT" → "L-Shift", "SPACE" → "Space", "W" → "W" + return switch (raw) { + case "SPACE" -> "Space"; + case "LSHIFT" -> "L-Shift"; + case "RSHIFT" -> "R-Shift"; + case "LCONTROL"-> "L-Ctrl"; + case "RCONTROL"-> "R-Ctrl"; + case "LMENU" -> "L-Alt"; + case "RMENU" -> "R-Alt"; + case "RETURN" -> "Enter"; + case "BACK" -> "Backspace"; + case "UP" -> "Pfeil-Hoch"; + case "DOWN" -> "Pfeil-Runter"; + case "LEFT" -> "Pfeil-Links"; + case "RIGHT" -> "Pfeil-Rechts"; + default -> raw.length() == 1 ? raw : capitalize(raw); + }; + } + + private static String capitalize(String s) { + return s.isEmpty() ? s : s.charAt(0) + s.substring(1).toLowerCase(); + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java index d5bce24..6b26941 100644 --- a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java +++ b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java @@ -1,168 +1,168 @@ -package de.blight.game.config; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; - -public class PauseMenu extends BaseAppState { - - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_BTN = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_BTN_DIS = new ColorRGBA(0.12f, 0.12f, 0.18f, 1.00f); - private static final ColorRGBA COL_BTN_QUIT = new ColorRGBA(0.38f, 0.10f, 0.10f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f); - private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f); - - private static final int BTN_GRAFIK = 0; - private static final int BTN_AUDIO = 1; - private static final int BTN_STEUERUNG = 2; - private static final int BTN_BEENDEN = 3; - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - private Node panel; - - private Runnable onGraphics; - private Runnable onControls; - - // [x, y, w, h] per button - private final float[][] btnBounds = new float[4][4]; - - public PauseMenu(Runnable onGraphics, Runnable onControls) { - this.onGraphics = onGraphics; - this.onControls = onControls; - } - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_PauseClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - app.getInputManager().deleteMapping("_PauseClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - panel = new Node("pause-panel"); - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 320, ph = 360; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - BitmapText title = txt("PAUSE", 26, COL_TEXT); - centerText(title, px, py + ph - 48, pw); - panel.attachChild(title); - - String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"}; - boolean[] enabled = {true, false, true, true}; - float bw = 260, bh = 52; - float bx = px + (pw - bw) / 2f; - float startY = py + ph - 112; - float step = 62; - - for (int i = 0; i < 4; i++) { - float by = startY - i * step; - ColorRGBA bgCol = !enabled[i] ? COL_BTN_DIS : (i == BTN_BEENDEN ? COL_BTN_QUIT : COL_BTN); - ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS; - - addQuad(panel, bx, by, bw, bh, bgCol, 0); - - BitmapText lbl = txt(labels[i], 18, txCol); - if (!enabled[i]) { - // Center label in upper portion, show hint below - lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1); - BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB); - hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1); - panel.attachChild(hint); - } else { - centerText(lbl, bx, by + bh - 16, bw); - } - panel.attachChild(lbl); - - btnBounds[i][0] = bx; btnBounds[i][1] = by; - btnBounds[i][2] = bw; btnBounds[i][3] = bh; - } - - guiNode.attachChild(panel); - } - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f c = app.getInputManager().getCursorPosition(); - - for (int i = 0; i < 4; i++) { - if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue; - switch (i) { - case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); } - case BTN_AUDIO -> { /* Bald verfügbar */ } - case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); } - case BTN_BEENDEN -> app.stop(); - } - return; - } - }; - - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText txt(String s, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font, false); - t.setSize(size); t.setColor(color); t.setText(s); - return t; - } - - private void centerText(BitmapText t, float x, float y, float width) { - t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); - } - - private boolean hits(Vector2f p, float x, float y, float w, float h) { - return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; - } -} +package de.blight.game.config; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; + +public class PauseMenu extends BaseAppState { + + private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); + private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); + private static final ColorRGBA COL_BTN = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); + private static final ColorRGBA COL_BTN_DIS = new ColorRGBA(0.12f, 0.12f, 0.18f, 1.00f); + private static final ColorRGBA COL_BTN_QUIT = new ColorRGBA(0.38f, 0.10f, 0.10f, 1.00f); + private static final ColorRGBA COL_TEXT = ColorRGBA.White; + private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f); + private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f); + + private static final int BTN_GRAFIK = 0; + private static final int BTN_AUDIO = 1; + private static final int BTN_STEUERUNG = 2; + private static final int BTN_BEENDEN = 3; + + private SimpleApplication app; + private Node guiNode; + private BitmapFont font; + private Node panel; + + private Runnable onGraphics; + private Runnable onControls; + + // [x, y, w, h] per button + private final float[][] btnBounds = new float[4][4]; + + public PauseMenu(Runnable onGraphics, Runnable onControls) { + this.onGraphics = onGraphics; + this.onControls = onControls; + } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.guiNode = this.app.getGuiNode(); + this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + protected void onEnable() { + buildUI(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, "_PauseClick"); + } + + @Override + protected void onDisable() { + if (panel != null) { guiNode.detachChild(panel); panel = null; } + app.getInputManager().deleteMapping("_PauseClick"); + app.getInputManager().setCursorVisible(false); + } + + @Override protected void cleanup(Application app) {} + + private void buildUI() { + float sw = app.getCamera().getWidth(); + float sh = app.getCamera().getHeight(); + panel = new Node("pause-panel"); + addQuad(panel, 0, 0, sw, sh, COL_BG, -2); + + float pw = 320, ph = 360; + float px = (sw - pw) / 2f, py = (sh - ph) / 2f; + addQuad(panel, px, py, pw, ph, COL_PANEL, -1); + + BitmapText title = txt("PAUSE", 26, COL_TEXT); + centerText(title, px, py + ph - 48, pw); + panel.attachChild(title); + + String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"}; + boolean[] enabled = {true, false, true, true}; + float bw = 260, bh = 52; + float bx = px + (pw - bw) / 2f; + float startY = py + ph - 112; + float step = 62; + + for (int i = 0; i < 4; i++) { + float by = startY - i * step; + ColorRGBA bgCol = !enabled[i] ? COL_BTN_DIS : (i == BTN_BEENDEN ? COL_BTN_QUIT : COL_BTN); + ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS; + + addQuad(panel, bx, by, bw, bh, bgCol, 0); + + BitmapText lbl = txt(labels[i], 18, txCol); + if (!enabled[i]) { + // Center label in upper portion, show hint below + lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1); + BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB); + hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1); + panel.attachChild(hint); + } else { + centerText(lbl, bx, by + bh - 16, bw); + } + panel.attachChild(lbl); + + btnBounds[i][0] = bx; btnBounds[i][1] = by; + btnBounds[i][2] = bw; btnBounds[i][3] = bh; + } + + guiNode.attachChild(panel); + } + + private final ActionListener clickListener = (name, isPressed, tpf) -> { + if (!isPressed) return; + Vector2f c = app.getInputManager().getCursorPosition(); + + for (int i = 0; i < 4; i++) { + if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue; + switch (i) { + case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); } + case BTN_AUDIO -> { /* Bald verfügbar */ } + case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); } + case BTN_BEENDEN -> app.stop(); + } + return; + } + }; + + // ----------------------------------------------------------------------- + + private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { + Geometry geo = new Geometry("q", new Quad(w, h)); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color.clone()); + if (color.a < 1f) { + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + } + geo.setMaterial(mat); + geo.setLocalTranslation(x, y, z); + parent.attachChild(geo); + return geo; + } + + private BitmapText txt(String s, int size, ColorRGBA color) { + BitmapText t = new BitmapText(font, false); + t.setSize(size); t.setColor(color); t.setText(s); + return t; + } + + private void centerText(BitmapText t, float x, float y, float width) { + t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); + } + + private boolean hits(Vector2f p, float x, float y, float w, float h) { + return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; + } +} diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 97c4512..ff48f27 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -1,113 +1,113 @@ -package de.blight.game.control; - -import com.jme3.bullet.control.CharacterControl; -import com.jme3.input.InputManager; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.KeyTrigger; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; -import com.jme3.renderer.Camera; -import com.jme3.scene.Spatial; -import de.blight.game.config.KeyBindings; - -public class PlayerInputControl { - - private static final float MOVE_SPEED = 0.07f; - private static final float SPRINT_MULT = 1.5f; - private static final float ROTATE_SPEED = 10f; - - private static final String[] ACTION_NAMES = - {"Forward", "Backward", "Left", "Right", "Jump", "Sprint"}; - - private final InputManager inputManager; - private final Camera cam; - - private CharacterControl physicsChar; - private Spatial visual; - - private boolean forward, backward, left, right, sprint; - private boolean paused = false; - - // Listener als Feld, damit er bei reload nicht doppelt registriert wird - private final ActionListener actionListener = (name, isPressed, tpf) -> { - if (paused) return; - switch (name) { - case "Forward" -> forward = isPressed; - case "Backward" -> backward = isPressed; - case "Left" -> left = isPressed; - case "Right" -> right = isPressed; - case "Sprint" -> sprint = isPressed; - case "Jump" -> { if (isPressed && physicsChar != null) physicsChar.jump(); } - } - }; - - public PlayerInputControl(InputManager inputManager, Camera cam, KeyBindings kb) { - this.inputManager = inputManager; - this.cam = cam; - registerMappings(kb); - } - - public void setPhysicsCharacter(CharacterControl physicsChar) { - this.physicsChar = physicsChar; - } - - public void setVisual(Spatial visual) { - this.visual = visual; - } - - public void setPaused(boolean paused) { - this.paused = paused; - if (paused) { - forward = backward = left = right = sprint = false; - if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); - } - } - - /** Löscht alte Mappings und registriert neue aus den übergebenen KeyBindings. */ - public void reloadBindings(KeyBindings kb) { - for (String a : ACTION_NAMES) inputManager.deleteMapping(a); - registerMappings(kb); - // Zustand zurücksetzen, damit keine Taste „hängt" - forward = backward = left = right = sprint = false; - if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); - } - - private void registerMappings(KeyBindings kb) { - inputManager.addMapping("Forward", new KeyTrigger(kb.forward)); - inputManager.addMapping("Backward", new KeyTrigger(kb.backward)); - inputManager.addMapping("Left", new KeyTrigger(kb.left)); - inputManager.addMapping("Right", new KeyTrigger(kb.right)); - inputManager.addMapping("Jump", new KeyTrigger(kb.jump)); - inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint)); - inputManager.addListener(actionListener, ACTION_NAMES); - } - - public void update(float tpf) { - if (physicsChar == null || paused) return; - - Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal(); - Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal(); - - Vector3f moveDir = new Vector3f(); - if (forward) moveDir.addLocal(camDir); - if (backward) moveDir.subtractLocal(camDir); - if (left) moveDir.addLocal(camLeft); - if (right) moveDir.subtractLocal(camLeft); - - if (moveDir.lengthSquared() > 0.001f) { - moveDir.normalizeLocal(); - float speed = sprint ? MOVE_SPEED * SPRINT_MULT : MOVE_SPEED; - physicsChar.setWalkDirection(moveDir.mult(speed)); - - if (visual != null) { - Quaternion targetRot = new Quaternion(); - targetRot.lookAt(moveDir, Vector3f.UNIT_Y); - Quaternion current = visual.getLocalRotation().clone(); - current.slerp(targetRot, ROTATE_SPEED * tpf); - visual.setLocalRotation(current); - } - } else { - physicsChar.setWalkDirection(Vector3f.ZERO); - } - } -} +package de.blight.game.control; + +import com.jme3.bullet.control.CharacterControl; +import com.jme3.input.InputManager; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.scene.Spatial; +import de.blight.game.config.KeyBindings; + +public class PlayerInputControl { + + private static final float MOVE_SPEED = 0.07f; + private static final float SPRINT_MULT = 1.5f; + private static final float ROTATE_SPEED = 10f; + + private static final String[] ACTION_NAMES = + {"Forward", "Backward", "Left", "Right", "Jump", "Sprint"}; + + private final InputManager inputManager; + private final Camera cam; + + private CharacterControl physicsChar; + private Spatial visual; + + private boolean forward, backward, left, right, sprint; + private boolean paused = false; + + // Listener als Feld, damit er bei reload nicht doppelt registriert wird + private final ActionListener actionListener = (name, isPressed, tpf) -> { + if (paused) return; + switch (name) { + case "Forward" -> forward = isPressed; + case "Backward" -> backward = isPressed; + case "Left" -> left = isPressed; + case "Right" -> right = isPressed; + case "Sprint" -> sprint = isPressed; + case "Jump" -> { if (isPressed && physicsChar != null) physicsChar.jump(); } + } + }; + + public PlayerInputControl(InputManager inputManager, Camera cam, KeyBindings kb) { + this.inputManager = inputManager; + this.cam = cam; + registerMappings(kb); + } + + public void setPhysicsCharacter(CharacterControl physicsChar) { + this.physicsChar = physicsChar; + } + + public void setVisual(Spatial visual) { + this.visual = visual; + } + + public void setPaused(boolean paused) { + this.paused = paused; + if (paused) { + forward = backward = left = right = sprint = false; + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); + } + } + + /** Löscht alte Mappings und registriert neue aus den übergebenen KeyBindings. */ + public void reloadBindings(KeyBindings kb) { + for (String a : ACTION_NAMES) inputManager.deleteMapping(a); + registerMappings(kb); + // Zustand zurücksetzen, damit keine Taste „hängt" + forward = backward = left = right = sprint = false; + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); + } + + private void registerMappings(KeyBindings kb) { + inputManager.addMapping("Forward", new KeyTrigger(kb.forward)); + inputManager.addMapping("Backward", new KeyTrigger(kb.backward)); + inputManager.addMapping("Left", new KeyTrigger(kb.left)); + inputManager.addMapping("Right", new KeyTrigger(kb.right)); + inputManager.addMapping("Jump", new KeyTrigger(kb.jump)); + inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint)); + inputManager.addListener(actionListener, ACTION_NAMES); + } + + public void update(float tpf) { + if (physicsChar == null || paused) return; + + Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal(); + Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal(); + + Vector3f moveDir = new Vector3f(); + if (forward) moveDir.addLocal(camDir); + if (backward) moveDir.subtractLocal(camDir); + if (left) moveDir.addLocal(camLeft); + if (right) moveDir.subtractLocal(camLeft); + + if (moveDir.lengthSquared() > 0.001f) { + moveDir.normalizeLocal(); + float speed = sprint ? MOVE_SPEED * SPRINT_MULT : MOVE_SPEED; + physicsChar.setWalkDirection(moveDir.mult(speed)); + + if (visual != null) { + Quaternion targetRot = new Quaternion(); + targetRot.lookAt(moveDir, Vector3f.UNIT_Y); + Quaternion current = visual.getLocalRotation().clone(); + current.slerp(targetRot, ROTATE_SPEED * tpf); + visual.setLocalRotation(current); + } + } else { + physicsChar.setWalkDirection(Vector3f.ZERO); + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java index c1610a6..b107c89 100644 --- a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java +++ b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java @@ -1,93 +1,93 @@ -package de.blight.game.control; - -import com.jme3.input.InputManager; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.AnalogListener; -import com.jme3.input.controls.MouseAxisTrigger; -import com.jme3.math.*; -import com.jme3.renderer.Camera; -import com.jme3.scene.Spatial; - -/** - * Third-Person-Kamera: - * - Mausbewegung → Kamera um den Charakter drehen (immer, kein Klick nötig) - * - Y-Achse invertiert → Maus hoch = Kamera runter (oldschool) - * - Mausrad → Zoom (Abstand) - */ -public class ThirdPersonCamera { - - private static final float MOUSE_SENSITIVITY = 1.8f; - private static final float MIN_DISTANCE = 3f; - private static final float MAX_DISTANCE = 20f; - private static final float MIN_VERTICAL_ANGLE = -0.3f; - private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f; - private static final float TARGET_HEIGHT = 1.6f; - - private final Camera cam; - private final InputManager inputManager; - - private Spatial target; - - private float yaw = 0f; - private float pitch = 0.4f; - private float distance = 10f; - private boolean paused = false; - - public ThirdPersonCamera(Camera cam, InputManager inputManager) { - this.cam = cam; - this.inputManager = inputManager; - registerMappings(); - } - - public void setTarget(Spatial target) { - this.target = target; - } - - public void setPaused(boolean paused) { this.paused = paused; } - - // ----------------------------------------------------------------------- - - private void registerMappings() { - inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false)); - inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)); - inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false)); - inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true)); - inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false)); - inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true)); - - AnalogListener analogListener = (name, value, tpf) -> { - if (paused) return; - switch (name) { - // Horizontale Rotation - case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY; - case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY; - // Vertikale Rotation — Y invertiert: Maus hoch → Kamera runter - case "MouseY" -> pitch = FastMath.clamp(pitch - value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE); - case "MouseYNeg" -> pitch = FastMath.clamp(pitch + value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE); - // Zoom - case "ZoomIn" -> distance = FastMath.clamp(distance - value * 20f, MIN_DISTANCE, MAX_DISTANCE); - case "ZoomOut" -> distance = FastMath.clamp(distance + value * 20f, MIN_DISTANCE, MAX_DISTANCE); - } - }; - inputManager.addListener(analogListener, - "MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut"); - } - - public void update(float tpf) { - if (target == null) return; - - Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0); - - // Sphärische Koordinaten → kartesisch - float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw); - float y = distance * FastMath.sin(pitch); - float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw); - - Vector3f camPos = pivot.add(x, y, z); - cam.setLocation(camPos); - cam.lookAt(pivot, Vector3f.UNIT_Y); - } - - /** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */ - public float getYaw() { return yaw; } -} +package de.blight.game.control; + +import com.jme3.input.InputManager; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.AnalogListener; +import com.jme3.input.controls.MouseAxisTrigger; +import com.jme3.math.*; +import com.jme3.renderer.Camera; +import com.jme3.scene.Spatial; + +/** + * Third-Person-Kamera: + * - Mausbewegung → Kamera um den Charakter drehen (immer, kein Klick nötig) + * - Y-Achse invertiert → Maus hoch = Kamera runter (oldschool) + * - Mausrad → Zoom (Abstand) + */ +public class ThirdPersonCamera { + + private static final float MOUSE_SENSITIVITY = 1.8f; + private static final float MIN_DISTANCE = 3f; + private static final float MAX_DISTANCE = 20f; + private static final float MIN_VERTICAL_ANGLE = -0.3f; + private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f; + private static final float TARGET_HEIGHT = 1.6f; + + private final Camera cam; + private final InputManager inputManager; + + private Spatial target; + + private float yaw = 0f; + private float pitch = 0.4f; + private float distance = 10f; + private boolean paused = false; + + public ThirdPersonCamera(Camera cam, InputManager inputManager) { + this.cam = cam; + this.inputManager = inputManager; + registerMappings(); + } + + public void setTarget(Spatial target) { + this.target = target; + } + + public void setPaused(boolean paused) { this.paused = paused; } + + // ----------------------------------------------------------------------- + + private void registerMappings() { + inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false)); + inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)); + inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false)); + inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true)); + inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false)); + inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true)); + + AnalogListener analogListener = (name, value, tpf) -> { + if (paused) return; + switch (name) { + // Horizontale Rotation + case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY; + case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY; + // Vertikale Rotation — Y invertiert: Maus hoch → Kamera runter + case "MouseY" -> pitch = FastMath.clamp(pitch - value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE); + case "MouseYNeg" -> pitch = FastMath.clamp(pitch + value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE); + // Zoom + case "ZoomIn" -> distance = FastMath.clamp(distance - value * 20f, MIN_DISTANCE, MAX_DISTANCE); + case "ZoomOut" -> distance = FastMath.clamp(distance + value * 20f, MIN_DISTANCE, MAX_DISTANCE); + } + }; + inputManager.addListener(analogListener, + "MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut"); + } + + public void update(float tpf) { + if (target == null) return; + + Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0); + + // Sphärische Koordinaten → kartesisch + float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw); + float y = distance * FastMath.sin(pitch); + float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw); + + Vector3f camPos = pivot.add(x, y, z); + cam.setLocation(camPos); + cam.lookAt(pivot, Vector3f.UNIT_Y); + } + + /** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */ + public float getYaw() { return yaw; } +} diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 037fffd..5ab13dd 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -1,308 +1,388 @@ -package de.blight.game.scene; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.asset.AssetManager; -import com.jme3.bullet.BulletAppState; -import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; -import com.jme3.bullet.control.CharacterControl; -import com.jme3.bullet.control.RigidBodyControl; -import com.jme3.bullet.util.CollisionShapeFactory; -import com.jme3.light.*; -import com.jme3.material.Material; -import com.jme3.math.*; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.*; -import com.jme3.scene.shape.*; -import com.jme3.shadow.*; -import com.jme3.terrain.geomipmap.*; -import com.jme3.texture.*; -import com.jme3.util.SkyFactory; -import de.blight.game.config.KeyBindings; -import de.blight.game.control.PlayerInputControl; -import de.blight.game.control.ThirdPersonCamera; - -public class WorldScene extends BaseAppState { - - private SimpleApplication app; - private Node rootNode; - private AssetManager assetManager; - private BulletAppState bulletAppState; - - private final KeyBindings keyBindings; - private ThirdPersonCamera thirdPersonCam; - private PlayerInputControl playerInput; - - public WorldScene(KeyBindings keyBindings) { - this.keyBindings = keyBindings; - } - - /** Wird von ConfigScreen nach dem Speichern aufgerufen. */ - public void reloadBindings(KeyBindings kb) { - if (playerInput != null) playerInput.reloadBindings(kb); - } - - public void setPaused(boolean paused) { - if (playerInput != null) playerInput.setPaused(paused); - if (thirdPersonCam != null) thirdPersonCam.setPaused(paused); - } - - // ----------------------------------------------------------------------- - // Lifecycle - // ----------------------------------------------------------------------- - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.rootNode = this.app.getRootNode(); - this.assetManager = app.getAssetManager(); - - bulletAppState = new BulletAppState(); - app.getStateManager().attach(bulletAppState); - } - - @Override - protected void onEnable() { - buildLighting(); - TerrainQuad terrain = buildTerrain(); - buildDecorations(terrain); - - Node character = buildCharacter(); - rootNode.attachChild(character); - - // Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1) - CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1); - CharacterControl physicsChar = new CharacterControl(capsule, 0.05f); - physicsChar.setJumpSpeed(12f); - physicsChar.setFallSpeed(35f); - physicsChar.setGravity(35f); - physicsChar.setPhysicsLocation(new Vector3f(0, 5f, 0)); - character.addControl(physicsChar); - bulletAppState.getPhysicsSpace().add(physicsChar); - - playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings); - playerInput.setPhysicsCharacter(physicsChar); - playerInput.setVisual(character); - - thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager()); - thirdPersonCam.setTarget(character); - - // Maus einfangen – keine Klick-Pflicht für Kamerasteuerung - app.getInputManager().setCursorVisible(false); - } - - @Override - public void update(float tpf) { - playerInput.update(tpf); - thirdPersonCam.update(tpf); - } - - @Override protected void cleanup(Application app) {} - @Override protected void onDisable() {} - - // ----------------------------------------------------------------------- - // Beleuchtung - // ----------------------------------------------------------------------- - - private void buildLighting() { - DirectionalLight sun = new DirectionalLight(); - sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal()); - sun.setColor(ColorRGBA.White.mult(1.4f)); - rootNode.addLight(sun); - - AmbientLight ambient = new AmbientLight(); - ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f)); - rootNode.addLight(ambient); - - DirectionalLightShadowRenderer shadowRenderer = - new DirectionalLightShadowRenderer(assetManager, 2048, 3); - shadowRenderer.setLight(sun); - shadowRenderer.setShadowIntensity(0.4f); - app.getViewPort().addProcessor(shadowRenderer); - - try { - Spatial sky = SkyFactory.createSky(assetManager, - "Textures/Sky/Bright/BrightSky.dds", - SkyFactory.EnvMapType.CubeMap); - rootNode.attachChild(sky); - } catch (Exception ignored) {} - } - - // ----------------------------------------------------------------------- - // Terrain - // ----------------------------------------------------------------------- - - private TerrainQuad buildTerrain() { - int size = 257; - float[] heights = new float[size * size]; - - for (int z = 0; z < size; z++) { - for (int x = 0; x < size; x++) { - float nx = x / (float) size; - float nz = z / (float) size; - heights[z * size + x] = - FastMath.sin(nx * FastMath.TWO_PI * 2) * 2f - + FastMath.sin(nz * FastMath.TWO_PI * 3) * 1.5f - + FastMath.sin((nx + nz) * FastMath.TWO_PI * 1.5f) * 1f; - } - } - - TerrainQuad terrain = new TerrainQuad("terrain", 65, size, heights); - - Material terrainMat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); - Texture grass = assetManager.loadTexture("Textures/gras.png"); - grass.setWrap(Texture.WrapMode.Repeat); - terrainMat.setTexture("Tex1", grass); - terrainMat.setFloat("Tex1Scale", 64f); - - terrain.setMaterial(terrainMat); - terrain.setLocalTranslation(0, -5f, 0); - terrain.setLocalScale(0.5f, 0.5f, 0.5f); - terrain.setShadowMode(RenderQueue.ShadowMode.Receive); - - // Statischer Physics-Body (mass=0) für Terrain-Kollision - RigidBodyControl terrainPhysics = new RigidBodyControl( - CollisionShapeFactory.createMeshShape(terrain), 0f); - terrain.addControl(terrainPhysics); - bulletAppState.getPhysicsSpace().add(terrainPhysics); - - rootNode.attachChild(terrain); - return terrain; - } - - // ----------------------------------------------------------------------- - // Dekorationen – Höhe per TerrainQuad.getHeight() anpassen - // ----------------------------------------------------------------------- - - private void buildDecorations(TerrainQuad terrain) { - Material stoneMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); - stoneMat.setBoolean("UseMaterialColors", true); - stoneMat.setColor("Diffuse", new ColorRGBA(0.55f, 0.55f, 0.55f, 1f)); - stoneMat.setColor("Ambient", new ColorRGBA(0.3f, 0.3f, 0.3f, 1f)); - stoneMat.setColor("Specular", ColorRGBA.White); - stoneMat.setFloat("Shininess", 32f); - - Material treeTrunkMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); - treeTrunkMat.setBoolean("UseMaterialColors", true); - treeTrunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.28f, 0.1f, 1f)); - treeTrunkMat.setColor("Ambient", new ColorRGBA(0.2f, 0.12f, 0.04f, 1f)); - treeTrunkMat.setColor("Specular", ColorRGBA.Black); - - Material treeTopMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); - treeTopMat.setBoolean("UseMaterialColors", true); - treeTopMat.setColor("Diffuse", new ColorRGBA(0.1f, 0.55f, 0.15f, 1f)); - treeTopMat.setColor("Ambient", new ColorRGBA(0.05f, 0.25f, 0.07f, 1f)); - treeTopMat.setColor("Specular", ColorRGBA.Black); - - float[][] treeXZ = { - {12, 8}, {-15, 5}, {20, -10}, {-8, -18}, - {5, 25}, {-22, 12}, {18, 20}, {-10, -5} - }; - for (float[] xz : treeXZ) { - float worldY = terrainWorldY(terrain, xz[0], xz[1]); - Node tree = new Node("tree"); - - Geometry trunk = new Geometry("trunk", new Cylinder(8, 8, 0.25f, 2.5f, true)); - trunk.setMaterial(treeTrunkMat); - trunk.rotate(FastMath.HALF_PI, 0, 0); - trunk.setLocalTranslation(0, 1.25f, 0); - trunk.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); - - Geometry crown = new Geometry("crown", new Sphere(12, 12, 2.2f)); - crown.setMaterial(treeTopMat); - crown.setLocalTranslation(0, 3.8f, 0); - crown.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); - - tree.attachChild(trunk); - tree.attachChild(crown); - tree.setLocalTranslation(xz[0], worldY, xz[1]); - rootNode.attachChild(tree); - } - - float[][] stoneXZ = {{6, -6}, {-12, 15}, {16, -4}, {-3, 10}}; - for (float[] xz : stoneXZ) { - float worldY = terrainWorldY(terrain, xz[0], xz[1]); - float r = 0.6f + FastMath.nextRandomFloat() * 0.8f; - Geometry stone = new Geometry("stone", new Sphere(8, 8, r)); - stone.setMaterial(stoneMat); - stone.setLocalTranslation(xz[0], worldY + r * 0.5f, xz[1]); - stone.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); - rootNode.attachChild(stone); - } - } - - /** Konvertiert Welt-XZ in lokale Terrain-Koordinaten und fragt die Höhe ab. */ - private float terrainWorldY(TerrainQuad terrain, float worldX, float worldZ) { - Vector3f terrainTranslation = terrain.getWorldTranslation(); - Vector3f terrainScale = terrain.getWorldScale(); - float localX = (worldX - terrainTranslation.x) / terrainScale.x; - float localZ = (worldZ - terrainTranslation.z) / terrainScale.z; - float localH = terrain.getHeight(new Vector2f(localX, localZ)); - return terrainTranslation.y + localH * terrainScale.y; - } - - // ----------------------------------------------------------------------- - // Charakter (visueller Node, ohne eigenes Mesh für Physics) - // ----------------------------------------------------------------------- - - private Node buildCharacter() { - Node character = new Node("character"); - - Material bodyMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); - bodyMat.setBoolean("UseMaterialColors", true); - bodyMat.setColor("Diffuse", new ColorRGBA(0.2f, 0.4f, 0.8f, 1f)); - bodyMat.setColor("Ambient", new ColorRGBA(0.1f, 0.2f, 0.4f, 1f)); - bodyMat.setColor("Specular", ColorRGBA.White); - bodyMat.setFloat("Shininess", 64f); - - Material headMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); - headMat.setBoolean("UseMaterialColors", true); - headMat.setColor("Diffuse", new ColorRGBA(0.9f, 0.75f, 0.6f, 1f)); - headMat.setColor("Ambient", new ColorRGBA(0.45f, 0.37f, 0.3f, 1f)); - headMat.setColor("Specular", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f)); - headMat.setFloat("Shininess", 16f); - - Geometry body = new Geometry("body", new Cylinder(8, 16, 0.35f, 1.1f, true)); - body.setMaterial(bodyMat); - body.rotate(FastMath.HALF_PI, 0, 0); - body.setLocalTranslation(0, 0.9f, 0); - body.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); - - Geometry head = new Geometry("head", new Sphere(16, 16, 0.28f)); - head.setMaterial(headMat); - head.setLocalTranslation(0, 1.75f, 0); - head.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); - - Geometry armL = buildLimb(bodyMat, 0.1f, 0.55f); - armL.setLocalTranslation(-0.5f, 0.85f, 0); - armL.rotate(0, 0, FastMath.DEG_TO_RAD * 20f); - - Geometry armR = buildLimb(bodyMat, 0.1f, 0.55f); - armR.setLocalTranslation(0.5f, 0.85f, 0); - armR.rotate(0, 0, FastMath.DEG_TO_RAD * -20f); - - Geometry legL = buildLimb(bodyMat, 0.12f, 0.6f); - legL.setLocalTranslation(-0.18f, 0.3f, 0); - - Geometry legR = buildLimb(bodyMat, 0.12f, 0.6f); - legR.setLocalTranslation(0.18f, 0.3f, 0); - - character.attachChild(body); - character.attachChild(head); - character.attachChild(armL); - character.attachChild(armR); - character.attachChild(legL); - character.attachChild(legR); - - return character; - } - - private Geometry buildLimb(Material mat, float radius, float height) { - Geometry limb = new Geometry("limb", new Cylinder(6, 12, radius, height, true)); - limb.setMaterial(mat); - limb.rotate(FastMath.HALF_PI, 0, 0); - limb.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); - return limb; - } -} +package de.blight.game.scene; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.bullet.BulletAppState; +import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; +import com.jme3.bullet.control.CharacterControl; +import com.jme3.bullet.control.RigidBodyControl; +import com.jme3.bullet.util.CollisionShapeFactory; +import com.jme3.light.*; +import com.jme3.material.Material; +import com.jme3.math.*; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.*; +import com.jme3.scene.shape.*; +import com.jme3.shadow.*; +import com.jme3.terrain.geomipmap.*; +import com.jme3.texture.*; +import com.jme3.util.SkyFactory; +import de.blight.common.MapData; +import de.blight.common.MapIO; +import de.blight.game.config.KeyBindings; +import de.blight.game.control.PlayerInputControl; +import de.blight.game.control.ThirdPersonCamera; +import de.blight.game.state.GrassState; + +import java.io.IOException; + +public class WorldScene extends BaseAppState { + + private SimpleApplication app; + private Node rootNode; + private AssetManager assetManager; + private BulletAppState bulletAppState; + private MapData loadedMapData; + + private final KeyBindings keyBindings; + private ThirdPersonCamera thirdPersonCam; + private PlayerInputControl playerInput; + private float spawnY = 5f; // wird in buildTerrain() gesetzt + + public WorldScene(KeyBindings keyBindings) { + this.keyBindings = keyBindings; + } + + /** Wird von ConfigScreen nach dem Speichern aufgerufen. */ + public void reloadBindings(KeyBindings kb) { + if (playerInput != null) playerInput.reloadBindings(kb); + } + + public void setPaused(boolean paused) { + if (playerInput != null) playerInput.setPaused(paused); + if (thirdPersonCam != null) thirdPersonCam.setPaused(paused); + } + + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.rootNode = this.app.getRootNode(); + this.assetManager = app.getAssetManager(); + + bulletAppState = new BulletAppState(); + app.getStateManager().attach(bulletAppState); + } + + @Override + protected void onEnable() { + buildLighting(); + TerrainQuad terrain = buildTerrain(); + buildDecorations(terrain); + + if (loadedMapData != null) { + app.getStateManager().attach(new GrassState(loadedMapData, terrain)); + } + + Node character = buildCharacter(); + rootNode.attachChild(character); + + // Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1) + CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1); + CharacterControl physicsChar = new CharacterControl(capsule, 0.05f); + physicsChar.setJumpSpeed(12f); + physicsChar.setFallSpeed(35f); + physicsChar.setGravity(35f); + physicsChar.setPhysicsLocation(new Vector3f(0, spawnY, 0)); + character.addControl(physicsChar); + bulletAppState.getPhysicsSpace().add(physicsChar); + + playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings); + playerInput.setPhysicsCharacter(physicsChar); + playerInput.setVisual(character); + + thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager()); + thirdPersonCam.setTarget(character); + + // Maus einfangen – keine Klick-Pflicht für Kamerasteuerung + app.getInputManager().setCursorVisible(false); + } + + @Override + public void update(float tpf) { + playerInput.update(tpf); + thirdPersonCam.update(tpf); + } + + @Override protected void cleanup(Application app) {} + @Override protected void onDisable() {} + + // ----------------------------------------------------------------------- + // Beleuchtung + // ----------------------------------------------------------------------- + + private void buildLighting() { + DirectionalLight sun = new DirectionalLight(); + sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal()); + sun.setColor(ColorRGBA.White.mult(1.4f)); + rootNode.addLight(sun); + + AmbientLight ambient = new AmbientLight(); + ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f)); + rootNode.addLight(ambient); + + DirectionalLightShadowRenderer shadowRenderer = + new DirectionalLightShadowRenderer(assetManager, 2048, 3); + shadowRenderer.setLight(sun); + shadowRenderer.setShadowIntensity(0.4f); + app.getViewPort().addProcessor(shadowRenderer); + + try { + Spatial sky = SkyFactory.createSky(assetManager, + "Textures/Sky/Bright/BrightSky.dds", + SkyFactory.EnvMapType.CubeMap); + rootNode.attachChild(sky); + } catch (Exception ignored) {} + } + + // ----------------------------------------------------------------------- + // Terrain + // ----------------------------------------------------------------------- + + /** + * Baut das Terrain. Falls eine gespeicherte Karte vorhanden ist, wird diese + * geladen; andernfalls wird ein prozedurales Demo-Terrain erzeugt. + * Setzt außerdem {@link #spawnY}. + */ + private TerrainQuad buildTerrain() { + if (MapIO.exists()) { + try { + loadedMapData = MapIO.load(); + return buildTerrainFromMap(loadedMapData); + } catch (IOException e) { + System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage() + + " — Fallback auf prozedurales Terrain."); + } + } + return buildProceduralTerrain(); + } + + /** + * Erstellt ein Terrain aus der gespeicherten {@link MapData}. + * Die 4097×4097 Editor-Daten werden auf 513×513 heruntergesampelt + * (jeder 8. Vertex), mit Scale (8, 1, 8) auf die gleiche Weltgröße + * 4096 × 4096 WE gebracht. + */ + private TerrainQuad buildTerrainFromMap(MapData map) { + final int GAME_VERTS = 513; // 512 Zellen à 8 WE = 4096 WE + final int STEP = 8; // 4096 / 512 = 8 Vertices überspringen + final int SRC_VERTS = MapData.TERRAIN_VERTS; // 4097 + + float[] heights = new float[GAME_VERTS * GAME_VERTS]; + for (int gz = 0; gz < GAME_VERTS; gz++) { + int sz = gz * STEP; + for (int gx = 0; gx < GAME_VERTS; gx++) { + heights[gz * GAME_VERTS + gx] = map.terrainHeight[sz * SRC_VERTS + gx * STEP]; + } + } + + // Höhe in der Weltmitte als Spawn-Grundlage + float centerHeight = heights[(GAME_VERTS / 2) * GAME_VERTS + (GAME_VERTS / 2)]; + spawnY = centerHeight + 3f; + + TerrainQuad terrain = new TerrainQuad("terrain", 65, GAME_VERTS, heights); + terrain.setLocalScale(8f, 1f, 8f); // 512 Zellen * 8 WE = 4096 WE pro Achse + terrain.setShadowMode(RenderQueue.ShadowMode.Receive); + + applyTerrainMaterial(terrain, 32f); + + RigidBodyControl terrainPhysics = new RigidBodyControl( + CollisionShapeFactory.createMeshShape(terrain), 0f); + terrain.addControl(terrainPhysics); + bulletAppState.getPhysicsSpace().add(terrainPhysics); + + rootNode.attachChild(terrain); + System.out.println("[WorldScene] Karte geladen, Spawn Y=" + spawnY); + return terrain; + } + + /** Prozedurales Demo-Terrain als Fallback (keine gespeicherte Karte). */ + private TerrainQuad buildProceduralTerrain() { + int size = 257; + float[] heights = new float[size * size]; + for (int z = 0; z < size; z++) { + for (int x = 0; x < size; x++) { + float nx = x / (float) size; + float nz = z / (float) size; + heights[z * size + x] = + FastMath.sin(nx * FastMath.TWO_PI * 2) * 2f + + FastMath.sin(nz * FastMath.TWO_PI * 3) * 1.5f + + FastMath.sin((nx + nz) * FastMath.TWO_PI * 1.5f) * 1f; + } + } + + spawnY = 5f; + + TerrainQuad terrain = new TerrainQuad("terrain", 65, size, heights); + terrain.setLocalTranslation(0, -5f, 0); + terrain.setLocalScale(0.5f, 0.5f, 0.5f); + terrain.setShadowMode(RenderQueue.ShadowMode.Receive); + + applyTerrainMaterial(terrain, 64f); + + RigidBodyControl terrainPhysics = new RigidBodyControl( + CollisionShapeFactory.createMeshShape(terrain), 0f); + terrain.addControl(terrainPhysics); + bulletAppState.getPhysicsSpace().add(terrainPhysics); + + rootNode.attachChild(terrain); + return terrain; + } + + private void applyTerrainMaterial(TerrainQuad terrain, float texScale) { + Material mat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); + try { + Texture grass = assetManager.loadTexture("Textures/gras.png"); + grass.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("Tex1", grass); + mat.setFloat("Tex1Scale", texScale); + } catch (Exception e) { + // Fallback: einfarbiges Material + mat = new Material(assetManager, "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.15f, 0.30f, 0.09f, 1f)); + } + terrain.setMaterial(mat); + } + + // ----------------------------------------------------------------------- + // Dekorationen – Höhe per TerrainQuad.getHeight() anpassen + // ----------------------------------------------------------------------- + + private void buildDecorations(TerrainQuad terrain) { + Material stoneMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + stoneMat.setBoolean("UseMaterialColors", true); + stoneMat.setColor("Diffuse", new ColorRGBA(0.55f, 0.55f, 0.55f, 1f)); + stoneMat.setColor("Ambient", new ColorRGBA(0.3f, 0.3f, 0.3f, 1f)); + stoneMat.setColor("Specular", ColorRGBA.White); + stoneMat.setFloat("Shininess", 32f); + + Material treeTrunkMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + treeTrunkMat.setBoolean("UseMaterialColors", true); + treeTrunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.28f, 0.1f, 1f)); + treeTrunkMat.setColor("Ambient", new ColorRGBA(0.2f, 0.12f, 0.04f, 1f)); + treeTrunkMat.setColor("Specular", ColorRGBA.Black); + + Material treeTopMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + treeTopMat.setBoolean("UseMaterialColors", true); + treeTopMat.setColor("Diffuse", new ColorRGBA(0.1f, 0.55f, 0.15f, 1f)); + treeTopMat.setColor("Ambient", new ColorRGBA(0.05f, 0.25f, 0.07f, 1f)); + treeTopMat.setColor("Specular", ColorRGBA.Black); + + float[][] treeXZ = { + {12, 8}, {-15, 5}, {20, -10}, {-8, -18}, + {5, 25}, {-22, 12}, {18, 20}, {-10, -5} + }; + for (float[] xz : treeXZ) { + float worldY = terrainWorldY(terrain, xz[0], xz[1]); + Node tree = new Node("tree"); + + Geometry trunk = new Geometry("trunk", new Cylinder(8, 8, 0.25f, 2.5f, true)); + trunk.setMaterial(treeTrunkMat); + trunk.rotate(FastMath.HALF_PI, 0, 0); + trunk.setLocalTranslation(0, 1.25f, 0); + trunk.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + + Geometry crown = new Geometry("crown", new Sphere(12, 12, 2.2f)); + crown.setMaterial(treeTopMat); + crown.setLocalTranslation(0, 3.8f, 0); + crown.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + + tree.attachChild(trunk); + tree.attachChild(crown); + tree.setLocalTranslation(xz[0], worldY, xz[1]); + rootNode.attachChild(tree); + } + + float[][] stoneXZ = {{6, -6}, {-12, 15}, {16, -4}, {-3, 10}}; + for (float[] xz : stoneXZ) { + float worldY = terrainWorldY(terrain, xz[0], xz[1]); + float r = 0.6f + FastMath.nextRandomFloat() * 0.8f; + Geometry stone = new Geometry("stone", new Sphere(8, 8, r)); + stone.setMaterial(stoneMat); + stone.setLocalTranslation(xz[0], worldY + r * 0.5f, xz[1]); + stone.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + rootNode.attachChild(stone); + } + } + + /** Konvertiert Welt-XZ in lokale Terrain-Koordinaten und fragt die Höhe ab. */ + private float terrainWorldY(TerrainQuad terrain, float worldX, float worldZ) { + Vector3f terrainTranslation = terrain.getWorldTranslation(); + Vector3f terrainScale = terrain.getWorldScale(); + float localX = (worldX - terrainTranslation.x) / terrainScale.x; + float localZ = (worldZ - terrainTranslation.z) / terrainScale.z; + float localH = terrain.getHeight(new Vector2f(localX, localZ)); + return terrainTranslation.y + localH * terrainScale.y; + } + + // ----------------------------------------------------------------------- + // Charakter (visueller Node, ohne eigenes Mesh für Physics) + // ----------------------------------------------------------------------- + + private Node buildCharacter() { + Node character = new Node("character"); + + Material bodyMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + bodyMat.setBoolean("UseMaterialColors", true); + bodyMat.setColor("Diffuse", new ColorRGBA(0.2f, 0.4f, 0.8f, 1f)); + bodyMat.setColor("Ambient", new ColorRGBA(0.1f, 0.2f, 0.4f, 1f)); + bodyMat.setColor("Specular", ColorRGBA.White); + bodyMat.setFloat("Shininess", 64f); + + Material headMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); + headMat.setBoolean("UseMaterialColors", true); + headMat.setColor("Diffuse", new ColorRGBA(0.9f, 0.75f, 0.6f, 1f)); + headMat.setColor("Ambient", new ColorRGBA(0.45f, 0.37f, 0.3f, 1f)); + headMat.setColor("Specular", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f)); + headMat.setFloat("Shininess", 16f); + + Geometry body = new Geometry("body", new Cylinder(8, 16, 0.35f, 1.1f, true)); + body.setMaterial(bodyMat); + body.rotate(FastMath.HALF_PI, 0, 0); + body.setLocalTranslation(0, 0.9f, 0); + body.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + + Geometry head = new Geometry("head", new Sphere(16, 16, 0.28f)); + head.setMaterial(headMat); + head.setLocalTranslation(0, 1.75f, 0); + head.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + + Geometry armL = buildLimb(bodyMat, 0.1f, 0.55f); + armL.setLocalTranslation(-0.5f, 0.85f, 0); + armL.rotate(0, 0, FastMath.DEG_TO_RAD * 20f); + + Geometry armR = buildLimb(bodyMat, 0.1f, 0.55f); + armR.setLocalTranslation(0.5f, 0.85f, 0); + armR.rotate(0, 0, FastMath.DEG_TO_RAD * -20f); + + Geometry legL = buildLimb(bodyMat, 0.12f, 0.6f); + legL.setLocalTranslation(-0.18f, 0.3f, 0); + + Geometry legR = buildLimb(bodyMat, 0.12f, 0.6f); + legR.setLocalTranslation(0.18f, 0.3f, 0); + + character.attachChild(body); + character.attachChild(head); + character.attachChild(armL); + character.attachChild(armR); + character.attachChild(legL); + character.attachChild(legR); + + return character; + } + + private Geometry buildLimb(Material mat, float radius, float height) { + Geometry limb = new Geometry("limb", new Cylinder(6, 12, radius, height, true)); + limb.setMaterial(mat); + limb.rotate(FastMath.HALF_PI, 0, 0); + limb.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + return limb; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/GrassState.java b/blight-game/src/main/java/de/blight/game/state/GrassState.java new file mode 100644 index 0000000..887aadc --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/GrassState.java @@ -0,0 +1,221 @@ +package de.blight.game.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.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 java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.*; + +/** + * Rendert Gras im Spiel aus der in MapData gespeicherten Dichte-Map. + * + * Chunks werden gestreckt über mehrere Frames aufgebaut (INIT_PER_FRAME), + * um Startlags zu vermeiden. GrassVisibilityControl cullt entfernte Chunks. + */ +public class GrassState extends BaseAppState { + + // ── Konstanten (identisch mit PlacedObjectState im Editor) ──────────────── + private static final int TERRAIN_HALF = 2048; + private static final float WORLD_SIZE = 4096f; + private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; + private static final float SPLAT_WE_PER_PX = WORLD_SIZE / (SPLAT_SIZE - 1); + private static final int CHUNK_SIZE = 128; + private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE; + private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS; + private static final int MAX_BLADES_PER_PX = 3; + private static final float BLADE_WIDTH = 0.18f; + private static final float DEFAULT_HEIGHT = 1.5f; + private static final float FAR_DIST = 150f; // WE (game terrain is 1:1 WE) + private static final float FAR_DIST_SQ = FAR_DIST * FAR_DIST; + private static final int INIT_PER_FRAME = 4; + + // ── Abhängigkeiten ──────────────────────────────────────────────────────── + private final MapData mapData; + private final TerrainQuad terrain; + + // ── Runtime-Zustand ─────────────────────────────────────────────────────── + private Camera cam; + private Node grassNode; + private Material grassMat; + private int nextChunk = 0; + + public GrassState(MapData mapData, TerrainQuad terrain) { + this.mapData = mapData; + this.terrain = terrain; + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + protected void initialize(Application app) { + this.cam = app.getCamera(); + grassNode = new Node("gameGrass"); + ((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) { + int built = 0; + while (nextChunk < CHUNK_COUNT && built < INIT_PER_FRAME) { + buildChunk(nextChunk++); + built++; + } + } + + // ── Material ────────────────────────────────────────────────────────────── + + private Material buildGrassMaterial(AssetManager assets) { + try { + Material mat = new Material(assets, "MatDefs/Grass.j3md"); + mat.setColor("Color", new ColorRGBA(0.28f, 0.72f, 0.18f, 1f)); + mat.setFloat("WindSpeed", 0.5f); + mat.setFloat("WindStrength", 0.14f); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + return mat; + } catch (Exception e) { + System.err.println("[GrassState] Grass.j3md nicht gefunden, Fallback: " + e.getMessage()); + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.25f, 0.65f, 0.15f, 1f)); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + return mat; + } + } + + // ── Chunk aufbauen ──────────────────────────────────────────────────────── + + private void buildChunk(int idx) { + 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; + + 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)); + + List blades = new ArrayList<>(); + Vector3f scale = terrain.getWorldScale(); + Vector3f trans = terrain.getWorldTranslation(); + + for (int pz = pzMin; pz <= pzMax; pz++) { + for (int px = pxMin; px <= pxMax; px++) { + int d = mapData.grassDensity[pz * SPLAT_SIZE + px] & 0xFF; + if (d == 0) continue; + int count = Math.max(1, (int)(d / 255f * MAX_BLADES_PER_PX)); + 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; + // Welt→lokal→Höhe→Welt + float localX = (bx - trans.x) / scale.x; + float localZ = (bz - trans.z) / scale.z; + float th = terrain.getHeight(new Vector2f(localX, localZ)); + if (Float.isNaN(th)) continue; + float worldY = trans.y + th * scale.y; + float h = DEFAULT_HEIGHT * (0.7f + rng.nextFloat() * 0.6f); + blades.add(new float[]{bx, worldY, bz, h}); + } + } + } + + 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("grass_" + idx, mesh); + geo.setMaterial(grassMat); + geo.addControl(new GrassVisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ))); + grassNode.attachChild(geo); + } + + // ── Mesh: Kreuz-Quad mit UV ─────────────────────────────────────────────── + + private static Mesh buildGrassMesh(List 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); + + 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); + + 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 > FAR_DIST_SQ + ? Spatial.CullHint.Always + : Spatial.CullHint.Inherit); + } + + @Override protected void controlRender(RenderManager rm, ViewPort vp) {} + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..efb7ad4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +// ── Gemeinsame Konfiguration für alle Subprojekte ─────────────────────────── + +allprojects { + group = 'de.blight' + version = '0.1.0' +} + +subprojects { + apply plugin: 'java' + + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + compileJava.options.encoding = 'UTF-8' + + repositories { + mavenCentral() + } +} diff --git a/doc/Abschluss Kapitel 1.odt b/doc/Abschluss Kapitel 1.odt new file mode 100644 index 0000000..8a660ff Binary files /dev/null and b/doc/Abschluss Kapitel 1.odt differ diff --git a/blight-data/doc/Option A.odt b/doc/Backlog/Option A.odt similarity index 100% rename from blight-data/doc/Option A.odt rename to doc/Backlog/Option A.odt diff --git a/blight-data/doc/Option B.odt b/doc/Backlog/Option B.odt similarity index 100% rename from blight-data/doc/Option B.odt rename to doc/Backlog/Option B.odt diff --git a/doc/Blight Crawler.odt b/doc/Blight Crawler.odt new file mode 100644 index 0000000..c9f0840 Binary files /dev/null and b/doc/Blight Crawler.odt differ diff --git a/doc/Eingeborene.odt b/doc/Eingeborene.odt new file mode 100644 index 0000000..8551958 Binary files /dev/null and b/doc/Eingeborene.odt differ diff --git a/doc/Klippenbeißer.odt b/doc/Klippenbeißer.odt new file mode 100644 index 0000000..86db564 Binary files /dev/null and b/doc/Klippenbeißer.odt differ diff --git a/blight-data/doc/Plott.odt b/doc/Plott.odt similarity index 100% rename from blight-data/doc/Plott.odt rename to doc/Plott.odt diff --git a/doc/Silas.odt b/doc/Silas.odt new file mode 100644 index 0000000..8b0f4a8 Binary files /dev/null and b/doc/Silas.odt differ diff --git a/doc/Skilltree.odt b/doc/Skilltree.odt new file mode 100644 index 0000000..84157fe Binary files /dev/null and b/doc/Skilltree.odt differ diff --git a/ez-tree-jme/assets/MatDefs/Tree.j3md b/ez-tree-jme/assets/MatDefs/Tree.j3md new file mode 100644 index 0000000..25e4729 --- /dev/null +++ b/ez-tree-jme/assets/MatDefs/Tree.j3md @@ -0,0 +1,21 @@ +MaterialDef Tree { + + MaterialParameters { + Color Diffuse (Color) : 0.42 0.26 0.10 1.0 + Float WindStrength : 0.15 + Float WindSpeed : 0.5 + Texture2D BarkMap + Boolean HasBarkMap : false + } + + Technique { + VertexShader GLSL150 : Shaders/Tree.vert + FragmentShader GLSL150 : Shaders/Tree.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + Time + } + } +} diff --git a/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md b/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md new file mode 100644 index 0000000..d57e4bd --- /dev/null +++ b/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md @@ -0,0 +1,83 @@ +MaterialDef TreeLeaf { + + MaterialParameters { + Color Diffuse (Color) : 0.18 0.60 0.10 1.0 + Float WindStrength : 0.30 + Float WindSpeed : 0.7 + Texture2D LeafMap + Boolean HasLeafMap : false + + // Vom Shadow-Renderer befüllt (PostShadow-Pass) — vollständige Liste aus PostShadow.j3md + Int BoundDrawBuffer + Int FilterMode + Boolean HardwareShadows + Texture2D ShadowMap0 + Texture2D ShadowMap1 + Texture2D ShadowMap2 + Texture2D ShadowMap3 + Texture2D ShadowMap4 + Texture2D ShadowMap5 + Float ShadowIntensity : 1.0 + Vector4 Splits + Vector2 FadeInfo + Matrix4 LightViewProjectionMatrix0 + Matrix4 LightViewProjectionMatrix1 + Matrix4 LightViewProjectionMatrix2 + Matrix4 LightViewProjectionMatrix3 + Matrix4 LightViewProjectionMatrix4 + Matrix4 LightViewProjectionMatrix5 + Vector3 LightPos + Vector3 LightDir + Float PCFEdge + Float ShadowMapSize + Boolean BackfaceShadows : false + } + + Technique { + VertexShader GLSL150 : Shaders/Tree.vert + FragmentShader GLSL150 : Shaders/TreeLeaf.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + Time + } + + RenderState { + FaceCull Off + } + } + + Technique PostShadow { + VertexShader GLSL150 : Shaders/LeafPostShadow.vert + FragmentShader GLSL150 : Shaders/LeafPostShadow.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + } + + ForcedRenderState { + Blend Modulate + FaceCull Off + DepthWrite Off + } + } + + Technique PreShadow { + VertexShader GLSL150 : Shaders/LeafPreShadow.vert + FragmentShader GLSL150 : Shaders/LeafPreShadow.frag + + WorldParameters { + WorldViewProjectionMatrix + } + + ForcedRenderState { + FaceCull Off + DepthTest On + DepthWrite On + PolyOffset 5 3 + ColorWrite Off + } + } +} diff --git a/ez-tree-jme/assets/Shaders/LeafPostShadow.frag b/ez-tree-jme/assets/Shaders/LeafPostShadow.frag new file mode 100644 index 0000000..f87574b --- /dev/null +++ b/ez-tree-jme/assets/Shaders/LeafPostShadow.frag @@ -0,0 +1,20 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform sampler2D m_ShadowMap0; +uniform float m_ShadowIntensity; +uniform sampler2D m_LeafMap; +uniform bool m_HasLeafMap; + +in vec4 shadowCoord; +in vec2 texCoord; + +void main() { + // Transparente Blattbereiche empfangen keinen Schatten + if (m_HasLeafMap && texture2D(m_LeafMap, texCoord).a < 0.5) discard; + + vec3 coord = shadowCoord.xyz / shadowCoord.w; + float mapDepth = texture2D(m_ShadowMap0, coord.xy).r; + float lit = (coord.z > mapDepth + 0.001) ? (1.0 - m_ShadowIntensity) : 1.0; + + gl_FragColor = vec4(lit, lit, lit, 1.0); +} diff --git a/ez-tree-jme/assets/Shaders/LeafPostShadow.vert b/ez-tree-jme/assets/Shaders/LeafPostShadow.vert new file mode 100644 index 0000000..5b13bb8 --- /dev/null +++ b/ez-tree-jme/assets/Shaders/LeafPostShadow.vert @@ -0,0 +1,18 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; +uniform mat4 m_LightViewProjectionMatrix0; + +in vec3 inPosition; +in vec2 inTexCoord; + +out vec4 shadowCoord; +out vec2 texCoord; + +void main() { + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); + vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); + shadowCoord = m_LightViewProjectionMatrix0 * worldPos; + texCoord = inTexCoord; +} diff --git a/ez-tree-jme/assets/Shaders/LeafPreShadow.frag b/ez-tree-jme/assets/Shaders/LeafPreShadow.frag new file mode 100644 index 0000000..59a31e7 --- /dev/null +++ b/ez-tree-jme/assets/Shaders/LeafPreShadow.frag @@ -0,0 +1,15 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform sampler2D m_LeafMap; +uniform bool m_HasLeafMap; + +in vec2 texCoord; + +void main() { + if (m_HasLeafMap) { + vec4 tex = texture2D(m_LeafMap, texCoord); + if (tex.a < 0.5) discard; + } + // Nur Tiefe schreiben — ColorWrite ist per ForcedRenderState deaktiviert + gl_FragColor = vec4(1.0); +} diff --git a/ez-tree-jme/assets/Shaders/LeafPreShadow.vert b/ez-tree-jme/assets/Shaders/LeafPreShadow.vert new file mode 100644 index 0000000..8b60506 --- /dev/null +++ b/ez-tree-jme/assets/Shaders/LeafPreShadow.vert @@ -0,0 +1,13 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform mat4 g_WorldViewProjectionMatrix; + +in vec3 inPosition; +in vec2 inTexCoord; + +out vec2 texCoord; + +void main() { + gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0); + texCoord = inTexCoord; +} diff --git a/ez-tree-jme/assets/Shaders/Tree.frag b/ez-tree-jme/assets/Shaders/Tree.frag new file mode 100644 index 0000000..4232df7 --- /dev/null +++ b/ez-tree-jme/assets/Shaders/Tree.frag @@ -0,0 +1,29 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform vec4 m_Diffuse; +uniform sampler2D m_BarkMap; +uniform bool m_HasBarkMap; + +in vec2 texCoord; +in vec3 worldNormal; + +void main() { + vec3 n = normalize(worldNormal); + + // Sun at ~45° elevation from SE — good contrast on vertical cylinders + vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4)); + vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4)); + vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7)); + + float sun = max(dot(n, sunDir), 0.0); + float fill = max(dot(n, fillDir), 0.0) * 0.22; + float rim = max(dot(n, rimDir), 0.0) * 0.14; + float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; // [0.0, 0.8] + float light = sun * 0.75 + fill + rim + sky * 0.09 + 0.05; + + vec3 baseColor = m_HasBarkMap + ? texture2D(m_BarkMap, texCoord).rgb * m_Diffuse.rgb + : m_Diffuse.rgb; + + gl_FragColor = vec4(baseColor * clamp(light, 0.0, 1.0), m_Diffuse.a); +} diff --git a/ez-tree-jme/assets/Shaders/Tree.vert b/ez-tree-jme/assets/Shaders/Tree.vert new file mode 100644 index 0000000..583272c --- /dev/null +++ b/ez-tree-jme/assets/Shaders/Tree.vert @@ -0,0 +1,33 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldMatrix; +uniform float g_Time; +uniform float m_WindStrength; +uniform float m_WindSpeed; + +in vec3 inPosition; +in vec3 inNormal; +in vec2 inTexCoord; +in vec4 inColor; // R = Wind-Gewicht (0 = Wurzel, 1 = Spitze) + +out vec2 texCoord; +out vec3 worldNormal; + +void main() { + float windW = inColor.r; + float t = g_Time * m_WindSpeed; + + // Welt-Position für orts-abhängige Phase (verhindert synchrones Schwingen) + vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0); + float phase = worldPos.x * 0.08 + worldPos.z * 0.06; + + float swayX = sin(t + phase) * windW * m_WindStrength; + float swayZ = cos(t * 0.73 + phase) * windW * m_WindStrength * 0.55; + + vec3 animPos = inPosition + vec3(swayX, 0.0, swayZ); + + gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0); + texCoord = inTexCoord; + worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz); +} diff --git a/ez-tree-jme/assets/Shaders/TreeLeaf.frag b/ez-tree-jme/assets/Shaders/TreeLeaf.frag new file mode 100644 index 0000000..d805691 --- /dev/null +++ b/ez-tree-jme/assets/Shaders/TreeLeaf.frag @@ -0,0 +1,27 @@ +#import "Common/ShaderLib/GLSLCompat.glsllib" + +uniform vec4 m_Diffuse; +uniform sampler2D m_LeafMap; +uniform bool m_HasLeafMap; + +in vec2 texCoord; +in vec3 worldNormal; + +void main() { + vec3 baseColor; + + if (m_HasLeafMap) { + vec4 tex = texture2D(m_LeafMap, texCoord); + if (tex.a < 0.5) discard; + baseColor = tex.rgb * m_Diffuse.rgb; + } else { + // Fallback: kreisförmiger Clip + vec2 uv = texCoord * 2.0 - 1.0; + if (dot(uv, uv) > 0.95) discard; + float edge = 1.0 - dot(uv, uv); + baseColor = m_Diffuse.rgb * (0.7 + 0.3 * edge); + } + + // Leaves transmit light — no directional shading, uniform brightness + gl_FragColor = vec4(baseColor, 1.0); +} diff --git a/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg new file mode 100644 index 0000000..6277b94 Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg differ diff --git a/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg new file mode 100644 index 0000000..b2525e9 Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg differ diff --git a/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg new file mode 100644 index 0000000..15ed21b Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg differ diff --git a/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg new file mode 100644 index 0000000..43ee658 Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg differ diff --git a/ez-tree-jme/assets/Textures/leaves/ash.png b/ez-tree-jme/assets/Textures/leaves/ash.png new file mode 100644 index 0000000..7f7c844 Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/ash.png differ diff --git a/ez-tree-jme/assets/Textures/leaves/aspen.png b/ez-tree-jme/assets/Textures/leaves/aspen.png new file mode 100644 index 0000000..89265a9 Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/aspen.png differ diff --git a/ez-tree-jme/assets/Textures/leaves/oak.png b/ez-tree-jme/assets/Textures/leaves/oak.png new file mode 100644 index 0000000..061fbf6 Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/oak.png differ diff --git a/ez-tree-jme/assets/Textures/leaves/palm.png b/ez-tree-jme/assets/Textures/leaves/palm.png new file mode 100644 index 0000000..4c8f4b2 Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/palm.png differ diff --git a/ez-tree-jme/assets/Textures/leaves/pine.png b/ez-tree-jme/assets/Textures/leaves/pine.png new file mode 100644 index 0000000..478dca3 Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/pine.png differ diff --git a/ez-tree-jme/build.gradle b/ez-tree-jme/build.gradle new file mode 100644 index 0000000..e71d133 --- /dev/null +++ b/ez-tree-jme/build.gradle @@ -0,0 +1,15 @@ +ext { + jmeVersion = '3.9.0-stable' +} + +dependencies { + implementation "org.jmonkeyengine:jme3-core:${jmeVersion}" +} + +sourceSets { + main { + resources { + srcDirs = ['src/main/resources', 'assets'] + } + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class new file mode 100644 index 0000000..71c0561 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java new file mode 100644 index 0000000..ca6cfcf --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java @@ -0,0 +1,23 @@ +package de.blight.eztree; + +public final class BarkOptions { + + /** Optional asset path for a bark diffuse texture. */ + public String textureFile = null; + public boolean flatShading = false; + public float textureScaleX = 1f; + public float textureScaleY = 1f; + + /** Base bark color (used when no texture is assigned). */ + public float r = 0.45f, g = 0.30f, b = 0.18f; + + public BarkOptions copy() { + BarkOptions c = new BarkOptions(); + c.textureFile = textureFile; + c.flatShading = flatShading; + c.textureScaleX = textureScaleX; + c.textureScaleY = textureScaleY; + c.r = r; c.g = g; c.b = b; + return c; + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class new file mode 100644 index 0000000..acb1ca3 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java new file mode 100644 index 0000000..8ac7d1f --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java @@ -0,0 +1,5 @@ +package de.blight.eztree; + +public enum Billboard { + NONE, CROSS, ROTATE_X +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java b/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java new file mode 100644 index 0000000..8a8bee5 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java @@ -0,0 +1,18 @@ +package de.blight.eztree; + +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; + +public final class Branch { + + /** World-space start position. */ + public Vector3f origin = new Vector3f(); + + /** Rotation that maps Vector3f.UNIT_Y to the initial branch direction. */ + public Quaternion orientation = new Quaternion(); + + public float length; + public float radius; + public int level; + public Branch parent; +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class new file mode 100644 index 0000000..2150f53 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java new file mode 100644 index 0000000..8901d78 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java @@ -0,0 +1,81 @@ +package de.blight.eztree; + +import java.util.HashMap; +import java.util.Map; + +/** + * All branch parameters, stored as per-level maps (key = branch level). + * When a level is not present the highest defined level at or below is used. + */ +public final class BranchOptions { + + /** Maximum branch depth (0 = trunk only). */ + public int levels = 3; + + /** Spread angle in degrees from parent direction for children at each level. */ + public Map angle = new HashMap<>(); + /** Number of child branches produced by a branch at each level. */ + public Map children = new HashMap<>(); + /** Random direction perturbation magnitude (radians) per section step. */ + public Map gnarliness = new HashMap<>(); + /** Gravitational / wind force applied to branch growth direction. */ + public ForceOptions force = new ForceOptions(); + /** Length multiplier for children (relative to their parent). Level 0 = absolute trunk length. */ + public Map length = new HashMap<>(); + /** Radius multiplier for children (relative to parent section radius). Level 0 = absolute trunk radius. */ + public Map radius = new HashMap<>(); + /** Number of cylinder sections along a branch at each level. */ + public Map sections = new HashMap<>(); + /** Number of polygon sides per cylinder cross-section at each level. */ + public Map segments = new HashMap<>(); + /** Fraction along parent at which children of this level begin to appear. */ + public Map start = new HashMap<>(); + /** Tip-radius / base-radius ratio for each level (0=needle, 1=no taper). */ + public Map taper = new HashMap<>(); + /** Degrees per section by which the ring frame is rotated around the branch axis. */ + public Map twist = new HashMap<>(); + + // ── Accessors with fallback ────────────────────────────────────────────── + + public float getAngle(int lv) { return floatAt(angle, lv, 45f); } + public int getChildren(int lv) { return intAt (children, lv, 0); } + public float getGnarliness(int lv){ return floatAt(gnarliness,lv, 0f); } + public float getLength(int lv) { return floatAt(length, lv, 1f); } + public float getRadius(int lv) { return floatAt(radius, lv, 0.5f);} + public int getSections(int lv) { return intAt (sections, lv, 4); } + public int getSegments(int lv) { return intAt (segments, lv, 6); } + public float getStart(int lv) { return floatAt(start, lv, 0f); } + public float getTaper(int lv) { return floatAt(taper, lv, 0.6f);} + public float getTwist(int lv) { return floatAt(twist, lv, 0f); } + + private float floatAt(Map m, int lv, float def) { + if (m.containsKey(lv)) return m.get(lv); + int best = -1; + for (int k : m.keySet()) if (k <= lv && k > best) best = k; + return best >= 0 ? m.get(best) : def; + } + + private int intAt(Map m, int lv, int def) { + if (m.containsKey(lv)) return m.get(lv); + int best = -1; + for (int k : m.keySet()) if (k <= lv && k > best) best = k; + return best >= 0 ? m.get(best) : def; + } + + public BranchOptions copy() { + BranchOptions c = new BranchOptions(); + c.levels = levels; + c.angle = new HashMap<>(angle); + c.children = new HashMap<>(children); + c.gnarliness = new HashMap<>(gnarliness); + c.force = force.copy(); + c.length = new HashMap<>(length); + c.radius = new HashMap<>(radius); + c.sections = new HashMap<>(sections); + c.segments = new HashMap<>(segments); + c.start = new HashMap<>(start); + c.taper = new HashMap<>(taper); + c.twist = new HashMap<>(twist); + return c; + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java b/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java new file mode 100644 index 0000000..3081f75 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java @@ -0,0 +1,6 @@ +package de.blight.eztree; + +import com.jme3.math.Vector3f; + +/** Immutable snapshot of one section along a branch. */ +record BranchSection(Vector3f pos, float radius, Vector3f dir, float t) {} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class new file mode 100644 index 0000000..e428c35 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java new file mode 100644 index 0000000..6807961 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java @@ -0,0 +1,20 @@ +package de.blight.eztree; + +import com.jme3.math.Vector3f; + +public final class ForceOptions { + + public Vector3f direction = new Vector3f(0f, 0.5f, 0f); + public float strength = 0.02f; + + public ForceOptions() {} + + public ForceOptions(float dx, float dy, float dz, float strength) { + this.direction.set(dx, dy, dz); + this.strength = strength; + } + + public ForceOptions copy() { + return new ForceOptions(direction.x, direction.y, direction.z, strength); + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class new file mode 100644 index 0000000..e294fbc Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java new file mode 100644 index 0000000..3549b63 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java @@ -0,0 +1,30 @@ +package de.blight.eztree; + +public final class LeavesOptions { + + /** Optional asset path for a leaf alpha texture. */ + public String textureFile = null; + public Billboard billboard = Billboard.CROSS; + public int count = 10; + /** Fraction along the last-level branch at which leaf placement begins. */ + public float start = 0.4f; + public float size = 0.5f; + public float sizeVariance = 0.25f; + public float alphaTest = 0.5f; + + /** Base leaf color. */ + public float r = 0.13f, g = 0.53f, b = 0.17f; + + public LeavesOptions copy() { + LeavesOptions c = new LeavesOptions(); + c.textureFile = textureFile; + c.billboard = billboard; + c.count = count; + c.start = start; + c.size = size; + c.sizeVariance = sizeVariance; + c.alphaTest = alphaTest; + c.r = r; c.g = g; c.b = b; + return c; + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java b/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java new file mode 100644 index 0000000..0b51320 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java @@ -0,0 +1,30 @@ +package de.blight.eztree; + +/** Marsaglia's Multiply-With-Carry RNG — matches the ez-tree JS implementation. */ +public final class Rng { + + private int mz = 987654321; + private int mw; + + public Rng(int seed) { + this.mw = (seed == 0) ? 12345 : seed; + } + + /** Returns a value in [0, 1). */ + public float next() { + mz = 36969 * (mz & 0xFFFF) + (mz >>> 16); + mw = 18000 * (mw & 0xFFFF) + (mw >>> 16); + long result = (((long) mz << 16) + (mw & 0xFFFFL)) & 0xFFFFFFFFL; + return (float) (result / 4294967296.0); + } + + /** Returns a value in [min, max). */ + public float range(float min, float max) { + return min + next() * (max - min); + } + + /** Returns an integer in [min, max). */ + public int integer(int min, int max) { + return min + (int) (next() * (max - min)); + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java b/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java new file mode 100644 index 0000000..3a8e55a --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java @@ -0,0 +1,398 @@ +package de.blight.eztree; + +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +/** + * JME3 port of the ez-tree procedural tree generator. + * + * After construction call {@link #generate()} (or re-call it to regenerate). + * The resulting scene graph has two child {@link Geometry} nodes: + *
    + *
  • {@code "bark"} — cylinder mesh with VertexBuffer.Color.R = wind factor
  • + *
  • {@code "leaves"} — quad mesh with VertexBuffer.Color.R = wind factor
  • + *
+ * Materials must be assigned by the caller. + */ +public class Tree extends Node { + + private final TreeOptions opts; + + // Accumulated geometry data, reset on each generate() call + private final List barkPos = new ArrayList<>(); + private final List barkNorm = new ArrayList<>(); + private final List barkUV = new ArrayList<>(); + private final List barkIdx = new ArrayList<>(); + private final List barkWind = new ArrayList<>(); + + private final List leafPos = new ArrayList<>(); + private final List leafNorm = new ArrayList<>(); + private final List leafUV = new ArrayList<>(); + private final List leafIdx = new ArrayList<>(); + private final List leafWind = new ArrayList<>(); + + public Tree(TreeOptions opts) { + super("EzTree"); + this.opts = opts; + } + + public TreeOptions getOptions() { return opts; } + + // ── Public API ─────────────────────────────────────────────────────────── + + /** Clears previous geometry and rebuilds the tree mesh. */ + public void generate() { + detachAllChildren(); + barkPos.clear(); barkNorm.clear(); barkUV.clear(); barkIdx.clear(); barkWind.clear(); + leafPos.clear(); leafNorm.clear(); leafUV.clear(); leafIdx.clear(); leafWind.clear(); + + Rng rng = new Rng(opts.seed); + + Branch trunk = new Branch(); + trunk.level = 0; + trunk.length = opts.branch.getLength(0); + trunk.radius = opts.branch.getRadius(0); + + Queue queue = new ArrayDeque<>(); + queue.add(trunk); + + while (!queue.isEmpty()) { + processBranch(queue.poll(), rng, queue); + } + + if (!barkPos.isEmpty()) { + attachChild(buildGeometry("bark", barkPos, barkNorm, barkUV, barkIdx, barkWind)); + } + if (!leafPos.isEmpty()) { + attachChild(buildGeometry("leaves", leafPos, leafNorm, leafUV, leafIdx, leafWind)); + } + + if (opts.trellis.enabled) { + Node trellisNode = Trellis.build(opts.trellis); + attachChild(trellisNode); + } + } + + // ── Branch processing ──────────────────────────────────────────────────── + + private void processBranch(Branch branch, Rng rng, Queue queue) { + BranchOptions bo = opts.branch; + int level = branch.level; + int sections = bo.getSections(level); + int segments = bo.getSegments(level); + float gnarliness = bo.getGnarliness(level); + float taper = bo.getTaper(level); + float twistStep = bo.getTwist(level) * FastMath.DEG_TO_RAD; + float sectionLen = branch.length / sections; + + float startRad = branch.radius; + float endRad = startRad * taper; + + // Initial direction = orientation applied to Y+ + Vector3f dir = branch.orientation.mult(Vector3f.UNIT_Y).normalizeLocal(); + Vector3f pos = branch.origin.clone(); + + // Parallel-transport frame for smooth cylinder rings + Vector3f frameRight = findPerp(dir); + Vector3f frameUp = dir.cross(frameRight).normalizeLocal(); + + List pts = new ArrayList<>(sections + 1); + pts.add(new BranchSection(pos.clone(), startRad, dir.clone(), 0f)); + + for (int i = 0; i < sections; i++) { + float t = (i + 1f) / sections; + + // Gnarliness: random XZ perturbation of direction + if (gnarliness > 0f) { + dir.x += rng.range(-gnarliness, gnarliness); + dir.z += rng.range(-gnarliness, gnarliness); + dir.normalizeLocal(); + } + + // External force (gravity / wind bias) + Vector3f fd = bo.force.direction; + float fs = bo.force.strength; + if (fs != 0f) { + dir.x += fd.x * fs; + dir.y += fd.y * fs; + dir.z += fd.z * fs; + dir.normalizeLocal(); + } + + // Twist: spin the transport frame around the branch axis + if (twistStep != 0f) { + Quaternion twistQ = new Quaternion().fromAngleAxis(twistStep, dir); + frameRight = twistQ.mult(frameRight).normalizeLocal(); + frameUp = dir.cross(frameRight).normalizeLocal(); + } + + pos.addLocal(dir.mult(sectionLen)); + float radius = startRad + (endRad - startRad) * t; + pts.add(new BranchSection(pos.clone(), radius, dir.clone(), t)); + } + + addCylinderGeometry(pts, segments, taper); + + if (level < bo.levels) { + generateChildren(branch, pts, rng, queue); + } + + // Leaves on the last (finest) level branches + if (level == bo.levels) { + generateLeaves(pts, rng); + } + } + + // ── Cylinder mesh contribution ─────────────────────────────────────────── + + private void addCylinderGeometry(List pts, int segments, float taper) { + int vertsPerRing = segments + 1; + int baseVertex = barkPos.size() / 3; + + // Parallel-transport frame + Vector3f frameRight = findPerp(pts.get(0).dir()); + Vector3f frameUp = pts.get(0).dir().cross(frameRight).normalizeLocal(); + + for (BranchSection sp : pts) { + Vector3f d = sp.dir().normalize(); + + // Transport the frame to the new direction + float dot = frameRight.dot(d); + frameRight = frameRight.subtract(d.mult(dot)); + if (frameRight.lengthSquared() < 1e-6f) { + frameRight = findPerp(d); + } else { + frameRight.normalizeLocal(); + } + frameUp = d.cross(frameRight).normalizeLocal(); + + float uv_v = (1f - sp.t()) * opts.bark.textureScaleY; + + for (int seg = 0; seg <= segments; seg++) { + float angle = (float) seg / segments * FastMath.TWO_PI; + float cosA = FastMath.cos(angle); + float sinA = FastMath.sin(angle); + + // Vertex = center + radial offset along (right, up) + float ox = frameRight.x * cosA + frameUp.x * sinA; + float oy = frameRight.y * cosA + frameUp.y * sinA; + float oz = frameRight.z * cosA + frameUp.z * sinA; + float r = sp.radius(); + + barkPos.add(sp.pos().x + ox * r); + barkPos.add(sp.pos().y + oy * r); + barkPos.add(sp.pos().z + oz * r); + + // Normal = outward radial direction + float nlen = FastMath.sqrt(ox * ox + oy * oy + oz * oz); + barkNorm.add(ox / nlen); + barkNorm.add(oy / nlen); + barkNorm.add(oz / nlen); + + barkUV.add((float) seg / segments * opts.bark.textureScaleX); + barkUV.add(uv_v); + barkWind.add(sp.t()); + } + } + + // Connect adjacent rings with quads + int numRings = pts.size(); + for (int si = 0; si < numRings - 1; si++) { + int r0 = baseVertex + si * vertsPerRing; + int r1 = baseVertex + (si + 1) * vertsPerRing; + for (int seg = 0; seg < segments; seg++) { + int a = r0 + seg, b = r0 + seg + 1; + int c = r1 + seg, d = r1 + seg + 1; + barkIdx.add(a); barkIdx.add(b); barkIdx.add(c); + barkIdx.add(b); barkIdx.add(d); barkIdx.add(c); + } + } + } + + // ── Child branch placement ─────────────────────────────────────────────── + + private void generateChildren(Branch parent, List pts, + Rng rng, Queue queue) { + BranchOptions bo = opts.branch; + int level = parent.level; + int childLevel = level + 1; + int childCount = bo.getChildren(level); + float childStart = bo.getStart(childLevel); + float childLenFactor = bo.getLength(childLevel); + float childRadFactor = bo.getRadius(childLevel); + float childAngle = bo.getAngle(childLevel) * FastMath.DEG_TO_RAD; + float childTwistBase = bo.getTwist(childLevel) * FastMath.DEG_TO_RAD; + int sections = pts.size() - 1; + + for (int i = 0; i < childCount; i++) { + // Stratified placement along parent with small jitter + float t = childStart + ((float) i / childCount) * (1f - childStart); + t = FastMath.clamp(t + rng.range(-0.02f, 0.02f), childStart, 1f); + + int sectionIdx = Math.min(Math.max(0, (int) (t * sections)), sections); + BranchSection sp = pts.get(sectionIdx); + + // Azimuth: evenly distribute around parent axis, with optional twist accumulation + float phi = ((float) i / childCount) * FastMath.TWO_PI + + childTwistBase * i + + rng.range(-0.15f, 0.15f); + + Vector3f parentDir = sp.dir().normalize(); + Vector3f perp = findPerp(parentDir); + + // Rotate perp around parentDir by phi to obtain the swing axis + Quaternion phiQ = new Quaternion().fromAngleAxis(phi, parentDir); + Vector3f swingAxis = phiQ.mult(perp).normalizeLocal(); + + // Rotate parentDir by childAngle around swingAxis + Quaternion thetaQ = new Quaternion().fromAngleAxis(childAngle, swingAxis); + Vector3f childDir = thetaQ.mult(parentDir).normalizeLocal(); + + Branch child = new Branch(); + child.origin = sp.pos().clone(); + child.orientation = dirToQuat(childDir); + child.level = childLevel; + child.parent = parent; + child.length = parent.length * childLenFactor; + child.radius = sp.radius() * childRadFactor; + + queue.add(child); + } + } + + // ── Leaf placement ─────────────────────────────────────────────────────── + + private void generateLeaves(List pts, Rng rng) { + LeavesOptions lo = opts.leaves; + int count = lo.count; + int nSections = pts.size() - 1; + + for (int i = 0; i < count; i++) { + float t = lo.start + rng.next() * (1f - lo.start); + int si = Math.min(Math.max(0, (int) (t * nSections)), nSections); + BranchSection sp = pts.get(si); + + float size = lo.size * (1f + rng.range(-lo.sizeVariance, lo.sizeVariance)); + float yaw = rng.range(0f, FastMath.TWO_PI); + float pitch = rng.range(-FastMath.QUARTER_PI * 0.4f, FastMath.QUARTER_PI * 0.4f); + float wind = sp.t(); + + switch (lo.billboard) { + case CROSS -> { + addLeafQuad(sp.pos(), yaw, pitch, size, wind); + addLeafQuad(sp.pos(), yaw + FastMath.HALF_PI, pitch, size, wind); + } + case ROTATE_X -> addLeafQuad(sp.pos(), yaw, FastMath.HALF_PI, size, wind); + default -> addLeafQuad(sp.pos(), yaw, pitch, size, wind); + } + } + } + + private void addLeafQuad(Vector3f center, float yaw, float pitch, float size, float wind) { + float h = size * 0.5f; + float cy = FastMath.cos(yaw), sy = FastMath.sin(yaw); + float cp = FastMath.cos(pitch), sp = FastMath.sin(pitch); + + // Local frame: right = rotate X by yaw, up = rotated Y by yaw+pitch + Vector3f right = new Vector3f(cy, 0f, -sy); + Vector3f up = new Vector3f(sy * sp, cp, cy * sp); + Vector3f norm = right.cross(up).normalizeLocal(); + + // 4 corners: BL, BR, TR, TL + float[] cornerX = { center.x - right.x*h - up.x*h, + center.x + right.x*h - up.x*h, + center.x + right.x*h + up.x*h, + center.x - right.x*h + up.x*h }; + float[] cornerY = { center.y - right.y*h - up.y*h, + center.y + right.y*h - up.y*h, + center.y + right.y*h + up.y*h, + center.y - right.y*h + up.y*h }; + float[] cornerZ = { center.z - right.z*h - up.z*h, + center.z + right.z*h - up.z*h, + center.z + right.z*h + up.z*h, + center.z - right.z*h + up.z*h }; + + int base = leafPos.size() / 3; + for (int i = 0; i < 4; i++) { + leafPos.add(cornerX[i]); leafPos.add(cornerY[i]); leafPos.add(cornerZ[i]); + leafNorm.add(norm.x); leafNorm.add(norm.y); leafNorm.add(norm.z); + leafWind.add(wind); + } + leafUV.add(0f); leafUV.add(0f); + leafUV.add(1f); leafUV.add(0f); + leafUV.add(1f); leafUV.add(1f); + leafUV.add(0f); leafUV.add(1f); + + leafIdx.add(base); leafIdx.add(base+1); leafIdx.add(base+2); + leafIdx.add(base); leafIdx.add(base+2); leafIdx.add(base+3); + } + + // ── Mesh assembly ──────────────────────────────────────────────────────── + + private static Geometry buildGeometry(String name, + List pos, List norm, + List uv, List idx, + List wind) { + int nVerts = pos.size() / 3; + + FloatBuffer pb = BufferUtils.createFloatBuffer(nVerts * 3); + FloatBuffer nb = BufferUtils.createFloatBuffer(nVerts * 3); + FloatBuffer ub = BufferUtils.createFloatBuffer(nVerts * 2); + FloatBuffer cb = BufferUtils.createFloatBuffer(nVerts * 4); // RGBA — R = wind + IntBuffer ib = BufferUtils.createIntBuffer(idx.size()); + + for (float f : pos) pb.put(f); + for (float f : norm) nb.put(f); + for (float f : uv) ub.put(f); + for (float f : wind) { cb.put(f); cb.put(1f); cb.put(1f); cb.put(1f); } + for (int i : idx) ib.put(i); + + pb.flip(); nb.flip(); ub.flip(); cb.flip(); ib.flip(); + + Mesh mesh = new Mesh(); + mesh.setBuffer(Type.Position, 3, pb); + mesh.setBuffer(Type.Normal, 3, nb); + mesh.setBuffer(Type.TexCoord, 2, ub); + mesh.setBuffer(Type.Color, 4, cb); + mesh.setBuffer(Type.Index, 3, ib); + mesh.updateBound(); + + return new Geometry(name, mesh); + } + + // ── Math helpers ───────────────────────────────────────────────────────── + + /** Returns a unit vector perpendicular to v. */ + private static Vector3f findPerp(Vector3f v) { + Vector3f p = v.cross(Vector3f.UNIT_X); + if (p.lengthSquared() < 0.01f) p = v.cross(Vector3f.UNIT_Z); + return p.normalizeLocal(); + } + + /** Creates a quaternion that rotates UNIT_Y onto dir. */ + private static Quaternion dirToQuat(Vector3f dir) { + Vector3f d = dir.normalize(); + Vector3f axis = Vector3f.UNIT_Y.cross(d); + float len = axis.length(); + if (len < 1e-6f) { + if (Vector3f.UNIT_Y.dot(d) > 0f) return new Quaternion(); + return new Quaternion().fromAngleAxis(FastMath.PI, Vector3f.UNIT_X); + } + axis.divideLocal(len); + float angle = FastMath.acos(FastMath.clamp(Vector3f.UNIT_Y.dot(d), -1f, 1f)); + return new Quaternion().fromAngleAxis(angle, axis); + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class new file mode 100644 index 0000000..4366ac8 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java new file mode 100644 index 0000000..12f061a --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java @@ -0,0 +1,22 @@ +package de.blight.eztree; + +public final class TreeOptions { + + public int seed = 0; + public TreeType type = TreeType.DECIDUOUS; + public BarkOptions bark = new BarkOptions(); + public BranchOptions branch = new BranchOptions(); + public LeavesOptions leaves = new LeavesOptions(); + public TrellisOptions trellis = new TrellisOptions(); + + public TreeOptions copy() { + TreeOptions c = new TreeOptions(); + c.seed = seed; + c.type = type; + c.bark = bark.copy(); + c.branch = branch.copy(); + c.leaves = leaves.copy(); + c.trellis = trellis.copy(); + return c; + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class new file mode 100644 index 0000000..c1d6178 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java new file mode 100644 index 0000000..1b5f596 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java @@ -0,0 +1,537 @@ +package de.blight.eztree; + +/** + * Presets from https://github.com/dgreenheck/ez-tree/tree/main/src/lib/presets + * + * Scaling applied vs. raw JSON values to match JME3 meter units: + * trunk length[0] ÷ 4 + * trunk radius[0] ÷ 4 + * leaf size ÷ 5 + * Relative factors (length/radius level 1+) are JSON[n]/JSON[n-1] — unchanged. + * Twist: JSON radians × 57.2958 → degrees. + * Pine children capped at 20-22 (JS uses 91-100 for needle density, our generator + * creates full branch geometry per child). + */ +public final class TreePresets { + + private TreePresets() {} + + private static final String BARK1 = "Textures/bark/Bark001_Color.jpg"; + private static final String BARK2 = "Textures/bark/Bark002_Color.jpg"; + private static final String BARK3 = "Textures/bark/Bark003_Color.jpg"; + + private static final String LEAF_OAK = "Textures/leaves/oak.png"; + private static final String LEAF_ASH = "Textures/leaves/ash.png"; + private static final String LEAF_ASPEN = "Textures/leaves/aspen.png"; + private static final String LEAF_PINE = "Textures/leaves/pine.png"; + + // ════════════════════════════════════════════════════════════════════════ + // OAK + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions oakSmall() { + TreeOptions o = new TreeOptions(); + o.seed = 30895; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 54f); b.angle.put(2, 58f); b.angle.put(3, 32f); + b.children.put(0, 4); b.children.put(1, 2); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f; + b.gnarliness.put(0, 0.07f); b.gnarliness.put(1, 0.08f); + b.gnarliness.put(2, 0.11f); b.gnarliness.put(3, 0.09f); + b.length.put(0, 7.02f); b.length.put(1, 0.162f); b.length.put(2, 2.149f); b.length.put(3, 0.732f); + b.radius.put(0, 0.25f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 16); b.sections.put(1, 9); b.sections.put(2, 8); + b.segments.put(0, 7); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3); + b.start.put(1, 0.49f); b.start.put(2, 0.06f); b.start.put(3, 0.12f); + b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f); + b.twist.put(0, -13.18f); b.twist.put(1, 24.06f); + + o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 14, 0.16f, 0.28f, 0.70f, 0.5f); + return o; + } + + public static TreeOptions oakMedium() { + TreeOptions o = new TreeOptions(); + o.seed = 35729; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 54f); b.angle.put(2, 58f); b.angle.put(3, 32f); + b.children.put(0, 6); b.children.put(1, 4); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.02f; + b.gnarliness.put(0, 0.00f); b.gnarliness.put(1, 0.10f); + b.gnarliness.put(2, 0.15f); b.gnarliness.put(3, 0.09f); + b.length.put(0, 9.31f); b.length.put(1, 0.298f); b.length.put(2, 1.118f); b.length.put(3, 0.578f); + b.radius.put(0, 0.35f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 8); b.sections.put(1, 6); b.sections.put(2, 4); + b.segments.put(0, 7); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3); + b.start.put(1, 0.49f); b.start.put(2, 0.06f); b.start.put(3, 0.12f); + b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f); + b.twist.put(0, -13.18f); b.twist.put(1, 24.06f); + + o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 18, 0.16f, 0.50f, 0.70f, 0.5f); + return o; + } + + public static TreeOptions oakLarge() { + TreeOptions o = new TreeOptions(); + o.seed = 23399; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 54f); b.angle.put(2, 43f); b.angle.put(3, 32f); + b.children.put(0, 9); b.children.put(1, 5); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.02f; + b.gnarliness.put(0, 0.04f); b.gnarliness.put(1, 0.16f); + b.gnarliness.put(2, 0.06f); b.gnarliness.put(3, 0.09f); + b.length.put(0, 11.93f); b.length.put(1, 0.616f); b.length.put(2, 0.600f); b.length.put(3, 0.406f); + b.radius.put(0, 0.75f); b.radius.put(1, 0.58f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 16); b.sections.put(1, 9); b.sections.put(2, 8); b.sections.put(3, 3); + b.segments.put(0, 12); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3); + b.start.put(1, 0.35f); b.start.put(2, 0.10f); b.start.put(3, 0.00f); + b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f); + b.twist.put(0, -13.18f); b.twist.put(1, 24.06f); + + o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 10, 0.16f, 0.90f, 0.70f, 0.5f); + return o; + } + + // ════════════════════════════════════════════════════════════════════════ + // ASH + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions ashSmall() { + TreeOptions o = new TreeOptions(); + o.seed = 26867; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f; + o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f; + + BranchOptions b = o.branch; + b.levels = 2; + b.angle.put(1, 48f); b.angle.put(2, 75f); b.angle.put(3, 60f); + b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f; + b.gnarliness.put(0, 0.11f); b.gnarliness.put(1, 0.09f); + b.gnarliness.put(2, 0.05f); b.gnarliness.put(3, 0.09f); + b.length.put(0, 5.97f); b.length.put(1, 0.754f); b.length.put(2, 0.311f); b.length.put(3, 0.823f); + b.radius.put(0, 0.20f); b.radius.put(1, 0.62f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 10); b.sections.put(3, 10); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.53f); b.start.put(2, 0.33f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + b.twist.put(0, 17.19f); b.twist.put(1, -4.01f); + + o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 30, 0.00f, 0.41f, 0.717f, 0.5f); + return o; + } + + public static TreeOptions ashMedium() { + TreeOptions o = new TreeOptions(); + o.seed = 36330; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f; + o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 48f); b.angle.put(2, 75f); b.angle.put(3, 60f); + b.children.put(0, 7); b.children.put(1, 4); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f; + b.gnarliness.put(0, 0.03f); b.gnarliness.put(1, 0.25f); + b.gnarliness.put(2, 0.20f); b.gnarliness.put(3, 0.09f); + b.length.put(0, 10.87f); b.length.put(1, 0.624f); b.length.put(2, 0.350f); b.length.put(3, 0.484f); + b.radius.put(0, 0.50f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 8); b.sections.put(2, 6); b.sections.put(3, 4); + b.segments.put(0, 12); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.23f); b.start.put(2, 0.33f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + b.twist.put(0, 5.16f); b.twist.put(1, -4.01f); + + o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 16, 0.00f, 0.53f, 0.720f, 0.5f); + return o; + } + + public static TreeOptions ashLarge() { + TreeOptions o = new TreeOptions(); + o.seed = 29919; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f; + o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 39f); b.angle.put(2, 39f); b.angle.put(3, 51f); + b.children.put(0, 10); b.children.put(1, 4); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f; + b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.20f); + b.gnarliness.put(2, 0.16f); b.gnarliness.put(3, 0.05f); + b.length.put(0, 11.25f); b.length.put(1, 0.654f); b.length.put(2, 0.520f); b.length.put(3, 0.301f); + b.radius.put(0, 0.76f); b.radius.put(1, 0.58f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 8); b.sections.put(2, 6); b.sections.put(3, 4); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.32f); b.start.put(2, 0.34f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.62f); b.taper.put(2, 0.76f); b.taper.put(3, 0.00f); + b.twist.put(0, 5.16f); b.twist.put(1, -4.01f); + + o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 10, 0.01f, 0.92f, 0.720f, 0.5f); + return o; + } + + // ════════════════════════════════════════════════════════════════════════ + // ASPEN + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions aspenSmall() { + TreeOptions o = new TreeOptions(); + o.seed = 36330; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK2; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f; + + BranchOptions b = o.branch; + b.levels = 2; + b.angle.put(1, 70f); b.angle.put(2, 35f); b.angle.put(3, 7f); + b.children.put(0, 4); b.children.put(1, 3); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.011f; + b.gnarliness.put(0, 0.04f); b.gnarliness.put(1, 0.01f); + b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f); + b.length.put(0, 6.00f); b.length.put(1, 0.140f); b.length.put(2, 2.292f); b.length.put(3, 0.130f); + b.radius.put(0, 0.093f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.45f); b.start.put(2, 0.33f); + b.taper.put(0, 0.37f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + + o.leaves = leaves(LEAF_ASPEN, 1.000f, 0.980f, 0.384f, Billboard.CROSS, 13, 0.20f, 0.50f, 0.70f, 0.5f); + return o; + } + + public static TreeOptions aspenMedium() { + TreeOptions o = new TreeOptions(); + o.seed = 18020; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK2; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f; + + BranchOptions b = o.branch; + b.levels = 2; + b.angle.put(1, 75f); b.angle.put(2, 32f); b.angle.put(3, 7f); + b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.015f; + b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.12f); + b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f); + b.length.put(0, 12.50f); b.length.put(1, 0.121f); b.length.put(2, 1.843f); b.length.put(3, 0.089f); + b.radius.put(0, 0.18f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.59f); b.start.put(2, 0.35f); + b.taper.put(0, 0.37f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + + o.leaves = leaves(LEAF_ASPEN, 1.000f, 0.980f, 0.384f, Billboard.CROSS, 11, 0.124f, 0.50f, 0.70f, 0.5f); + return o; + } + + public static TreeOptions aspenLarge() { + TreeOptions o = new TreeOptions(); + o.seed = 30631; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK2; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f; + + BranchOptions b = o.branch; + b.levels = 2; + b.angle.put(1, 47f); b.angle.put(2, 63f); b.angle.put(3, 7f); + b.children.put(0, 10); b.children.put(1, 6); b.children.put(2, 0); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.022f; + b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.03f); + b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f); + b.length.put(0, 17.40f); b.length.put(1, 0.267f); b.length.put(2, 0.603f); b.length.put(3, 0.089f); + b.radius.put(0, 0.28f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.62f); b.start.put(2, 0.05f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + + o.leaves = leaves(LEAF_ASPEN, 0.988f, 1.000f, 0.149f, Billboard.CROSS, 20, 0.152f, 0.70f, 0.70f, 0.5f); + return o; + } + + // ════════════════════════════════════════════════════════════════════════ + // PINE + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions pineSmall() { + TreeOptions o = new TreeOptions(); + o.seed = 11744; + o.type = TreeType.EVERGREEN; + o.bark.textureFile = BARK3; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f; + + BranchOptions b = o.branch; + b.levels = 1; + b.angle.put(1, 117f); b.angle.put(2, 60f); + b.children.put(0, 20); b.children.put(1, 7); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f; + b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f); + b.length.put(0, 9.89f); b.length.put(1, 0.307f); b.length.put(2, 0.825f); + b.radius.put(0, 0.14f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); + b.start.put(1, 0.16f); b.start.put(2, 0.30f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); + + o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 21, 0.00f, 0.19f, 0.70f, 0.3f); + return o; + } + + public static TreeOptions pineMedium() { + TreeOptions o = new TreeOptions(); + o.seed = 13977; + o.type = TreeType.EVERGREEN; + o.bark.textureFile = BARK3; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f; + + BranchOptions b = o.branch; + b.levels = 1; + b.angle.put(1, 110f); b.angle.put(2, 16f); + b.children.put(0, 18); b.children.put(1, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = -0.003f; + b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f); + b.length.put(0, 12.50f); b.length.put(1, 0.477f); b.length.put(2, 0.590f); + b.radius.put(0, 0.26f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); + b.start.put(1, 0.27f); b.start.put(2, 0.14f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); + + o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 30, 0.09f, 0.29f, 0.201f, 0.3f); + return o; + } + + public static TreeOptions pineLarge() { + TreeOptions o = new TreeOptions(); + o.seed = 44166; + o.type = TreeType.EVERGREEN; + o.bark.textureFile = BARK3; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f; + + BranchOptions b = o.branch; + b.levels = 1; + b.angle.put(1, 129f); b.angle.put(2, 16f); + b.children.put(0, 22); b.children.put(1, 3); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.009f; + b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f); + b.length.put(0, 16.31f); b.length.put(1, 0.534f); b.length.put(2, 0.782f); + b.radius.put(0, 0.32f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f); + b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); + b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); + b.start.put(1, 0.294f); b.start.put(2, 0.14f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); + + o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 18, 0.076f, 0.52f, 0.201f, 0.3f); + return o; + } + + // ════════════════════════════════════════════════════════════════════════ + // BUSH + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions bush1() { + TreeOptions o = new TreeOptions(); + o.seed = 45590; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f; + o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 21.52f); b.angle.put(2, 62.61f); b.angle.put(3, 60f); + b.children.put(0, 7); b.children.put(1, 3); b.children.put(2, 2); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f; + b.gnarliness.put(0, 0.11f); b.gnarliness.put(1, 0.09f); + b.gnarliness.put(2, 0.05f); b.gnarliness.put(3, 0.09f); + // Near-zero trunk: 0.25f stump, factor adjusted so actual branch1 ≈ 3.8 units + b.length.put(0, 0.25f); b.length.put(1, 15.30f); b.length.put(2, 0.365f); b.length.put(3, 0.823f); + b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 4); b.sections.put(1, 6); b.sections.put(2, 10); b.sections.put(3, 10); + b.segments.put(0, 4); b.segments.put(1, 4); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.53f); b.start.put(2, 0.33f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + b.twist.put(0, 17.19f); b.twist.put(1, -4.01f); + + o.leaves = leaves(LEAF_ASH, 0.878f, 1.000f, 0.835f, Billboard.CROSS, 12, 0.00f, 0.49f, 0.717f, 0.5f); + return o; + } + + public static TreeOptions bush2() { + TreeOptions o = new TreeOptions(); + o.seed = 45590; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f; + o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f; + + BranchOptions b = o.branch; + b.levels = 2; + b.angle.put(1, 19.57f); b.angle.put(2, 27.39f); b.angle.put(3, 60f); + b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 2); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f; + b.gnarliness.put(0, 0.022f); b.gnarliness.put(1, 0.109f); + b.gnarliness.put(2, 0.050f); b.gnarliness.put(3, 0.090f); + // Actual branch1 ≈ 4.9 units (19.646/4 ÷ 0.25) + b.length.put(0, 0.25f); b.length.put(1, 19.65f); b.length.put(2, 0.392f); b.length.put(3, 0.597f); + b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 3); b.sections.put(1, 4); b.sections.put(2, 10); b.sections.put(3, 10); + b.segments.put(0, 4); b.segments.put(1, 4); b.segments.put(2, 4); b.segments.put(3, 3); + b.start.put(1, 0.641f); b.start.put(2, 0.707f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + b.twist.put(0, 20.55f); b.twist.put(1, -2.49f); + + o.leaves = leaves(LEAF_ASPEN, 0.878f, 1.000f, 0.835f, Billboard.CROSS, 7, 0.00f, 0.49f, 0.717f, 0.5f); + return o; + } + + public static TreeOptions bush3() { + TreeOptions o = new TreeOptions(); + o.seed = 31343; + o.type = TreeType.EVERGREEN; + o.bark.textureFile = BARK1; + o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f; + o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 66.52f); b.angle.put(2, 52.83f); b.angle.put(3, 0f); + b.children.put(0, 13); b.children.put(1, 4); b.children.put(2, 4); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f; + b.gnarliness.put(0, 0.054f); b.gnarliness.put(1, 0.065f); + b.gnarliness.put(2, 0.050f); b.gnarliness.put(3, 0.090f); + b.length.put(0, 2.74f); b.length.put(1, 1.991f); b.length.put(2, 0.602f); b.length.put(3, 0.421f); + b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 4); b.sections.put(1, 3); b.sections.put(2, 3); b.sections.put(3, 10); + b.segments.put(0, 3); b.segments.put(1, 3); b.segments.put(2, 3); b.segments.put(3, 3); + b.start.put(1, 0.141f); b.start.put(2, 0.294f); + b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f); + b.twist.put(0, 17.19f); b.twist.put(1, -1.87f); + + o.leaves = leaves(LEAF_PINE, 0.616f, 0.765f, 1.000f, Billboard.CROSS, 3, 0.152f, 0.61f, 0.457f, 0.5f); + return o; + } + + // ════════════════════════════════════════════════════════════════════════ + // TRELLIS + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions trellis() { + TreeOptions o = new TreeOptions(); + o.seed = 41563; + o.type = TreeType.DECIDUOUS; + o.bark.textureFile = BARK1; + o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f; + o.bark.textureScaleX = 1f; o.bark.textureScaleY = 8f; + + BranchOptions b = o.branch; + b.levels = 3; + b.angle.put(1, 26f); b.angle.put(2, 79f); b.angle.put(3, 0f); + b.children.put(0, 7); b.children.put(1, 5); b.children.put(2, 1); + b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.026f; + b.gnarliness.put(0, 0.00f); b.gnarliness.put(1, 0.02f); + b.gnarliness.put(2, 0.41f); b.gnarliness.put(3, 0.09f); + b.length.put(0, 1.20f); b.length.put(1, 3.521f); b.length.put(2, 0.669f); b.length.put(3, 0.982f); + b.radius.put(0, 0.068f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f); + b.sections.put(0, 6); b.sections.put(1, 12); b.sections.put(2, 10); b.sections.put(3, 4); + b.segments.put(0, 3); b.segments.put(1, 3); b.segments.put(2, 3); b.segments.put(3, 3); + b.start.put(1, 0.19f); b.start.put(2, 0.10f); b.start.put(3, 0.06f); + b.taper.put(0, 0.60f); b.taper.put(1, 0.50f); b.taper.put(2, 0.50f); b.taper.put(3, 0.50f); + b.twist.put(0, -1.15f); b.twist.put(1, -0.57f); b.twist.put(2, 5.16f); + + // tint 15204310 = 0xE7D8C6: (231,216,198)/255 + o.leaves = leaves(LEAF_ASH, 0.906f, 0.847f, 0.776f, Billboard.NONE, 13, 0.00f, 0.34f, 0.50f, 0.5f); + + o.trellis.enabled = true; + o.trellis.sections = 8; + o.trellis.radius = 0.5f; + o.trellis.length = 4f; + o.trellis.memberRadius = 0.08f; + o.trellis.crossMembers = 4; + + return o; + } + + // ════════════════════════════════════════════════════════════════════════ + // Lookup + // ════════════════════════════════════════════════════════════════════════ + + public static TreeOptions byName(String name) { + return switch (name.toLowerCase()) { + case "oak small" -> oakSmall(); + case "oak medium" -> oakMedium(); + case "oak large" -> oakLarge(); + case "ash small" -> ashSmall(); + case "ash medium" -> ashMedium(); + case "ash large" -> ashLarge(); + case "aspen small" -> aspenSmall(); + case "aspen medium" -> aspenMedium(); + case "aspen large" -> aspenLarge(); + case "pine small" -> pineSmall(); + case "pine medium" -> pineMedium(); + case "pine large" -> pineLarge(); + case "bush 1" -> bush1(); + case "bush 2" -> bush2(); + case "bush 3" -> bush3(); + case "trellis" -> trellis(); + default -> oakMedium(); + }; + } + + public static String[] presetNames() { + return new String[]{ + "Oak Small", "Oak Medium", "Oak Large", + "Ash Small", "Ash Medium", "Ash Large", + "Aspen Small", "Aspen Medium", "Aspen Large", + "Pine Small", "Pine Medium", "Pine Large", + "Bush 1", "Bush 2", "Bush 3", + "Trellis" + }; + } + + private static LeavesOptions leaves(String tex, float r, float g, float b, + Billboard bb, int count, + float start, float size, float sizeVar, float alpha) { + LeavesOptions l = new LeavesOptions(); + l.textureFile = tex; + l.r = r; l.g = g; l.b = b; + l.billboard = bb; + l.count = count; + l.start = start; + l.size = size; + l.sizeVariance = sizeVar; + l.alphaTest = alpha; + return l; + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class new file mode 100644 index 0000000..d8d1811 Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java new file mode 100644 index 0000000..68b57c2 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java @@ -0,0 +1,5 @@ +package de.blight.eztree; + +public enum TreeType { + DECIDUOUS, EVERGREEN +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java b/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java new file mode 100644 index 0000000..0cdf19a --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java @@ -0,0 +1,141 @@ +package de.blight.eztree; + +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Builds a cylindrical lattice (trellis) for climbing plants. + * + * The trellis consists of {@code sections} vertical poles arranged in a circle + * of {@code radius}, connected by {@code crossMembers} horizontal rings. + * All members are thin cylinders with {@code memberRadius}. + */ +public final class Trellis { + + private Trellis() {} + + public static Node build(TrellisOptions o) { + Node node = new Node("trellis"); + + int poles = Math.max(3, o.sections); + int crossMembers = Math.max(1, o.crossMembers); + float totalHeight = o.length; + float poleRadius = o.radius; + float memberR = o.memberRadius; + int cylSegs = 5; + + List pos = new ArrayList<>(); + List norm = new ArrayList<>(); + List uv = new ArrayList<>(); + List idx = new ArrayList<>(); + + // Vertical poles + for (int p = 0; p < poles; p++) { + float phi = (float) p / poles * FastMath.TWO_PI; + float px = FastMath.cos(phi) * poleRadius; + float pz = FastMath.sin(phi) * poleRadius; + addCylinder(pos, norm, uv, idx, + new Vector3f(px, 0f, pz), + new Vector3f(px, totalHeight, pz), + memberR, cylSegs); + } + + // Horizontal cross-member rings + for (int cm = 0; cm < crossMembers; cm++) { + float y = totalHeight * (cm + 1f) / (crossMembers + 1f); + for (int p = 0; p < poles; p++) { + float phi0 = (float) p / poles * FastMath.TWO_PI; + float phi1 = (float) (p + 1) / poles * FastMath.TWO_PI; + Vector3f from = new Vector3f( + FastMath.cos(phi0) * poleRadius, y, FastMath.sin(phi0) * poleRadius); + Vector3f to = new Vector3f( + FastMath.cos(phi1) * poleRadius, y, FastMath.sin(phi1) * poleRadius); + addCylinder(pos, norm, uv, idx, from, to, memberR, cylSegs); + } + } + + Geometry geom = toGeometry("trellis-mesh", pos, norm, uv, idx); + node.attachChild(geom); + return node; + } + + // ── Cylinder helper ────────────────────────────────────────────────────── + + private static void addCylinder(List pos, List norm, List uv, + List idx, + Vector3f from, Vector3f to, float radius, int segs) { + Vector3f dir = to.subtract(from).normalizeLocal(); + Vector3f right = findPerp(dir); + Vector3f up = dir.cross(right).normalizeLocal(); + + int base = pos.size() / 3; + + for (int ring = 0; ring <= 1; ring++) { + Vector3f center = (ring == 0) ? from : to; + float v = ring; + for (int s = 0; s <= segs; s++) { + float a = (float) s / segs * FastMath.TWO_PI; + float ca = FastMath.cos(a), sa = FastMath.sin(a); + float ox = right.x * ca + up.x * sa; + float oy = right.y * ca + up.y * sa; + float oz = right.z * ca + up.z * sa; + pos.add(center.x + ox * radius); + pos.add(center.y + oy * radius); + pos.add(center.z + oz * radius); + float nl = FastMath.sqrt(ox*ox + oy*oy + oz*oz); + norm.add(ox / nl); norm.add(oy / nl); norm.add(oz / nl); + uv.add((float) s / segs); uv.add(v); + } + } + + int ringSz = segs + 1; + for (int s = 0; s < segs; s++) { + int a = base + s, b = base + s + 1; + int c = base + ringSz + s, d = base + ringSz + s + 1; + idx.add(a); idx.add(b); idx.add(c); + idx.add(b); idx.add(d); idx.add(c); + } + } + + private static Geometry toGeometry(String name, + List pos, List norm, + List uv, List idx) { + int nv = pos.size() / 3; + FloatBuffer pb = BufferUtils.createFloatBuffer(nv * 3); + FloatBuffer nb = BufferUtils.createFloatBuffer(nv * 3); + FloatBuffer ub = BufferUtils.createFloatBuffer(nv * 2); + IntBuffer ib = BufferUtils.createIntBuffer(idx.size()); + + for (float f : pos) pb.put(f); + for (float f : norm) nb.put(f); + for (float f : uv) ub.put(f); + for (int i : idx) ib.put(i); + + pb.flip(); nb.flip(); ub.flip(); ib.flip(); + + Mesh mesh = new Mesh(); + mesh.setBuffer(Type.Position, 3, pb); + mesh.setBuffer(Type.Normal, 3, nb); + mesh.setBuffer(Type.TexCoord, 2, ub); + mesh.setBuffer(Type.Index, 3, ib); + mesh.updateBound(); + + return new Geometry(name, mesh); + } + + private static Vector3f findPerp(Vector3f v) { + Vector3f p = v.cross(Vector3f.UNIT_X); + if (p.lengthSquared() < 0.01f) p = v.cross(Vector3f.UNIT_Z); + return p.normalizeLocal(); + } +} diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class new file mode 100644 index 0000000..1c91f9d Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class differ diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java new file mode 100644 index 0000000..693daf5 --- /dev/null +++ b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java @@ -0,0 +1,24 @@ +package de.blight.eztree; + +public final class TrellisOptions { + + public boolean enabled = false; + public int sections = 4; + public float radius = 0.5f; + public float length = 2f; + /** Radius of the cylindrical trellis members. */ + public float memberRadius = 0.04f; + /** Number of cross-members per section. */ + public int crossMembers = 3; + + public TrellisOptions copy() { + TrellisOptions c = new TrellisOptions(); + c.enabled = enabled; + c.sections = sections; + c.radius = radius; + c.length = length; + c.memberRadius = memberRadius; + c.crossMembers = crossMembers; + return c; + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a034286 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..97de990 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/img/editor/grasstool.png b/img/editor/grasstool.png new file mode 100644 index 0000000..7f581fe Binary files /dev/null and b/img/editor/grasstool.png differ diff --git a/img/editor/terraintool.png b/img/editor/terraintool.png new file mode 100644 index 0000000..f33bc63 Binary files /dev/null and b/img/editor/terraintool.png differ diff --git a/img/editor/terraintool_plateau.png b/img/editor/terraintool_plateau.png new file mode 100644 index 0000000..f3f8e1c Binary files /dev/null and b/img/editor/terraintool_plateau.png differ diff --git a/img/editor/terraintool_sinus.png b/img/editor/terraintool_sinus.png new file mode 100644 index 0000000..bb8dfc6 Binary files /dev/null and b/img/editor/terraintool_sinus.png differ diff --git a/img/editor/terraintool_smooth.png b/img/editor/terraintool_smooth.png new file mode 100644 index 0000000..2eff4bd Binary files /dev/null and b/img/editor/terraintool_smooth.png differ diff --git a/img/editor/terraintool_spike.png b/img/editor/terraintool_spike.png new file mode 100644 index 0000000..88f6c67 Binary files /dev/null and b/img/editor/terraintool_spike.png differ diff --git a/img/editor/textruretool.png b/img/editor/textruretool.png new file mode 100644 index 0000000..11e4518 Binary files /dev/null and b/img/editor/textruretool.png differ diff --git a/blight-data/logo/logo.png b/logo/logo.png similarity index 100% rename from blight-data/logo/logo.png rename to logo/logo.png diff --git a/blight-data/logo/orig.png b/logo/orig.png similarity index 100% rename from blight-data/logo/orig.png rename to logo/orig.png diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b214c1b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +rootProject.name = 'blight' + +include 'blight-common' +include 'blight-assets' +include 'blight-editor' +include 'blight-game' +include 'simarboreal' +include 'ez-tree-jme' diff --git a/simarboreal/.gitignore b/simarboreal/.gitignore new file mode 100644 index 0000000..da88288 --- /dev/null +++ b/simarboreal/.gitignore @@ -0,0 +1 @@ +/.gradle/ diff --git a/simarboreal/assets/MatDefs/MultiResolution.frag b/simarboreal/assets/MatDefs/MultiResolution.frag new file mode 100644 index 0000000..9682797 --- /dev/null +++ b/simarboreal/assets/MatDefs/MultiResolution.frag @@ -0,0 +1,332 @@ +#import "Common/ShaderLib/Parallax.glsllib" +#import "Common/ShaderLib/Optics.glsllib" +#define ATTENUATION +//#define HQ_ATTENUATION + +#import "MatDefs/FragScattering.glsllib" + +varying vec2 texCoord; +#ifdef SEPARATE_TEXCOORD + varying vec2 texCoord2; +#endif + +varying vec3 AmbientSum; +varying vec4 DiffuseSum; +varying vec3 SpecularSum; + +varying float z; + +#ifndef VERTEX_LIGHTING + uniform vec4 g_LightDirection; + //varying vec3 vPosition; + varying vec3 vViewDir; + varying vec4 vLightDir; + varying vec3 lightVec; +#else + varying vec2 vertexLightValues; +#endif + +#ifdef DIFFUSEMAP + uniform sampler2D m_DiffuseMap; + uniform sampler2D m_BackgroundDiffuseMap; + uniform sampler2D m_NoiseMap; +#endif + +#ifdef SPECULARMAP + uniform sampler2D m_SpecularMap; +#endif + +#ifdef PARALLAXMAP + uniform sampler2D m_ParallaxMap; +#endif +#if (defined(PARALLAXMAP) || (defined(NORMALMAP_PARALLAX) && defined(NORMALMAP))) && !defined(VERTEX_LIGHTING) + uniform float m_ParallaxHeight; +#endif + +#ifdef LIGHTMAP + uniform sampler2D m_LightMap; +#endif + +#ifdef NORMALMAP + uniform sampler2D m_NormalMap; +#else + varying vec3 vNormal; +#endif + +#ifdef ALPHAMAP + uniform sampler2D m_AlphaMap; +#endif + +#ifdef COLORRAMP + uniform sampler2D m_ColorRamp; +#endif + +uniform float m_AlphaDiscardThreshold; + +#ifndef VERTEX_LIGHTING +uniform float m_Shininess; + +#ifdef HQ_ATTENUATION +uniform vec4 g_LightPosition; +#endif + +#ifdef USE_REFLECTION + uniform float m_ReflectionPower; + uniform float m_ReflectionIntensity; + varying vec4 refVec; + + uniform ENVMAP m_EnvMap; +#endif + +float tangDot(in vec3 v1, in vec3 v2){ + float d = dot(v1,v2); + #ifdef V_TANGENT + d = 1.0 - d*d; + return step(0.0, d) * sqrt(d); + #else + return d; + #endif +} + +float lightComputeDiffuse(in vec3 norm, in vec3 lightdir, in vec3 viewdir){ + #ifdef MINNAERT + float NdotL = max(0.0, dot(norm, lightdir)); + float NdotV = max(0.0, dot(norm, viewdir)); + return NdotL * pow(max(NdotL * NdotV, 0.1), -1.0) * 0.5; + #else + return max(0.0, dot(norm, lightdir)); + #endif +} + +float lightComputeSpecular(in vec3 norm, in vec3 viewdir, in vec3 lightdir, in float shiny){ + // NOTE: check for shiny <= 1 removed since shininess is now + // 1.0 by default (uses matdefs default vals) + #ifdef LOW_QUALITY + // Blinn-Phong + // Note: preferably, H should be computed in the vertex shader + vec3 H = (viewdir + lightdir) * vec3(0.5); + return pow(max(tangDot(H, norm), 0.0), shiny); + #elif defined(WARDISO) + // Isotropic Ward + vec3 halfVec = normalize(viewdir + lightdir); + float NdotH = max(0.001, tangDot(norm, halfVec)); + float NdotV = max(0.001, tangDot(norm, viewdir)); + float NdotL = max(0.001, tangDot(norm, lightdir)); + float a = tan(acos(NdotH)); + float p = max(shiny/128.0, 0.001); + return NdotL * (1.0 / (4.0*3.14159265*p*p)) * (exp(-(a*a)/(p*p)) / (sqrt(NdotV * NdotL))); + #else + // Standard Phong + vec3 R = reflect(-lightdir, norm); + return pow(max(tangDot(R, viewdir), 0.0), shiny); + #endif +} + +vec2 computeLighting(in vec3 wvNorm, in vec3 wvViewDir, in vec3 wvLightDir){ + float diffuseFactor = lightComputeDiffuse(wvNorm, wvLightDir, wvViewDir); + float specularFactor = lightComputeSpecular(wvNorm, wvViewDir, wvLightDir, m_Shininess); + + #ifdef HQ_ATTENUATION + float att = clamp(1.0 - g_LightPosition.w * length(lightVec), 0.0, 1.0); + #else + float att = vLightDir.w; + #endif + + if (m_Shininess <= 1.0) { + specularFactor = 0.0; // should be one instruction on most cards .. + } + + specularFactor *= diffuseFactor; + + return vec2(diffuseFactor, specularFactor) * vec2(att); +} +#endif + +vec4 getColor( in sampler2D diffuseMap, in sampler2D diffuseMap2, + in sampler2D normalMap, in vec2 tc, in float distMix, + out vec3 normal ) { + + vec2 tcOffset; + tcOffset = texture2D(m_NoiseMap, tc * 0.01).xy * 6.0 - 3.0; + vec4 diffuseColor = texture2D(diffuseMap, (tc + tcOffset) * 0.75); + + tcOffset = (texture2D(m_NoiseMap, tc * 0.01).xy * 6.0) - 3.0; + vec4 subColor = texture2D(diffuseMap2, ((tc + tcOffset) * 1.0) * 0.1 ); + diffuseColor = mix(diffuseColor, subColor, distMix); + + #ifdef NORMALMAP + vec4 normalHeight = texture2D(normalMap, tc); + normal = normalize((normalHeight.xyz * vec3(2.0) - vec3(1.0))); + #else + normal = vec3(0.0, 1.0, 0.0); + #endif + + return diffuseColor; +} + + +void main(){ + vec2 newTexCoord; + + #if (defined(PARALLAXMAP) || (defined(NORMALMAP_PARALLAX) && defined(NORMALMAP))) && !defined(VERTEX_LIGHTING) + + #ifdef STEEP_PARALLAX + #ifdef NORMALMAP_PARALLAX + //parallax map is stored in the alpha channel of the normal map + newTexCoord = steepParallaxOffset(m_NormalMap, vViewDir, texCoord, m_ParallaxHeight); + #else + //parallax map is a texture + newTexCoord = steepParallaxOffset(m_ParallaxMap, vViewDir, texCoord, m_ParallaxHeight); + #endif + #else + #ifdef NORMALMAP_PARALLAX + //parallax map is stored in the alpha channel of the normal map + newTexCoord = classicParallaxOffset(m_NormalMap, vViewDir, texCoord, m_ParallaxHeight); + #else + //parallax map is a texture + newTexCoord = classicParallaxOffset(m_ParallaxMap, vViewDir, texCoord, m_ParallaxHeight); + #endif + #endif + #else + newTexCoord = texCoord; + #endif + + float distMix = z / 32.0; + distMix = clamp(distMix, 0.4, 1.0); + + #ifdef DIFFUSEMAP + vec3 newNormal; + #ifdef NORMALMAP + vec4 diffuseColor = getColor(m_DiffuseMap, m_BackgroundDiffuseMap, + m_NormalMap, texCoord, distMix, newNormal); + #else + vec4 diffuseColor = getColor(m_DiffuseMap, m_BackgroundDiffuseMap, + m_DiffuseMap, texCoord, distMix, newNormal); + #endif + #else + vec4 diffuseColor = vec4(1.0); + vec3 newNormal = vec3(0.0, 1.0, 0.0); + #endif + + float alpha = DiffuseSum.a * diffuseColor.a; + #ifdef ALPHAMAP + alpha = alpha * texture2D(m_AlphaMap, newTexCoord).r; + #endif + if(alpha < m_AlphaDiscardThreshold){ + discard; + } + + #ifndef VERTEX_LIGHTING + float spotFallOff = 1.0; + + #if __VERSION__ >= 110 + // allow use of control flow + if(g_LightDirection.w != 0.0){ + #endif + + vec3 L = normalize(lightVec.xyz); + vec3 spotdir = normalize(g_LightDirection.xyz); + float curAngleCos = dot(-L, spotdir); + float innerAngleCos = floor(g_LightDirection.w) * 0.001; + float outerAngleCos = fract(g_LightDirection.w); + float innerMinusOuter = innerAngleCos - outerAngleCos; + spotFallOff = (curAngleCos - outerAngleCos) / innerMinusOuter; + + #if __VERSION__ >= 110 + if(spotFallOff <= 0.0){ + gl_FragColor.rgb = AmbientSum * diffuseColor.rgb; + gl_FragColor.a = alpha; + return; + }else{ + spotFallOff = clamp(spotFallOff, 0.0, 1.0); + } + } + #else + spotFallOff = clamp(spotFallOff, step(g_LightDirection.w, 0.001), 1.0); + #endif + #endif + + // *********************** + // Read from textures + // *********************** + #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) + vec3 normal = newNormal; + #elif !defined(VERTEX_LIGHTING) + vec3 normal = vNormal; + #if !defined(LOW_QUALITY) && !defined(V_TANGENT) + normal = normalize(normal); + #endif + #endif + + #ifdef SPECULARMAP + vec4 specularColor = texture2D(m_SpecularMap, newTexCoord); + #else + vec4 specularColor = vec4(1.0); + #endif + + #ifdef LIGHTMAP + vec3 lightMapColor; + #ifdef SEPARATE_TEXCOORD + lightMapColor = texture2D(m_LightMap, texCoord2).rgb; + #else + lightMapColor = texture2D(m_LightMap, texCoord).rgb; + #endif + specularColor.rgb *= lightMapColor; + diffuseColor.rgb *= lightMapColor; + #endif + + #ifdef VERTEX_LIGHTING + vec2 light = vertexLightValues.xy; + #ifdef COLORRAMP + light.x = texture2D(m_ColorRamp, vec2(light.x, 0.0)).r; + light.y = texture2D(m_ColorRamp, vec2(light.y, 0.0)).r; + #endif + + #ifndef USE_SCATTERING + gl_FragColor.rgb = AmbientSum * diffuseColor.rgb + + DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) + + SpecularSum * specularColor.rgb * vec3(light.y); + #else + vec3 color = AmbientSum * diffuseColor.rgb + + DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) + + SpecularSum * specularColor.rgb * vec3(light.y); + gl_FragColor.rgb = calculateGroundColor(vec4(color, 1.0)).rgb; + #endif + #else + vec4 lightDir = vLightDir; + lightDir.xyz = normalize(lightDir.xyz); + vec3 viewDir = normalize(vViewDir); + + vec2 light = computeLighting(normal, viewDir, lightDir.xyz) * spotFallOff; + #ifdef COLORRAMP + diffuseColor.rgb *= texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb; + specularColor.rgb *= texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb; + #endif + + // Workaround, since it is not possible to modify varying variables + vec4 SpecularSum2 = vec4(SpecularSum, 1.0); + #ifdef USE_REFLECTION + vec4 refColor = Optics_GetEnvColor(m_EnvMap, refVec.xyz); + + // Interpolate light specularity toward reflection color + // Multiply result by specular map + specularColor = mix(SpecularSum2 * light.y, refColor, refVec.w) * specularColor; + + SpecularSum2 = vec4(1.0); + light.y = 1.0; + #endif + + #ifndef USE_SCATTERING + gl_FragColor.rgb = AmbientSum * diffuseColor.rgb + + DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) + + SpecularSum * specularColor.rgb * vec3(light.y); + #else + vec3 color = AmbientSum * diffuseColor.rgb + + DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) + + SpecularSum * specularColor.rgb * vec3(light.y); + gl_FragColor.rgb = calculateGroundColor(vec4(color, 1.0)).rgb; + #endif + + #endif + gl_FragColor.a = alpha; +} diff --git a/simarboreal/assets/MatDefs/MultiResolution.j3md b/simarboreal/assets/MatDefs/MultiResolution.j3md new file mode 100644 index 0000000..fab96ea --- /dev/null +++ b/simarboreal/assets/MatDefs/MultiResolution.j3md @@ -0,0 +1,352 @@ +MaterialDef Phong Lighting { + + MaterialParameters { + + // Compute vertex lighting in the shader + // For better performance + Boolean VertexLighting + + // Use more efficent algorithms to improve performance + Boolean LowQuality + + // Improve quality at the cost of performance + Boolean HighQuality + + // Output alpha from the diffuse map + Boolean UseAlpha + + // Alpha threshold for fragment discarding + Float AlphaDiscardThreshold (AlphaTestFallOff) + + // Normal map is in BC5/ATI2n/LATC/3Dc compression format + Boolean LATC + + // Use the provided ambient, diffuse, and specular colors + Boolean UseMaterialColors + + // Activate shading along the tangent, instead of the normal + // Requires tangent data to be available on the model. + Boolean VTangent + + // Use minnaert diffuse instead of lambert + Boolean Minnaert + + // Use ward specular instead of phong + Boolean WardIso + + // Use vertex color as an additional diffuse color. + Boolean UseVertexColor + + // Ambient color + Color Ambient (MaterialAmbient) + + // Diffuse color + Color Diffuse (MaterialDiffuse) + + // Specular color + Color Specular (MaterialSpecular) + + // Specular power/shininess + Float Shininess (MaterialShininess) : 1 + + // Diffuse map + Texture2D DiffuseMap + + // Diffuse map + Texture2D BackgroundDiffuseMap + + // Diffuse map + Texture2D NoiseMap + + // Normal map + Texture2D NormalMap + + // Specular/gloss map + Texture2D SpecularMap + + // Parallax/height map + Texture2D ParallaxMap + + //Set to true is parallax map is stored in the alpha channel of the normal map + Boolean PackedNormalParallax + + //Sets the relief height for parallax mapping + Float ParallaxHeight : 0.05 + + //Set to true to activate Steep Parallax mapping + Boolean SteepParallax + + // Texture that specifies alpha values + Texture2D AlphaMap + + // Color ramp, will map diffuse and specular values through it. + Texture2D ColorRamp + + // Texture of the glowing parts of the material + Texture2D GlowMap + + // Set to Use Lightmap + Texture2D LightMap + + // Set to use TexCoord2 for the lightmap sampling + Boolean SeparateTexCoord + + // The glow color of the object + Color GlowColor + + // Parameters for fresnel + // X = bias + // Y = scale + // Z = power + Vector3 FresnelParams + + // Env Map for reflection + TextureCubeMap EnvMap + + // the env map is a spheremap and not a cube map + Boolean EnvMapAsSphereMap + + //shadows + Int FilterMode + Boolean HardwareShadows + + Texture2D ShadowMap0 + Texture2D ShadowMap1 + Texture2D ShadowMap2 + Texture2D ShadowMap3 + //pointLights + Texture2D ShadowMap4 + Texture2D ShadowMap5 + + Float ShadowIntensity + Vector4 Splits + Vector2 FadeInfo + + Matrix4 LightViewProjectionMatrix0 + Matrix4 LightViewProjectionMatrix1 + Matrix4 LightViewProjectionMatrix2 + Matrix4 LightViewProjectionMatrix3 + //pointLight + Matrix4 LightViewProjectionMatrix4 + Matrix4 LightViewProjectionMatrix5 + Vector3 LightPos + + Float PCFEdge + Float ShadowMapSize + + // For hardware skinning + Int NumberOfBones + Matrix4Array BoneMatrices + + + // Ground scattering parameters + Boolean UseScattering + Vector3 SunPosition + Float Exposure + Float KmESun + Float InnerRadius + Float RadiusScale + Float PlanetScale : 1 + Vector3 InvWavelengthsKrESun + Float AverageDensityScale + Float InvAverageDensityHeight; + Vector3 KWavelengths4PI; + + } + + Technique { + + LightMode MultiPass + + VertexShader GLSL110: MatDefs/MultiResolution.vert + FragmentShader GLSL110: MatDefs/MultiResolution.frag + + WorldParameters { + WorldViewProjectionMatrix + NormalMatrix + WorldViewMatrix + ViewMatrix + CameraPosition + WorldMatrix + } + + Defines { + LATC : LATC + VERTEX_COLOR : UseVertexColor + VERTEX_LIGHTING : VertexLighting + ATTENUATION : Attenuation + MATERIAL_COLORS : UseMaterialColors + V_TANGENT : VTangent + MINNAERT : Minnaert + WARDISO : WardIso + LOW_QUALITY : LowQuality + HQ_ATTENUATION : HighQuality + + DIFFUSEMAP : DiffuseMap + NORMALMAP : NormalMap + SPECULARMAP : SpecularMap + PARALLAXMAP : ParallaxMap + NORMALMAP_PARALLAX : PackedNormalParallax + STEEP_PARALLAX : SteepParallax + ALPHAMAP : AlphaMap + COLORRAMP : ColorRamp + LIGHTMAP : LightMap + SEPARATE_TEXCOORD : SeparateTexCoord + + USE_REFLECTION : EnvMap + SPHERE_MAP : SphereMap + + NUM_BONES : NumberOfBones + + USE_SCATTERING : UseScattering + } + } + + Technique PreShadow { + + VertexShader GLSL110 : Common/MatDefs/Shadow/PreShadow.vert + FragmentShader GLSL110 : Common/MatDefs/Shadow/PreShadow.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldViewMatrix + } + + Defines { + COLOR_MAP : ColorMap + DISCARD_ALPHA : AlphaDiscardThreshold + NUM_BONES : NumberOfBones + } + + ForcedRenderState { + FaceCull Off + DepthTest On + DepthWrite On + PolyOffset 5 3 + ColorWrite Off + } + + } + + + Technique PostShadow15{ + VertexShader GLSL150: Common/MatDefs/Shadow/PostShadow15.vert + FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow15.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + } + + Defines { + HARDWARE_SHADOWS : HardwareShadows + FILTER_MODE : FilterMode + PCFEDGE : PCFEdge + DISCARD_ALPHA : AlphaDiscardThreshold + COLOR_MAP : ColorMap + SHADOWMAP_SIZE : ShadowMapSize + FADE : FadeInfo + PSSM : Splits + POINTLIGHT : LightViewProjectionMatrix5 + NUM_BONES : NumberOfBones + } + + ForcedRenderState { + Blend Modulate + DepthWrite Off + PolyOffset -0.1 0 + } + } + + Technique PostShadow{ + VertexShader GLSL110: Common/MatDefs/Shadow/PostShadow.vert + FragmentShader GLSL110: Common/MatDefs/Shadow/PostShadow.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldMatrix + } + + Defines { + HARDWARE_SHADOWS : HardwareShadows + FILTER_MODE : FilterMode + PCFEDGE : PCFEdge + DISCARD_ALPHA : AlphaDiscardThreshold + COLOR_MAP : ColorMap + SHADOWMAP_SIZE : ShadowMapSize + FADE : FadeInfo + PSSM : Splits + POINTLIGHT : LightViewProjectionMatrix5 + NUM_BONES : NumberOfBones + } + + ForcedRenderState { + Blend Modulate + DepthWrite Off + PolyOffset -0.1 0 + } + } + + Technique PreNormalPass { + + VertexShader GLSL110 : Common/MatDefs/SSAO/normal.vert + FragmentShader GLSL110 : Common/MatDefs/SSAO/normal.frag + + WorldParameters { + WorldViewProjectionMatrix + WorldViewMatrix + NormalMatrix + } + + Defines { + DIFFUSEMAP_ALPHA : DiffuseMap + NUM_BONES : NumberOfBones + } + + } + + Technique GBuf { + + VertexShader GLSL110: Common/MatDefs/Light/GBuf.vert + FragmentShader GLSL110: Common/MatDefs/Light/GBuf.frag + + WorldParameters { + WorldViewProjectionMatrix + NormalMatrix + WorldViewMatrix + WorldMatrix + } + + Defines { + VERTEX_COLOR : UseVertexColor + MATERIAL_COLORS : UseMaterialColors + V_TANGENT : VTangent + MINNAERT : Minnaert + WARDISO : WardIso + + DIFFUSEMAP : DiffuseMap + NORMALMAP : NormalMap + SPECULARMAP : SpecularMap + PARALLAXMAP : ParallaxMap + } + } + + Technique Glow { + + VertexShader GLSL110: Common/MatDefs/Misc/Unshaded.vert + FragmentShader GLSL110: Common/MatDefs/Light/Glow.frag + + WorldParameters { + WorldViewProjectionMatrix + } + + Defines { + NEED_TEXCOORD1 + HAS_GLOWMAP : GlowMap + HAS_GLOWCOLOR : GlowColor + + NUM_BONES : NumberOfBones + } + } + +} diff --git a/simarboreal/assets/MatDefs/MultiResolution.vert b/simarboreal/assets/MatDefs/MultiResolution.vert new file mode 100644 index 0000000..d5cbeed --- /dev/null +++ b/simarboreal/assets/MatDefs/MultiResolution.vert @@ -0,0 +1,237 @@ +#define ATTENUATION +//#define HQ_ATTENUATION + +#import "Common/ShaderLib/Skinning.glsllib" +#import "MatDefs/VertScattering.glsllib" + + +uniform mat4 g_WorldViewProjectionMatrix; +uniform mat4 g_WorldViewMatrix; +uniform mat4 g_WorldMatrix; +uniform mat3 g_NormalMatrix; +uniform mat4 g_ViewMatrix; +uniform vec3 g_CameraPosition; + +uniform vec4 m_Ambient; +uniform vec4 m_Diffuse; +uniform vec4 m_Specular; +uniform float m_Shininess; + +uniform vec4 g_LightColor; +uniform vec4 g_LightPosition; +uniform vec4 g_AmbientLightColor; + +varying vec2 texCoord; +#ifdef SEPARATE_TEXCOORD + varying vec2 texCoord2; + attribute vec2 inTexCoord2; +#endif + +varying vec3 AmbientSum; +varying vec4 DiffuseSum; +varying vec3 SpecularSum; + +varying float z; + +attribute vec3 inPosition; +attribute vec2 inTexCoord; +attribute vec3 inNormal; + +varying vec3 lightVec; +//varying vec4 spotVec; + +#ifdef VERTEX_COLOR + attribute vec4 inColor; +#endif + +#ifndef VERTEX_LIGHTING + attribute vec4 inTangent; + + #ifndef NORMALMAP + varying vec3 vNormal; + #endif + //varying vec3 vPosition; + varying vec3 vViewDir; + varying vec4 vLightDir; +#else + varying vec2 vertexLightValues; + uniform vec4 g_LightDirection; +#endif + +#ifdef USE_REFLECTION + uniform vec3 g_CameraPosition; + uniform mat4 g_WorldMatrix; + + uniform vec3 m_FresnelParams; + varying vec4 refVec; + + + /** + * Input: + * attribute inPosition + * attribute inNormal + * uniform g_WorldMatrix + * uniform g_CameraPosition + * + * Output: + * varying refVec + */ + void computeRef(in vec4 modelSpacePos){ + vec3 worldPos = (g_WorldMatrix * modelSpacePos).xyz; + + vec3 I = normalize( g_CameraPosition - worldPos ).xyz; + vec3 N = normalize( (g_WorldMatrix * vec4(inNormal, 0.0)).xyz ); + + refVec.xyz = reflect(I, N); + refVec.w = m_FresnelParams.x + m_FresnelParams.y * pow(1.0 + dot(I, N), m_FresnelParams.z); + } +#endif + +// JME3 lights in world space +void lightComputeDir(in vec3 worldPos, in vec4 color, in vec4 position, out vec4 lightDir){ + float posLight = step(0.5, color.w); + vec3 tempVec = position.xyz * sign(posLight - 0.5) - (worldPos * posLight); + lightVec = tempVec; + #ifdef ATTENUATION + float dist = length(tempVec); + lightDir.w = clamp(1.0 - position.w * dist * posLight, 0.0, 1.0); + lightDir.xyz = tempVec / vec3(dist); + #else + lightDir = vec4(normalize(tempVec), 1.0); + #endif +} + +#ifdef VERTEX_LIGHTING + float lightComputeDiffuse(in vec3 norm, in vec3 lightdir){ + return max(0.0, dot(norm, lightdir)); + } + + float lightComputeSpecular(in vec3 norm, in vec3 viewdir, in vec3 lightdir, in float shiny){ + if (shiny <= 1.0){ + return 0.0; + } + #ifndef LOW_QUALITY + vec3 H = (viewdir + lightdir) * vec3(0.5); + return pow(max(dot(H, norm), 0.0), shiny); + #else + return 0.0; + #endif + } + +vec2 computeLighting(in vec3 wvPos, in vec3 wvNorm, in vec3 wvViewDir, in vec4 wvLightPos){ + vec4 lightDir; + lightComputeDir(wvPos, g_LightColor, wvLightPos, lightDir); + float spotFallOff = 1.0; + if(g_LightDirection.w != 0.0){ + vec3 L=normalize(lightVec.xyz); + vec3 spotdir = normalize(g_LightDirection.xyz); + float curAngleCos = dot(-L, spotdir); + float innerAngleCos = floor(g_LightDirection.w) * 0.001; + float outerAngleCos = fract(g_LightDirection.w); + float innerMinusOuter = innerAngleCos - outerAngleCos; + spotFallOff = clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0); + } + float diffuseFactor = lightComputeDiffuse(wvNorm, lightDir.xyz); + float specularFactor = lightComputeSpecular(wvNorm, wvViewDir, lightDir.xyz, m_Shininess); + //specularFactor *= step(0.01, diffuseFactor); + return vec2(diffuseFactor, specularFactor) * vec2(lightDir.w)*spotFallOff; + } +#endif + +void main(){ + vec4 modelSpacePos = vec4(inPosition, 1.0); + vec3 modelSpaceNorm = inNormal; + + #ifndef VERTEX_LIGHTING + vec3 modelSpaceTan = inTangent.xyz; + #endif + + #ifdef NUM_BONES + #ifndef VERTEX_LIGHTING + Skinning_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan); + #else + Skinning_Compute(modelSpacePos, modelSpaceNorm); + #endif + #endif + + #ifdef USE_SCATTERING + vec4 wPos = g_WorldMatrix * modelSpacePos; + calculateVertexGroundScattering(wPos.xyz, g_CameraPosition); + #endif + + gl_Position = g_WorldViewProjectionMatrix * modelSpacePos; + texCoord = inTexCoord; + #ifdef SEPARATE_TEXCOORD + texCoord2 = inTexCoord2; + #endif + + vec3 wvPosition = (g_WorldViewMatrix * modelSpacePos).xyz; + + z = length(wvPosition); + + vec3 wvNormal = normalize(g_NormalMatrix * modelSpaceNorm); + vec3 viewDir = normalize(-wvPosition); + + //vec4 lightColor = g_LightColor[gl_InstanceID]; + //vec4 lightPos = g_LightPosition[gl_InstanceID]; + //vec4 wvLightPos = (g_ViewMatrix * vec4(lightPos.xyz, lightColor.w)); + //wvLightPos.w = lightPos.w; + + vec4 wvLightPos = (g_ViewMatrix * vec4(g_LightPosition.xyz,clamp(g_LightColor.w,0.0,1.0))); + wvLightPos.w = g_LightPosition.w; + vec4 lightColor = g_LightColor; + + #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING) + vec3 wvTangent = normalize(g_NormalMatrix * modelSpaceTan); + vec3 wvBinormal = cross(wvNormal, wvTangent); + + mat3 tbnMat = mat3(wvTangent, wvBinormal * -inTangent.w,wvNormal); + + //vPosition = wvPosition * tbnMat; + //vViewDir = viewDir * tbnMat; + vViewDir = -wvPosition * tbnMat; + lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir); + vLightDir.xyz = (vLightDir.xyz * tbnMat).xyz; + #elif !defined(VERTEX_LIGHTING) + vNormal = wvNormal; + + //vPosition = wvPosition; + vViewDir = viewDir; + + lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir); + + #ifdef V_TANGENT + vNormal = normalize(g_NormalMatrix * inTangent.xyz); + vNormal = -cross(cross(vLightDir.xyz, vNormal), vNormal); + #endif + #endif + + //computing spot direction in view space and unpacking spotlight cos +// spotVec = (g_ViewMatrix * vec4(g_LightDirection.xyz, 0.0) ); +// spotVec.w = floor(g_LightDirection.w) * 0.001; +// lightVec.w = fract(g_LightDirection.w); + + lightColor.w = 1.0; + #ifdef MATERIAL_COLORS + AmbientSum = (m_Ambient * g_AmbientLightColor).rgb; + DiffuseSum = m_Diffuse * lightColor; + SpecularSum = (m_Specular * lightColor).rgb; + #else + AmbientSum = vec3(0.2, 0.2, 0.2) * g_AmbientLightColor.rgb; // Default: ambient color is dark gray + DiffuseSum = lightColor; + SpecularSum = vec3(0.0); + #endif + + #ifdef VERTEX_COLOR + AmbientSum *= inColor.rgb; + DiffuseSum *= inColor; + #endif + + #ifdef VERTEX_LIGHTING + vertexLightValues = computeLighting(wvPosition, wvNormal, viewDir, wvLightPos); + #endif + + #ifdef USE_REFLECTION + computeRef(modelSpacePos); + #endif +} diff --git a/simarboreal/assets/MatDefs/Null.frag b/simarboreal/assets/MatDefs/Null.frag new file mode 100644 index 0000000..04a9e2e --- /dev/null +++ b/simarboreal/assets/MatDefs/Null.frag @@ -0,0 +1,10 @@ +#import "Common/ShaderLib/MultiSample.glsllib" + +uniform COLORTEXTURE m_Texture; +varying vec2 texCoord; + +void main() { + vec4 texVal = getColor(m_Texture, texCoord); + gl_FragColor = texVal; +} + diff --git a/simarboreal/assets/MatDefs/Null.j3md b/simarboreal/assets/MatDefs/Null.j3md new file mode 100644 index 0000000..98fe07f --- /dev/null +++ b/simarboreal/assets/MatDefs/Null.j3md @@ -0,0 +1,24 @@ +MaterialDef Depth Blur { + + MaterialParameters { + Int NumSamples + Int NumSamplesDepth + Texture2D Texture + Texture2D DepthTexture + } + + Technique { + VertexShader GLSL100: Common/MatDefs/Post/Post.vert + FragmentShader GLSL100: MatDefs/Null.frag + + WorldParameters { + WorldViewProjectionMatrix + } + + Defines { + RESOLVE_MS : NumSamples + RESOLVE_DEPTH_MS : NumSamplesDepth + } + } + +} diff --git a/simarboreal/assets/MatDefs/Shadows.frag b/simarboreal/assets/MatDefs/Shadows.frag new file mode 100644 index 0000000..5041007 --- /dev/null +++ b/simarboreal/assets/MatDefs/Shadows.frag @@ -0,0 +1,72 @@ +#import "Common/ShaderLib/MultiSample.glsllib" + +//#define SHOW_BOX +//#define SHOW_DELTA + +uniform vec2 g_FrustumNearFar; +uniform vec4 g_ViewPort; + +uniform vec4 m_ShadowColor; +uniform COLORTEXTURE m_FrameTexture; +uniform DEPTHTEXTURE m_DepthTexture; + +varying vec3 texCoord; +varying vec3 vViewDir; +varying vec3 boxScale; + +void main(){ + vec4 color = vec4(1.0); + + vec2 uv = vec2(gl_FragCoord.x/g_ViewPort.z, gl_FragCoord.y/g_ViewPort.w); + + float zBuffer = getDepth( m_DepthTexture, uv ).r; + + // + // z_buffer_value = a + b / z; + // + // Where: + // a = zFar / ( zFar - zNear ) + // b = zFar * zNear / ( zNear - zFar ) + // z = distance from the eye to the object + // + // Which means: + // zb - a = b / z; + // z * (zb - a) = b + // z = b / (zb - a) + // + float a = g_FrustumNearFar.y / (g_FrustumNearFar.y - g_FrustumNearFar.x); + float b = g_FrustumNearFar.y * g_FrustumNearFar.x / (g_FrustumNearFar.x - g_FrustumNearFar.y); + float z = b / (zBuffer - a); + + float us = b / (gl_FragCoord.z - a); + + float modelScale = 1.0; + + float delta = (z-us) * modelScale; + + #if defined(SHOW_DELTA) + color = vec4(delta, 0.0, 0.0, 1.0); + #elif defined(SHOW_BOX) + color = vec4(texCoord * boxScale,1.0); + #else + + vec3 view = normalize(vViewDir); + vec3 scene = texCoord + view * delta; + vec3 stu = scene * boxScale; + + float xTex = (0.5 - stu.x) * 2.0; + float zTex = (0.5 - stu.z) * 2.0; + float t = stu.y; + + float low = (t - 0.75) * 1.33333; + float hi = (t - 0.75) * 4.0; + float yTex = low * step(t, 0.75) + hi * step(0.75, t); + + float col = sqrt((xTex * xTex) + (zTex * zTex) + (yTex * yTex)); + float shadow = (1.0 - col); + color = vec4(m_ShadowColor); + color.a *= clamp(shadow, 0.0, 0.8); + #endif + + gl_FragColor = color; +} diff --git a/simarboreal/assets/MatDefs/Shadows.j3md b/simarboreal/assets/MatDefs/Shadows.j3md new file mode 100644 index 0000000..62880c5 --- /dev/null +++ b/simarboreal/assets/MatDefs/Shadows.j3md @@ -0,0 +1,29 @@ +MaterialDef Simple Shadows { + + MaterialParameters { + Int NumSamples + Int NumSamplesDepth + + Color ShadowColor + + Texture2D FrameTexture + Texture2D DepthTexture + } + + Technique { + VertexShader GLSL120: MatDefs/Shadows.vert + FragmentShader GLSL130: MatDefs/Shadows.frag + + WorldParameters { + ViewProjectionMatrix + FrustumNearFar + ViewPort + } + + Defines { + RESOLVE_MS : NumSamples + RESOLVE_DEPTH_MS : NumSamplesDepth + } + } + +} diff --git a/simarboreal/assets/MatDefs/Shadows.vert b/simarboreal/assets/MatDefs/Shadows.vert new file mode 100644 index 0000000..e04b6ba --- /dev/null +++ b/simarboreal/assets/MatDefs/Shadows.vert @@ -0,0 +1,22 @@ + +uniform mat4 g_ViewProjectionMatrix; + +attribute vec3 inPosition; // the world position +attribute vec3 inTexCoord; // the model space position, relative to a corner +attribute vec3 inTexCoord2; // the x,y,z scale to get from model space to 0->1 space +attribute vec3 inNormal; // the view direction in model-space + +varying vec3 texCoord; +varying vec3 vViewDir; +varying vec3 boxScale; + + + +void main(){ + vec4 modelSpacePos = vec4(inPosition, 1.0); + gl_Position = g_ViewProjectionMatrix * modelSpacePos; + + vViewDir = inNormal; + texCoord = inTexCoord; + boxScale = inTexCoord2; +} diff --git a/simarboreal/assets/Models/female-parts.j3o b/simarboreal/assets/Models/female-parts.j3o new file mode 100644 index 0000000..16f027a Binary files /dev/null and b/simarboreal/assets/Models/female-parts.j3o differ diff --git a/simarboreal/assets/Models/male-parts-no-bones.j3o b/simarboreal/assets/Models/male-parts-no-bones.j3o new file mode 100644 index 0000000..1767f16 Binary files /dev/null and b/simarboreal/assets/Models/male-parts-no-bones.j3o differ diff --git a/simarboreal/assets/Textures/brown-dirt-norm.jpg b/simarboreal/assets/Textures/brown-dirt-norm.jpg new file mode 100644 index 0000000..3ae610d Binary files /dev/null and b/simarboreal/assets/Textures/brown-dirt-norm.jpg differ diff --git a/simarboreal/assets/Textures/grass-flat.jpg b/simarboreal/assets/Textures/grass-flat.jpg new file mode 100644 index 0000000..103be5d Binary files /dev/null and b/simarboreal/assets/Textures/grass-flat.jpg differ diff --git a/simarboreal/assets/Textures/grass.jpg b/simarboreal/assets/Textures/grass.jpg new file mode 100644 index 0000000..c07909b Binary files /dev/null and b/simarboreal/assets/Textures/grass.jpg differ diff --git a/simarboreal/assets/Textures/test-pattern.png b/simarboreal/assets/Textures/test-pattern.png new file mode 100644 index 0000000..a245956 Binary files /dev/null and b/simarboreal/assets/Textures/test-pattern.png differ diff --git a/simarboreal/build.gradle b/simarboreal/build.gradle new file mode 100644 index 0000000..1a7358e --- /dev/null +++ b/simarboreal/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'application' +} + +application { + mainClass = 'com.simsilica.arboreal.TreeEditor' + applicationDefaultJvmArgs = ['-Xmx512m', '-XX:MaxDirectMemorySize=512m'] +} + +sourceSets.main.resources { + srcDirs += 'src/main/java' + exclude '**/*.java' + exclude '**/*.tmp' +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + runtimeOnly files('assets') +} + +tasks.register('extractNatives', Copy) { + from zipTree(file('libs/jME3-lwjgl-natives.jar')) + into "${buildDir}/natives" + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +run { + dependsOn extractNatives + workingDir = rootDir + jvmArgs "-Djava.library.path=${buildDir}/natives" +} diff --git a/simarboreal/libs/Lemur.jar b/simarboreal/libs/Lemur.jar new file mode 100644 index 0000000..2001759 Binary files /dev/null and b/simarboreal/libs/Lemur.jar differ diff --git a/simarboreal/libs/LemurProps.jar b/simarboreal/libs/LemurProps.jar new file mode 100644 index 0000000..d577b90 Binary files /dev/null and b/simarboreal/libs/LemurProps.jar differ diff --git a/simarboreal/libs/Pager.jar b/simarboreal/libs/Pager.jar new file mode 100644 index 0000000..36971f7 Binary files /dev/null and b/simarboreal/libs/Pager.jar differ diff --git a/simarboreal/libs/SimArboreal.jar b/simarboreal/libs/SimArboreal.jar new file mode 100644 index 0000000..e10714c Binary files /dev/null and b/simarboreal/libs/SimArboreal.jar differ diff --git a/simarboreal/libs/arboreal-assets.jar b/simarboreal/libs/arboreal-assets.jar new file mode 100644 index 0000000..b47bc2f Binary files /dev/null and b/simarboreal/libs/arboreal-assets.jar differ diff --git a/simarboreal/libs/assets.jar b/simarboreal/libs/assets.jar new file mode 100644 index 0000000..d16ffb8 Binary files /dev/null and b/simarboreal/libs/assets.jar differ diff --git a/simarboreal/libs/groovy-all-2.1.9.jar b/simarboreal/libs/groovy-all-2.1.9.jar new file mode 100644 index 0000000..8f1eb05 Binary files /dev/null and b/simarboreal/libs/groovy-all-2.1.9.jar differ diff --git a/simarboreal/libs/guava-12.0.jar b/simarboreal/libs/guava-12.0.jar new file mode 100644 index 0000000..fefd6b2 Binary files /dev/null and b/simarboreal/libs/guava-12.0.jar differ diff --git a/simarboreal/libs/jME3-core.jar b/simarboreal/libs/jME3-core.jar new file mode 100644 index 0000000..585ea7d Binary files /dev/null and b/simarboreal/libs/jME3-core.jar differ diff --git a/simarboreal/libs/jME3-desktop.jar b/simarboreal/libs/jME3-desktop.jar new file mode 100644 index 0000000..528edfc Binary files /dev/null and b/simarboreal/libs/jME3-desktop.jar differ diff --git a/simarboreal/libs/jME3-effects.jar b/simarboreal/libs/jME3-effects.jar new file mode 100644 index 0000000..4c09a75 Binary files /dev/null and b/simarboreal/libs/jME3-effects.jar differ diff --git a/simarboreal/libs/jME3-lwjgl-natives.jar b/simarboreal/libs/jME3-lwjgl-natives.jar new file mode 100644 index 0000000..8d55d8f Binary files /dev/null and b/simarboreal/libs/jME3-lwjgl-natives.jar differ diff --git a/simarboreal/libs/jME3-lwjgl.jar b/simarboreal/libs/jME3-lwjgl.jar new file mode 100644 index 0000000..d7093c0 Binary files /dev/null and b/simarboreal/libs/jME3-lwjgl.jar differ diff --git a/simarboreal/libs/jME3-plugins.jar b/simarboreal/libs/jME3-plugins.jar new file mode 100644 index 0000000..b341a01 Binary files /dev/null and b/simarboreal/libs/jME3-plugins.jar differ diff --git a/simarboreal/libs/jinput.jar b/simarboreal/libs/jinput.jar new file mode 100644 index 0000000..4c75006 Binary files /dev/null and b/simarboreal/libs/jinput.jar differ diff --git a/simarboreal/libs/log4j-1.2.12.jar b/simarboreal/libs/log4j-1.2.12.jar new file mode 100644 index 0000000..9b5a720 Binary files /dev/null and b/simarboreal/libs/log4j-1.2.12.jar differ diff --git a/simarboreal/libs/lwjgl.jar b/simarboreal/libs/lwjgl.jar new file mode 100644 index 0000000..f76c937 Binary files /dev/null and b/simarboreal/libs/lwjgl.jar differ diff --git a/simarboreal/libs/meta-jb-json-1.0.1.jar b/simarboreal/libs/meta-jb-json-1.0.1.jar new file mode 100644 index 0000000..3a70510 Binary files /dev/null and b/simarboreal/libs/meta-jb-json-1.0.1.jar differ diff --git a/simarboreal/libs/slf4j-api-1.7.5.jar b/simarboreal/libs/slf4j-api-1.7.5.jar new file mode 100644 index 0000000..8766455 Binary files /dev/null and b/simarboreal/libs/slf4j-api-1.7.5.jar differ diff --git a/simarboreal/libs/slf4j-log4j12-1.7.5.jar b/simarboreal/libs/slf4j-log4j12-1.7.5.jar new file mode 100644 index 0000000..afce5c2 Binary files /dev/null and b/simarboreal/libs/slf4j-log4j12-1.7.5.jar differ diff --git a/simarboreal/nbproject/assets-impl.xml b/simarboreal/nbproject/assets-impl.xml new file mode 100644 index 0000000..0a47d8d --- /dev/null +++ b/simarboreal/nbproject/assets-impl.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/simarboreal/nbproject/build-impl.xml b/simarboreal/nbproject/build-impl.xml new file mode 100644 index 0000000..45400b2 --- /dev/null +++ b/simarboreal/nbproject/build-impl.xml @@ -0,0 +1,1521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set platform.home + Must set platform.bootcp + Must set platform.java + Must set platform.javac + + The J2SE Platform is not correctly set up. + Your active platform is: ${platform.active}, but the corresponding property "platforms.${platform.active}.home" is not found in the project's properties files. + Either open the project in the IDE and setup the Platform with the same name or add it manually. + For example like this: + ant -Duser.properties.file=<path_to_property_file> jar (where you put the property "platforms.${platform.active}.home" in a .properties file) + or ant -Dplatforms.${platform.active}.home=<path_to_JDK_home> jar (where no properties file is used) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.resources.dir + Must set src.java.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + ${platform.java} -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/simarboreal/nbproject/genfiles.properties b/simarboreal/nbproject/genfiles.properties new file mode 100644 index 0000000..b988779 --- /dev/null +++ b/simarboreal/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=94bf7c61 +build.xml.script.CRC32=79a29eb7 +build.xml.stylesheet.CRC32=958a1d3e@1.32.1.45 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=0f2662fc +nbproject/build-impl.xml.script.CRC32=bb94fa75 +nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48 diff --git a/simarboreal/nbproject/project.properties b/simarboreal/nbproject/project.properties new file mode 100644 index 0000000..c6301cb --- /dev/null +++ b/simarboreal/nbproject/project.properties @@ -0,0 +1,124 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +ant.customtasks.libs=launch4j +application.desc=Editor for the tree parameters necessary for generating trees. +application.homepage=https://code.google.com/p/simsilica-tools/ +application.splash=C:\\Development\\google\\simsilica-tools\\trunk\\SimArboreal-Editor\\TreeEditor-Splash.png +application.title=SimArboreal-Editor +application.vendor=Simsilica, LLC +assets.jar.name=assets.jar +assets.excludes=**/*.j3odata,**/*.mesh,**/*.skeleton,**/*.mesh.xml,**/*.skeleton.xml,**/*.scene,**/*.material,**/*.obj,**/*.mtl,**/*.3ds,**/*.dae,**/*.blend,**/*.blend*[0-9],**/.backups/**,**/*.psd +assets.folder.name=assets +assets.compress=true +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form,**/.backups/** +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +compile.on.save=true +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/${application.title}.jar +dist.javadoc.dir=${dist.dir}/javadoc +endorsed.classpath= +excludes= +file.reference.arboreal-assets.jar=..\\SimArboreal\\dist\\lib\\arboreal-assets.jar +file.reference.groovy-all-2.1.9.jar=lib\\groovy-all-2.1.9.jar +file.reference.guava-12.0.jar=lib\\guava-12.0.jar +file.reference.log4j-1.2.12.jar=lib\\log4j-1.2.12.jar +file.reference.meta-jb-json-1.0.1.jar=lib\\meta-jb-json-1.0.1.jar +file.reference.simfx-assets.jar=..\\SimFX\\dist\\lib\\simfx-assets.jar +file.reference.slf4j-api-1.7.5.jar=lib\\slf4j-api-1.7.5.jar +file.reference.slf4j-log4j12-1.7.5.jar=lib\\slf4j-log4j12-1.7.5.jar +includes=** +jar.compress=false +javac.classpath=\ + ${reference.SimArboreal.jar}:\ + ${libs.jme3-lwjgl.classpath}:\ + ${libs.jme3-effects.classpath}:\ + ${libs.jme3-desktop.classpath}:\ + ${file.reference.arboreal-assets.jar}:\ + ${reference.Pager.jar}:\ + ${reference.SimFX.jar}:\ + ${libs.jme3-core.classpath}:\ + ${reference.Lemur.jar}:\ + ${reference.LemurProps.jar}:\ + ${file.reference.groovy-all-2.1.9.jar}:\ + ${file.reference.guava-12.0.jar}:\ + ${file.reference.log4j-1.2.12.jar}:\ + ${file.reference.meta-jb-json-1.0.1.jar}:\ + ${file.reference.slf4j-api-1.7.5.jar}:\ + ${file.reference.slf4j-log4j12-1.7.5.jar}:\ + ${file.reference.simfx-assets.jar} +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.6 +javac.target=1.6 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +jaxbwiz.endorsed.dirs="${netbeans.home}/../ide12/modules/ext/jaxb/api" +jnlp.codebase.type=local +jnlp.descriptor=application +jnlp.enabled=false +jnlp.offline-allowed=false +jnlp.signed=false +launch4j.exe.enabled=true +linux.launcher.enabled=true +mac.app.enabled=true +main.class=com.simsilica.arboreal.TreeEditor +meta.inf.dir=${src.dir}/META-INF +manifest.file=MANIFEST.MF +mkdist.disabled=false +platform.active=JDK_1.7 +project.Lemur=../../Lemur +project.LemurProps=../../Lemur/extensions/LemurProps +project.Pager=../Pager +project.SimArboreal=../SimArboreal +project.SimFX=../SimFX +reference.Lemur.jar=${project.Lemur}/dist/Lemur.jar +reference.LemurProps.jar=${project.LemurProps}/dist/LemurProps.jar +reference.Pager.jar=${project.Pager}/dist/Pager.jar +reference.SimArboreal.jar=${project.SimArboreal}/dist/SimArboreal.jar +reference.SimFX.jar=${project.SimFX}/dist/SimFX.jar +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir}:\ + ${assets.folder.name} +# Space-separated list of JVM arguments used when running the project +# (you may also define separate properties like run-sys-prop.name=value instead of -Dname=value +# or test-sys-prop.name=value to set system properties for unit tests): +run.jvmargs=-Xmx512m -XX:MaxDirectMemorySize=512m +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.java.dir=src\\main\\java +src.resources.dir=src\\main\\resources diff --git a/simarboreal/nbproject/project.xml b/simarboreal/nbproject/project.xml new file mode 100644 index 0000000..1e7497b --- /dev/null +++ b/simarboreal/nbproject/project.xml @@ -0,0 +1,71 @@ + + + org.netbeans.modules.java.j2seproject + + + + + + + + + + + + + + + + + SimArboreal-Editor + + + + + + + + + + Lemur + jar + + jar + clean + jar + + + LemurProps + jar + + jar + clean + jar + + + Pager + jar + + jar + clean + jar + + + SimArboreal + jar + + jar + clean + jar + + + SimFX + jar + + jar + clean + jar + + + + diff --git a/simarboreal/release/SimArboreal-Editor-Linux.zip b/simarboreal/release/SimArboreal-Editor-Linux.zip new file mode 100644 index 0000000..5976a29 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux.zip differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.jar b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.jar new file mode 100644 index 0000000..e3e1d65 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.sh b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.sh new file mode 100644 index 0000000..61026ac --- /dev/null +++ b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.sh @@ -0,0 +1,3 @@ +#!/bin/sh +java -Xmx512m -XX:MaxDirectMemorySize=512m -jar SimArboreal-Editor.jar + \ No newline at end of file diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/Lemur.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/Lemur.jar new file mode 100644 index 0000000..2001759 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/Lemur.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/LemurProps.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/LemurProps.jar new file mode 100644 index 0000000..d577b90 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/LemurProps.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/Pager.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/Pager.jar new file mode 100644 index 0000000..36971f7 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/Pager.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/SimArboreal.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/SimArboreal.jar new file mode 100644 index 0000000..e10714c Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/SimArboreal.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/arboreal-assets.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/arboreal-assets.jar new file mode 100644 index 0000000..b47bc2f Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/arboreal-assets.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/assets.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/assets.jar new file mode 100644 index 0000000..d16ffb8 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/assets.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/groovy-all-2.1.9.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/groovy-all-2.1.9.jar new file mode 100644 index 0000000..8f1eb05 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/groovy-all-2.1.9.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/guava-12.0.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/guava-12.0.jar new file mode 100644 index 0000000..fefd6b2 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/guava-12.0.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-core.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-core.jar new file mode 100644 index 0000000..585ea7d Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-core.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-desktop.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-desktop.jar new file mode 100644 index 0000000..528edfc Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-desktop.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-effects.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-effects.jar new file mode 100644 index 0000000..4c09a75 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-effects.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl-natives.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl-natives.jar new file mode 100644 index 0000000..8d55d8f Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl-natives.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl.jar new file mode 100644 index 0000000..d7093c0 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-plugins.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-plugins.jar new file mode 100644 index 0000000..b341a01 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-plugins.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jinput.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jinput.jar new file mode 100644 index 0000000..4c75006 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jinput.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/log4j-1.2.12.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/log4j-1.2.12.jar new file mode 100644 index 0000000..9b5a720 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/log4j-1.2.12.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/lwjgl.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/lwjgl.jar new file mode 100644 index 0000000..f76c937 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/lwjgl.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/meta-jb-json-1.0.1.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/meta-jb-json-1.0.1.jar new file mode 100644 index 0000000..3a70510 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/meta-jb-json-1.0.1.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-api-1.7.5.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-api-1.7.5.jar new file mode 100644 index 0000000..8766455 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-api-1.7.5.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-log4j12-1.7.5.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-log4j12-1.7.5.jar new file mode 100644 index 0000000..afce5c2 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-log4j12-1.7.5.jar differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/liblwjgl64.so b/simarboreal/release/SimArboreal-Editor-Linux/liblwjgl64.so new file mode 100644 index 0000000..314b892 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/liblwjgl64.so differ diff --git a/simarboreal/release/SimArboreal-Editor-Linux/libopenal64.so b/simarboreal/release/SimArboreal-Editor-Linux/libopenal64.so new file mode 100644 index 0000000..e0693c0 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/libopenal64.so differ diff --git a/simarboreal/release/SimArboreal-Editor-MacOSX.zip b/simarboreal/release/SimArboreal-Editor-MacOSX.zip new file mode 100644 index 0000000..c4b1545 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-MacOSX.zip differ diff --git a/simarboreal/release/SimArboreal-Editor-Windows.zip b/simarboreal/release/SimArboreal-Editor-Windows.zip new file mode 100644 index 0000000..553af11 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Windows.zip differ diff --git a/simarboreal/release/SimArboreal-Editor-changelog.txt b/simarboreal/release/SimArboreal-Editor-changelog.txt new file mode 100644 index 0000000..5777520 --- /dev/null +++ b/simarboreal/release/SimArboreal-Editor-changelog.txt @@ -0,0 +1,71 @@ +Revision ??? +------------- +- Added a dependency to the SimFX package and converted to use + its LightingState and SkyState (with scattering) +- Grass plane supports atmospherics and a toggle was added to + the visualization options panel. +- Added atmospheric support for trees along with a toggle. + + + +Revision 143 +------------- +- Added an action to save the tree atlas images + as PNG files. +- Fixed how the atlas textures are generated so that they + save as embedded textures in the j3o. +- Fixed impostor meshes to use short buffers instead of int. +- Added toggleable noise-based wind +- Added a video recodring option F12 +- Added editors for the tree wind-related parameters +- Changed the tree parameters file extension to just plain + .simap The old simap.json extension is still supported for + loading and the file format itself hasn't changed. + + +Revision 126 +------------- +- Reworked the FileActionsState to better allow embedding + in external applications. Save and load methods were added + and the buttons are now not added to the UI unless the state + is enabled. This should help facilitate a parallel JME SDK + plug-in effort. + + +Revision 94 +------------ +- Moved RollupPanel and TabbedPanel out into Lemur core. +- Moved the builder classes out into the new simsilica-tools Pager + library. +- Moved PropertyPanel into its own Lemur extension project LemurProps. +- Converted to use the now-standard Lemur 'glass' style with just a few + local custom extensions. +- Moved the Builderstate out to the builder project. + + +Revision 69 +------------- +- Added Y offset parameter that is separate from trunk + height. +- Added LOD support including two mesh reduction strategies: + Flat-Poly : renders the tree branches as a set of axis-aligned + billboarded flat quads. + Impostor : renders a single quad with a view-direction indexed texture. + (Note: impostors currently don't save properly to the j3o) +- Better visual separate of child properies in the UI. +- Reorganized UI to include outer rollup panels to separate vis + settings from tree parameters. +- Added an avatar toggle to the UI. +- Added shadow intensity and lighting direction settings to the + UI. +- Added a simplified 'drop shadow' filter that can be enabled instead + of regular shadows. +- File write operations now warn before overwriting existing files. +- Cleaned out the wire frame meshes from the exported j3o during save. +- Renamed the tree geometry elements to make more sense when viewing + the tree object in something like Scene Composer. + + +Revision 33 +------------- +- Initial release \ No newline at end of file diff --git a/simarboreal/release/SimArboreal-Editor.jar b/simarboreal/release/SimArboreal-Editor.jar new file mode 100644 index 0000000..e3e1d65 Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor.jar differ diff --git a/simarboreal/settings.gradle.bak b/simarboreal/settings.gradle.bak new file mode 100644 index 0000000..917403c --- /dev/null +++ b/simarboreal/settings.gradle.bak @@ -0,0 +1 @@ +rootProject.name = 'sim-arboreal-editor' diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/AtlasGeneratorState.java b/simarboreal/src/main/java/com/simsilica/arboreal/AtlasGeneratorState.java new file mode 100644 index 0000000..ff99bf2 --- /dev/null +++ b/simarboreal/src/main/java/com/simsilica/arboreal/AtlasGeneratorState.java @@ -0,0 +1,576 @@ +/* + * $Id$ + * + * Copyright (c) 2014, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.simsilica.arboreal; + +import com.simsilica.builder.BuilderState; +import com.jme3.app.Application; +import com.jme3.bounding.BoundingBox; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue.Bucket; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.debug.WireBox; +import com.jme3.scene.shape.Quad; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture2D; +import com.jme3.util.BufferUtils; +import com.simsilica.arboreal.mesh.BillboardedLeavesMeshGenerator; +import com.simsilica.arboreal.mesh.SkinnedTreeMeshGenerator; +import com.simsilica.arboreal.mesh.Vertex; +import com.simsilica.builder.Builder; +import com.simsilica.builder.BuilderReference; +import com.simsilica.lemur.GuiGlobals; +import com.simsilica.lemur.core.VersionedReference; +import com.simsilica.lemur.event.BaseAppState; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * + * @author Paul Speed + */ +public class AtlasGeneratorState extends BaseAppState { + + static Logger log = LoggerFactory.getLogger(AtlasGeneratorState.class); + + private VersionedReference treeParametersRef; + private Material treeMaterial; + private Material leafMaterial; + + private Builder builder; + private AtlasTreeBuilderReference builderRef; + + private Mesh trunkMesh; + private Mesh leafMesh; + + private FrameBuffer diffuseFb; + private FrameBuffer normalFb; + private CellView[] cellViews = new CellView[8]; + private Image diffuseMap; + private Texture2D diffuseTexture; + private Image normalMap; + private Texture2D normalTexture; + private int needTextureUpdate; + + private BitmapFont font; + + private boolean debugTextures = false; + + private boolean useNormalMaps = true; + + public AtlasGeneratorState() { + } + + public Image getDiffuseMap() { + return diffuseMap; + } + + public Image getNormalMap() { + return normalMap; + } + + protected Image createFrameBufferImage( FrameBuffer fb ) { + int width = fb.getWidth(); + int height = fb.getHeight(); + int size = width * height * 4; + ByteBuffer buffer = BufferUtils.createByteBuffer(size); + Image.Format format = fb.getColorBuffer().getFormat(); + + // I guess readFrameBuffer always writes in the same + // format regardless of the frame buffer format + format = Format.BGRA8; + return new Image(format, width, height, buffer); + } + + protected void updateTextures() { + + Renderer renderer = getApplication().getRenderer(); + if( diffuseMap == null ) { + diffuseMap = createFrameBufferImage(diffuseFb); + diffuseTexture = new Texture2D(diffuseMap); + getState(ForestGridState.class).getImpostorMaterial().setTexture("DiffuseMap", diffuseTexture); + } + renderer.readFrameBuffer(diffuseFb, diffuseMap.getData(0)); + diffuseMap.setUpdateNeeded(); + + if( normalMap == null ) { + normalMap = createFrameBufferImage(normalFb); + normalTexture = new Texture2D(normalMap); + if( useNormalMaps ) { + getState(ForestGridState.class).getImpostorMaterial().setTexture("NormalMap", normalTexture); + } + } + renderer.readFrameBuffer(normalFb, normalMap.getData(0)); + normalMap.setUpdateNeeded(); + + needTextureUpdate = 0; + } + + @Override + protected void initialize( Application app ) { + + this.treeParametersRef = getState(TreeParametersState.class).getTreeParametersRef(); + this.treeMaterial = getState(ForestGridState.class).getTreeMaterial(); + this.leafMaterial = getState(ForestGridState.class).getLeafMaterial(); + + this.builder = getState(BuilderState.class).getBuilder(); + this.builderRef = new AtlasTreeBuilderReference(); + + + this.font = GuiGlobals.getInstance().loadFont("Interface/Fonts/Default.fnt"); + + Camera camera = app.getCamera().clone(); + camera.resize(256, 256, true); + camera.resize(1024, 256, false); + + + FrameBuffer fb1 = new FrameBuffer(1024, 256, 1); + diffuseFb = fb1; + Texture2D fbTex1 = new Texture2D(1024, 256, Format.RGBA8); + fb1.setDepthBuffer(Format.Depth); + fb1.setColorTexture(fbTex1); + + FrameBuffer fb2 = new FrameBuffer(1024, 256, 1); + normalFb = fb2; + Texture2D fbTex2 = new Texture2D(1024, 256, Format.RGBA8); + fb2.setDepthBuffer(Format.Depth); + fb2.setColorTexture(fbTex2); + + + if( debugTextures ) { + Quad testQuad = new Quad(512, 128); + Geometry testGeom = new Geometry("test", testQuad); + Material mat = GuiGlobals.getInstance().createMaterial(fbTex1, false).getMaterial(); + testGeom.setMaterial(mat); + ((TreeEditor)app).getGuiNode().attachChild(testGeom); + + testQuad = new Quad(512, 128); + testGeom = new Geometry("test", testQuad); + testGeom.setLocalTranslation(0, 128, 0); + mat = GuiGlobals.getInstance().createMaterial(fbTex2, false).getMaterial(); + testGeom.setMaterial(mat); + ((TreeEditor)app).getGuiNode().attachChild(testGeom); + + updateTextures(); + + testQuad = new Quad(512, 128); + testGeom = new Geometry("test", testQuad); + testGeom.setLocalTranslation(0, 256, 0); + mat = GuiGlobals.getInstance().createMaterial(diffuseTexture, false).getMaterial(); + testGeom.setMaterial(mat); + ((TreeEditor)app).getGuiNode().attachChild(testGeom); + + testQuad = new Quad(512, 128); + testGeom = new Geometry("test", testQuad); + testGeom.setLocalTranslation(0, 384, 0); + mat = GuiGlobals.getInstance().createMaterial(normalTexture, false).getMaterial(); + testGeom.setMaterial(mat); + ((TreeEditor)app).getGuiNode().attachChild(testGeom); + } + + DirectionalLight sun = new DirectionalLight(); + //sun.setDirection(new Vector3f(0, -1f, -1).normalizeLocal()); + sun.setDirection(new Vector3f(0, 0, -1).normalizeLocal()); + + AmbientLight ambient = new AmbientLight(); + + if( useNormalMaps ) { + sun.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1)); + ambient.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1)); + } else { + sun.setColor(new ColorRGBA(1, 1, 1, 1)); + ambient.setColor(new ColorRGBA(0.25f, 0.25f, 0.25f, 1)); + } + + //-x * FastMath.TWO_PI - FastMath.QUARTER_PI + // The texture quads actually run a, c, d, b starting with + // the +, + quadrant + cellViews[0] = new CellView(fb1, camera, sun, ambient, FastMath.QUARTER_PI, 0.25f * 0); + cellViews[1] = new CellView(fb1, camera, sun, ambient, -FastMath.QUARTER_PI, 0.25f * 1); + cellViews[2] = new CellView(fb1, camera, sun, ambient, FastMath.PI - FastMath.QUARTER_PI, 0.25f * 2); + cellViews[3] = new CellView(fb1, camera, sun, ambient, FastMath.PI + FastMath.QUARTER_PI, 0.25f * 3); + + cellViews[4] = new NormalMapCellView(fb2, camera, sun, ambient, FastMath.QUARTER_PI, 0.25f * 0); + cellViews[5] = new NormalMapCellView(fb2, camera, sun, ambient, -FastMath.QUARTER_PI, 0.25f * 1); + cellViews[6] = new NormalMapCellView(fb2, camera, sun, ambient, FastMath.PI - FastMath.QUARTER_PI, 0.25f * 2); + cellViews[7] = new NormalMapCellView(fb2, camera, sun, ambient, FastMath.PI + FastMath.QUARTER_PI, 0.25f * 3); + + } + + @Override + protected void cleanup( Application app ) { + for( CellView view : cellViews ) { + app.getRenderManager().removeMainView(view.getViewPort()); + } + } + + @Override + protected void enable() { + } + + @Override + protected void disable() { + } + + protected void updateTree( Mesh trunkMesh, Mesh leafMesh ) { + if( this.trunkMesh == trunkMesh ) { + return; + } + + releaseMesh(this.trunkMesh); + releaseMesh(this.leafMesh); + this.trunkMesh = trunkMesh; + this.leafMesh = leafMesh; + + for( CellView view : cellViews ) { + if( view != null ) { + view.updateMesh(trunkMesh, leafMesh); + } + } + + // Texture updates need to happen one frame late... + // but we get the notification that we need the check + // early in _this_ frame. ie: updateTree() is called + // before our update(), render(). If we want to render + // a frame later then we need to skip this frame before + // updating textures. + needTextureUpdate = 2; + } + + protected void releaseMesh( Mesh mesh ) { + if( mesh == null ) { + return; + } + + // Delete the old buffers + for( VertexBuffer vb : mesh.getBufferList() ) { + if( log.isTraceEnabled() ) { + log.trace("--destroying buffer:" + vb); + } + BufferUtils.destroyDirectBuffer( vb.getData() ); + } + } + + private float nextUpdateCheck = 0.1f; + private float lastTpf; + @Override + public void update( float tpf ) { + lastTpf = tpf; + + nextUpdateCheck += tpf; + if( nextUpdateCheck <= 0.1f ) { + return; + } + nextUpdateCheck = 0; + + boolean changed = treeParametersRef.update(); + if( changed ) { + builder.build(builderRef); + } + } + + @Override + public void render( RenderManager rm ) { + if( cellViews != null ) { + // We update the logical state here because it is + // done after the other updates. So if another app + // state or control has modified our root then we + // are guaranteed to run after. + for( CellView view : cellViews ) { + if( view != null ) { + view.update(lastTpf); + } + } + } + + // Texture updates need to happen one frame later... + // but we get the notification that we need the check + // early in _this_ frame. + if( needTextureUpdate > 0 ) { + needTextureUpdate--; + if( needTextureUpdate == 0 ) { + updateTextures(); + } + } + } + + private class CellView { + private ViewPort viewport; + private Camera camera; + private Node root; + private Mesh leafMesh; + private Mesh trunkMesh; + private Geometry trunkGeom; + private Geometry leafGeom; + private Geometry wireBounds; + private boolean debugBounds = false; + private boolean debugCell = false; + + public CellView( FrameBuffer fb, Camera templateCamera, DirectionalLight sun, AmbientLight ambient, float angle, float x ) { + + this.camera = templateCamera.clone(); + camera.setViewPort(x, x + 0.25f, 0, 1); + + this.root = new Node("CellRoot:" + x ); + this.viewport = getApplication().getRenderManager().createMainView("AtlasCell[" + x + "]", camera); + this.viewport.setOutputFrameBuffer(fb); + this.root.rotate(0, -angle, 0); + + if( debugCell ) { + BitmapText label = new BitmapText(font); + label.setText("u:" + x + "\na:" + angle); + label.setLocalScale(0.01f); + Quaternion labelRot = root.getLocalRotation().inverse(); + label.setLocalRotation(labelRot); + label.setLocalTranslation(labelRot.mult(new Vector3f(0, 1, 2))); + root.attachChild(label); + } + + viewport.attachScene(root); + root.addLight(sun); + root.addLight(ambient); + + viewport.setClearFlags(true, true, true); + viewport.setBackgroundColor(new ColorRGBA(0, 0, 0, 0)); + this.camera.lookAtDirection(new Vector3f(0, 0, -1), Vector3f.UNIT_Y); + } + + public ViewPort getViewPort() { + return viewport; + } + + public void update( float tpf ) { + root.updateLogicalState(tpf); + root.updateGeometricState(); + } + + protected Material getTreeMaterial() { + return treeMaterial; + } + + protected Material getLeafMaterial() { + return leafMaterial; + } + + public void updateMesh( Mesh trunkMesh, Mesh leafMesh ) { + if( trunkGeom == null ) { + // Create it + trunkGeom = new Geometry("Trunk", trunkMesh); + trunkGeom.setMaterial(getTreeMaterial()); + root.attachChild(trunkGeom); + } else { + // Just swap out the mesh + trunkGeom.setMesh(trunkMesh); + } + this.trunkMesh = trunkMesh; + this.leafMesh = leafMesh; + if( leafMesh == null ) { + if( leafGeom != null ) { + leafGeom.removeFromParent(); + leafGeom = null; + } + } else { + if( leafGeom == null ) { + // Create it + leafGeom = new Geometry("Leaves", leafMesh); + leafGeom.setMaterial(getLeafMaterial()); + leafGeom.setQueueBucket(Bucket.Transparent); + root.attachChild(leafGeom); + } else { + // Just swap out the mesh + leafGeom.setMesh(leafMesh); + } + } + updateCamera(); + } + + protected void updateCamera() { + + BoundingBox bb = (BoundingBox)trunkMesh.getBound(); + if( leafGeom != null ) { + BoundingBox bb2 = (BoundingBox)leafMesh.getBound(); + bb = (BoundingBox)bb.merge(bb2); + } + + Vector3f min = bb.getMin(null); + Vector3f max = bb.getMax(null); + + float xSize = Math.max(Math.abs(min.x), Math.abs(max.x)); + float ySize = max.y - min.y; + float zSize = Math.max(Math.abs(min.z), Math.abs(max.z)); + + float size = ySize * 0.5f; + size = Math.max(size, xSize); + size = Math.max(size, zSize); + + // In the projection matrix, [1][1] should be: + // (2 * Zn) / camHeight + // where Zn is distance to near plane. + float m11 = camera.getViewProjectionMatrix().m11; + + // We want our position to be such that + // 'size' is otherwise = cameraHeight when rendered. + float z = m11 * size; + + // Add the z extents so that we adjust for the near plane + // of the bounding box... well we will be rotating so + // let's just be sure and take the max of x and z + //float offset = Math.max(bb.getXExtent(), bb.getZExtent()); + //z += offset; + // This creates problems because it makes way too much + // space around the tree. A proper solution would require + // a bunch of math and in the end would also have to be duplicated + // on the quad generation side or somehow stored with the atlas. + + Vector3f center = bb.getCenter(); + + float sizeOffset = size - (ySize*0.5f); + + Vector3f camLoc = new Vector3f(0, center.y + sizeOffset, z); + camera.setLocation(camLoc); + + if( debugBounds ) { + WireBox box; + if( wireBounds == null ) { + box = new WireBox(); + wireBounds = new Geometry("wire box", box); + Material mat = GuiGlobals.getInstance().createMaterial(ColorRGBA.Yellow, false).getMaterial(); + wireBounds.setMaterial(mat); + root.attachChild(wireBounds); + } else { + box = (WireBox)wireBounds.getMesh(); + } + box.updatePositions(bb.getXExtent(), bb.getYExtent(), bb.getZExtent()); + box.setBound(new BoundingBox(new Vector3f(0,0,0), 0, 0, 0)); + wireBounds.setLocalTranslation(bb.getCenter()); + wireBounds.setLocalRotation(leafGeom.getLocalRotation()); + } + } + + } + + + private class NormalMapCellView extends CellView { + public NormalMapCellView( FrameBuffer fb, Camera templateCamera, DirectionalLight sun, AmbientLight ambient, float angle, float x ) { + super(fb, templateCamera, sun, ambient, angle, x); + } + + @Override + protected Material getTreeMaterial() { + Material normalMaterial = treeMaterial.clone(); + normalMaterial.selectTechnique("PreNormalPass", getApplication().getRenderManager()); + return normalMaterial; + } + + @Override + protected Material getLeafMaterial() { + Material normalMaterial = leafMaterial.clone(); + normalMaterial.selectTechnique("PreNormalPass", getApplication().getRenderManager()); + return normalMaterial; + } + } + + + private class AtlasTreeBuilderReference implements BuilderReference { + + private Mesh trunkMesh; + private Mesh leafMesh; + + @Override + public int getPriority() { + // A relatively low priority + return 100; + } + + @Override + public void build() { + + TreeParameters treeParameters = treeParametersRef.get(); + + TreeGenerator treeGen = new TreeGenerator(); + Tree tree = treeGen.generateTree(treeParameters); + + SkinnedTreeMeshGenerator meshGen = new SkinnedTreeMeshGenerator(); + + List tips = new ArrayList(); + trunkMesh = meshGen.generateMesh(tree, + treeParameters.getLod(0), + treeParameters.getYOffset(), + treeParameters.getTextureURepeat(), + treeParameters.getTextureVScale(), + tips); + + if( treeParameters.getGenerateLeaves() ) { + BillboardedLeavesMeshGenerator leafGen = new BillboardedLeavesMeshGenerator(); + leafMesh = leafGen.generateMesh(tips, treeParameters.getLeafScale()); + } else { + leafMesh = null; + } + } + + @Override + public void apply() { + // Set the new trunk + updateTree(trunkMesh, leafMesh); + } + + @Override + public void release() { + + } + } +} diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/AvatarState.java b/simarboreal/src/main/java/com/simsilica/arboreal/AvatarState.java new file mode 100644 index 0000000..9fa6459 --- /dev/null +++ b/simarboreal/src/main/java/com/simsilica/arboreal/AvatarState.java @@ -0,0 +1,133 @@ +/* + * ${Id} + * + * Copyright (c) 2014, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.simsilica.arboreal; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.asset.AssetManager; +import com.jme3.bounding.BoundingBox; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.Spatial.CullHint; +import com.simsilica.lemur.GuiGlobals; +import com.simsilica.lemur.event.BaseAppState; + + +/** + * Shows some sample people for scale. + * + * @author Paul Speed + */ +public class AvatarState extends BaseAppState { + + private Node avatars; + private Spatial male; + private Spatial female; + + public AvatarState() { + } + + public void setShowAvatars( boolean b ) { + if( b ) { + avatars.setCullHint(CullHint.Inherit); + } else { + avatars.setCullHint(CullHint.Always); + } + } + + @Override + protected void initialize( Application app ) { + + AssetManager assets = app.getAssetManager(); + + // Add an avatar for scale + avatars = new Node("Avatars"); + avatars.move(2, 0, 0); + + female = (Node)assets.loadModel("Models/female-parts.j3o"); + BoundingBox bb = (BoundingBox)female.getWorldBound(); + float height = bb.getYExtent() * 2; + float femaleScale = 1.62f / height; + female.move(0, bb.getYExtent(), 0); + female.setLocalScale(femaleScale); + Material mat = GuiGlobals.getInstance().createMaterial(ColorRGBA.Gray, true).getMaterial(); + mat.setColor("Ambient", ColorRGBA.Gray); + female.setMaterial(mat); + female.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + avatars.attachChild(female); + + // Add an avatar for scale + male = (Node)assets.loadModel("Models/male-parts-no-bones.j3o"); + bb = (BoundingBox)male.getWorldBound(); + height = bb.getYExtent() * 2; + float maleScale = 1.77f / height; + male.move(bb.getCenter().negate()); + male.move(1, bb.getYExtent(), 0); + male.setLocalScale(maleScale); + male.setMaterial(mat); + male.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + avatars.attachChild(male); + + // For testing + //Spatial tree = assets.loadModel("Models/test1.j3o"); + //tree.setLocalTranslation(-20, 0, -20); + //avatars.attachChild(tree); + + + TreeOptionsState options = getState(TreeOptionsState.class); + options.addOptionToggle("Avatars", this, "setShowAvatars").setChecked(true); + + } + + @Override + protected void cleanup( Application app ) { + } + + @Override + protected void enable() { + Node rootNode = ((SimpleApplication)getApplication()).getRootNode(); + rootNode.attachChild(avatars); + } + + @Override + protected void disable() { + avatars.removeFromParent(); + } +} diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/DebugHudState.java b/simarboreal/src/main/java/com/simsilica/arboreal/DebugHudState.java new file mode 100644 index 0000000..0f3c766 --- /dev/null +++ b/simarboreal/src/main/java/com/simsilica/arboreal/DebugHudState.java @@ -0,0 +1,202 @@ +/* + * $Id$ + * + * Copyright (c) 2014, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.simsilica.arboreal; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.util.MemoryUtils; +import com.simsilica.lemur.Container; +import com.simsilica.lemur.GuiGlobals; +import com.simsilica.lemur.HAlignment; +import com.simsilica.lemur.Label; +import com.simsilica.lemur.core.VersionedReference; +import com.simsilica.lemur.event.BaseAppState; +import com.simsilica.lemur.input.InputMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * + * @author Paul Speed + */ +public class DebugHudState extends BaseAppState { + + static Logger log = LoggerFactory.getLogger(DebugHudState.class); + + private VersionedReference worldLoc; + private Runtime runtime = Runtime.getRuntime(); + + private Label location; + private Label memory; + private Label directMem; + + private long lastUsedMem; + private long lastMeg100; + private long lastDirectMem; + private long lastDirectMeg100; + private long nextUpdate = System.currentTimeMillis() + 16; // 60 FPS max + private long nextMemTime = System.currentTimeMillis() + 1000; + + private long frameCounter; + private long lastFrameCheck; + private double lastFps; + + private Container debugHud; + + public DebugHudState() { + } + + public void toggleHud() { + setEnabled( !isEnabled() ); + } + + @Override + protected void initialize( Application app ) { + + // Always register for our hot key as long as + // we are attached. + InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper(); + inputMapper.addDelegate( MainFunctions.F_HUD, this, "toggleHud" ); + + worldLoc = getState( MovementState.class ).getWorldPosition().createReference(); + + debugHud = new Container(); + + location = debugHud.addChild(new Label( "000.00 000.00 00.00" )); + location.setTextHAlignment( HAlignment.Right ); + resetLocation(); + + memory = debugHud.addChild(new Label( "Mem: 0.0 meg (0.0 %)" )); + memory.setTextHAlignment( HAlignment.Right ); + + directMem = debugHud.addChild(new Label( "DMem: 0.0 meg / 0" )); + directMem.setTextHAlignment( HAlignment.Right ); + } + + @Override + protected void cleanup( Application app ) { + InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper(); + inputMapper.removeDelegate( MainFunctions.F_HUD, this, "toggleHud" ); + } + + protected void resetLocation() { + Vector3f v = worldLoc.get(); + String loc = String.format( "%.2f, %.2f, %.2f", v.x, v.y, v.z ); + location.setText(loc); + } + + @Override + public void update( float tpf ) { + + frameCounter++; + long time = System.currentTimeMillis(); + if( time < nextUpdate ) + return; + nextUpdate = time + 16; // 60 FPS max + + if( worldLoc.update() ) { + resetLocation(); + } + + /*if( time > lastFrameCheck + 1000 ) + { + long delta = time - lastFrameCheck; + lastFrameCheck = time; + + double fps = frameCounter / (delta / 1000.0); + frameCounter = 0; + if( fps != lastFps ) + { + lastFps = fps; + String s = String.format( "FPS: %.2f", fps ); + fpsText.setText(s); + } + }*/ + + + // Refresh memory and other things less often---------------------------- + //----------------------------------------------------------------------- + if( time < nextMemTime ) + return; + nextMemTime = time + 1000; + + long usedMemory = runtime.totalMemory() - runtime.freeMemory(); + if( lastUsedMem != usedMemory ) { + lastUsedMem = usedMemory; + + long maxMemory = runtime.maxMemory(); + long meg100 = (usedMemory * 100) / (1024 * 1024); + if( lastMeg100 != meg100 ) { + lastMeg100 = meg100; + double meg = meg100 / 100.0; + double percent = (usedMemory * 100.0 / maxMemory); + String mem = String.format( "Mem: %.2f meg (%.1f %%)", meg, percent ); + memory.setText( mem ); + } + } + + long directUsage = MemoryUtils.getDirectMemoryUsage(); + if( directUsage != lastDirectMem ) { + lastDirectMem = directUsage; + + long meg100 = (directUsage * 100) / (1024 * 1024); + if( lastDirectMeg100 != meg100 ) { + long directCount = MemoryUtils.getDirectMemoryCount(); + double meg = meg100 / 100.0; + String mem = String.format( "DMem: %.2f meg / %d", meg, directCount ); + directMem.setText( mem ); + } + } + + Camera cam = getApplication().getCamera(); + Vector3f pref = debugHud.getPreferredSize(); + debugHud.setLocalTranslation(cam.getWidth() - pref.x - 10, cam.getHeight() - 10, 0); + } + + @Override + protected void enable() { + ((SimpleApplication)getApplication()).getGuiNode().attachChild(debugHud); + } + + @Override + protected void disable() { + debugHud.removeFromParent(); + } +} diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/DropShadowFilter.java b/simarboreal/src/main/java/com/simsilica/arboreal/DropShadowFilter.java new file mode 100644 index 0000000..ad19bc9 --- /dev/null +++ b/simarboreal/src/main/java/com/simsilica/arboreal/DropShadowFilter.java @@ -0,0 +1,407 @@ +/* + * $Id$ + * + * Copyright (c) 2013 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.simsilica.arboreal; + +import com.jme3.asset.AssetManager; +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.material.Material; +import com.jme3.material.RenderState.BlendMode; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Matrix4f; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.post.Filter; +import com.jme3.renderer.Camera; +import com.jme3.renderer.Camera.FrustumIntersect; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.GeometryComparator; +import com.jme3.renderer.queue.GeometryList; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.renderer.queue.RenderQueue.ShadowMode; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Spatial; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.shadow.ShadowUtil; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Texture; +import com.jme3.util.BufferUtils; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + + +/** + * + * @author Paul Speed + */ +public class DropShadowFilter extends Filter { + + private static final int VERTS_PER_SHADOW = 8; // one per box corner + private static final int TRIS_PER_SHADOW = 12; // two per face + private static final int INDEXES_PER_SHADOW = TRIS_PER_SHADOW * 3; + + private static final Vector3f[] BASE_CORNERS = new Vector3f[] { + new Vector3f(-1, -1, 1), // 0 + new Vector3f( 1, -1, 1), // 1 + new Vector3f( 1, -1, -1), // 2 + new Vector3f(-1, -1, -1), // 3 + new Vector3f(-1, 1, 1), // 4 + new Vector3f( 1, 1, 1), // 5 + new Vector3f( 1, 1, -1), // 6 + new Vector3f(-1, 1, -1) // 7 + }; + + private static final short[] BASE_INDEXES = new short[] { + // top + 4, 5, 6, 4, 6, 7, + // bottom + 3, 2, 1, 3, 1, 0, + // +z + 0, 1, 5, 0, 5, 4, + // -z + 2, 3, 7, 2, 7, 6, + // -x + 3, 0, 4, 3, 4, 7, + // +x + 1, 2, 6, 1, 6, 5 + }; + + private Geometry shadowGeom; + private Material shadowMaterial; + private Mesh mesh; + private int maxShadows; + + private ColorRGBA shadowColor = new ColorRGBA(0, 0, 0, 0.75f); + + private VertexBuffer vbPos; + private VertexBuffer vbNormal; + private VertexBuffer vbTexCoord; + private VertexBuffer vbTexCoord2; + private VertexBuffer vbIndex; + + private GeometryList casters; + + public DropShadowFilter() { + this(500); + } + + public DropShadowFilter( int maxShadows ) { + this.maxShadows = maxShadows; + } + + public void setShadowIntensity( float f ) { + shadowColor.a = f; + } + + public float getShadowIntensity() { + return shadowColor.a; + } + + @Override + protected boolean isRequiresDepthTexture() { + return true; + } + + @Override + protected void initFilter(AssetManager assets, RenderManager rm, ViewPort vp, int w, int h) { + + // Cheating... side effect of being lazy and using a filter + // without actually needing to filter anything. + material = new Material( assets, "MatDefs/Null.j3md" ); + + mesh = new Mesh(); + + // Setup the mesh for the max shadows size + mesh.setBuffer( Type.Position, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) ); + mesh.setBuffer( Type.Normal, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) ); + mesh.setBuffer( Type.TexCoord, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) ); + mesh.setBuffer( Type.TexCoord2, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) ); + mesh.setBuffer( Type.Index, 3, BufferUtils.createShortBuffer(maxShadows * INDEXES_PER_SHADOW) ); + + vbPos = mesh.getBuffer(Type.Position); + vbNormal = mesh.getBuffer(Type.Normal); + vbTexCoord = mesh.getBuffer(Type.TexCoord); + vbTexCoord2 = mesh.getBuffer(Type.TexCoord2); + vbIndex = mesh.getBuffer(Type.Index); + + + shadowGeom = new Geometry("shadowVolumes", mesh); + Material m = shadowMaterial = new Material( assets, "MatDefs/Shadows.j3md" ); + m.setColor( "ShadowColor", shadowColor ); + m.getAdditionalRenderState().setDepthWrite(false); + m.getAdditionalRenderState().setDepthTest(false); + m.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); + shadowGeom.setMaterial(m); + shadowGeom.setLocalTranslation(0, 100, 0); + + shadowGeom.updateLogicalState(0.1f); + shadowGeom.updateGeometricState(); + + // Set our custom comparator for shadow casters + casters = new GeometryList(new CasterComparator()); + } + + @Override + protected Material getMaterial() { + return material; + } + + @Override + protected void postFrame( RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer ) { + + RenderQueue rq = viewPort.getQueue(); + for (Spatial scene : viewPort.getScenes()) { + //ShadowUtil.getGeometriesInCamFrustum(scene, viewPort.getCamera(), ShadowMode.Cast, casters); + } + if( casters.size() == 0 ) + return; + + Camera cam = viewPort.getCamera(); + BoundingSphere cullCheck = new BoundingSphere(); + Vector3f pos = new Vector3f(); + + Texture frameTex = prevFilterBuffer.getColorBuffer().getTexture(); + Texture depthTex = prevFilterBuffer.getDepthBuffer().getTexture(); + shadowMaterial.setTexture("FrameTexture", frameTex); + if( frameTex.getImage().getMultiSamples() > 1 ) { + shadowMaterial.setInt("NumSamples", frameTex.getImage().getMultiSamples()); + } else { + shadowMaterial.clearParam("NumSamples"); + } + + shadowMaterial.setTexture("DepthTexture", depthTex); + if( depthTex.getImage().getMultiSamples() > 1 ) { + shadowMaterial.setInt("NumSamplesDepth", depthTex.getImage().getMultiSamples()); + } else { + shadowMaterial.clearParam("NumSamplesDepth"); + } + + int size = casters.size(); + if( size > maxShadows ) { + // Give the shadows their best chance by sorting them. + casters.setCamera(cam); + casters.sort(); + } + + FloatBuffer bPos = (FloatBuffer)vbPos.getData().rewind(); + FloatBuffer bNormal = (FloatBuffer)vbNormal.getData().rewind(); + FloatBuffer bTexCoord = (FloatBuffer)vbTexCoord.getData().rewind(); + FloatBuffer bTexCoord2 = (FloatBuffer)vbTexCoord2.getData().rewind(); + ShortBuffer bIndex = (ShortBuffer)vbIndex.getData().rewind(); + + + Matrix4f viewMatrix = cam.getViewMatrix(); + Matrix4f worldMatrix = new Matrix4f(); + Matrix4f worldViewMatrix = new Matrix4f(); + float[] angles = new float[3]; + Vector3f vTemp = new Vector3f(); + Vector3f vert = new Vector3f(); + Vector3f viewDir = new Vector3f(); + Vector3f boxScale = new Vector3f(); + + int rendered = 0; + for( int i = 0; i < size; i++ ) { + Geometry g = casters.get(i); + + // Use the geometry bounds. We assumg it is still y-up + // and merely rotated. It's a decent enough approximiation + // in many cases and will produce better shadows for oblong + // objects than a simple round radius would. + BoundingBox bounds = (BoundingBox)g.getModelBound(); + + float scale = g.getWorldScale().x; + float xEx = bounds.getXExtent() * scale; + float yEx = bounds.getYExtent() * scale; + float zEx = bounds.getZExtent() * scale; + float volumeHeight = Math.max(yEx, Math.min(xEx,zEx)); + + float xOffset = bounds.getCenter().x * scale; + float yOffset = bounds.getCenter().y * scale; + float zOffset = bounds.getCenter().z * scale; + + yOffset -= yEx; + yOffset -= volumeHeight * 0.5f; + yOffset += 0.01f; + + pos.set(g.getWorldTranslation()); + pos.addLocal(xOffset, yOffset, zOffset); + + // A conservative approximation that works because our shadow volume + // is really just a round blob + float radius = Math.max(xEx, Math.max(yEx, zEx)); + cullCheck.setCenter(pos); + cullCheck.setRadius(radius); + + int save = cam.getPlaneState(); + cam.setPlaneState(0); + FrustumIntersect intersect = cam.contains(cullCheck); + cam.setPlaneState(save); + + if( intersect == FrustumIntersect.Outside ) { + continue; + } + + boxScale.set(0.5f/xEx, 0.5f/volumeHeight, 0.5f/zEx); + + Quaternion quat = g.getWorldRotation(); + angles = quat.toAngles(angles); + + Quaternion rotation = new Quaternion().fromAngles(0, angles[1], 0); + Quaternion invRotation = rotation.inverse(); + worldMatrix.setTranslation(pos); + worldMatrix.setRotationQuaternion(rotation); + + worldViewMatrix.set(viewMatrix); + worldViewMatrix.multLocal(worldMatrix); + + // Setup the vertexes for each corner + for( int j = 0; j < VERTS_PER_SHADOW; j++ ) { + vTemp.set(BASE_CORNERS[j].x * xEx, + BASE_CORNERS[j].y * volumeHeight, + BASE_CORNERS[j].z * zEx); + + // Get the transformed coordinate in world space + vert = worldMatrix.mult(vTemp, vert); + bPos.put(vert.x).put(vert.y).put(vert.z); + + // Now calculate the view direction + vert = vert.subtractLocal(cam.getLocation()); + vert.normalizeLocal(); + viewDir = invRotation.mult(vert, viewDir); + bNormal.put(viewDir.x).put(viewDir.y).put(viewDir.z); + + // Model space is easy to calculate + bTexCoord.put(BASE_CORNERS[j].x * xEx + xEx); + bTexCoord.put(BASE_CORNERS[j].y * volumeHeight + volumeHeight); + bTexCoord.put(BASE_CORNERS[j].z * zEx + zEx); + + // And so is the scale... since it's always the same + bTexCoord2.put(boxScale.x).put(boxScale.y).put(boxScale.z); + } + + // Fill in the index buffer + for( int j = 0; j < INDEXES_PER_SHADOW; j++ ) { + bIndex.put( (short)(BASE_INDEXES[j] + rendered * VERTS_PER_SHADOW) ); + } + + rendered++; + if( rendered >= maxShadows ) { + break; + } + } + + if( rendered > 0 ) { + // Need to zero out the left-overs + for( int i = rendered; i < maxShadows; i++ ) { + for( int j = 0; j < INDEXES_PER_SHADOW; j++ ) { + bIndex.put((short)0); + } + } + + // Update the buffers + bPos.rewind(); + bNormal.rewind(); + bTexCoord.rewind(); + bTexCoord2.rewind(); + bIndex.rewind(); + + vbPos.updateData(bPos); + vbNormal.updateData(bNormal); + vbTexCoord.updateData(bTexCoord); + vbTexCoord2.updateData(bTexCoord2); + vbIndex.updateData(bIndex); + + shadowGeom.updateGeometricState(); + renderManager.renderGeometry(shadowGeom); + } + + casters.clear(); + } + + private class CasterComparator implements GeometryComparator { + + private Camera cam; + private final Vector3f tempVec = new Vector3f(); + private final Vector3f tempVec2 = new Vector3f(); + + public void setCamera( Camera cam ) { + this.cam = cam; + } + + public float distanceToCam( Geometry spat ) { + if( spat == null ) { + return Float.NEGATIVE_INFINITY; + } + + if( spat.queueDistance != Float.NEGATIVE_INFINITY ) { + return spat.queueDistance; + } + + Vector3f camPosition = cam.getLocation(); + Vector3f viewVector = cam.getDirection(tempVec2); + Vector3f spatPosition; + + if( spat.getWorldBound() != null ) { + spatPosition = spat.getWorldBound().getCenter(); + } else { + spatPosition = spat.getWorldTranslation(); + } + + spatPosition.subtract(camPosition, tempVec); + spat.queueDistance = tempVec.dot(viewVector); + + return spat.queueDistance; + } + + public int compare( Geometry o1, Geometry o2 ) { + // Front to back sort + float d1 = distanceToCam(o1); + float d2 = distanceToCam(o2); + + if( d1 == d2 ) { + return 0; + } else if( d1 < d2 ) { + return -1; + } else { + return 1; + } + } + } + + +} diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/FileActionsState.java b/simarboreal/src/main/java/com/simsilica/arboreal/FileActionsState.java new file mode 100644 index 0000000..5439902 --- /dev/null +++ b/simarboreal/src/main/java/com/simsilica/arboreal/FileActionsState.java @@ -0,0 +1,393 @@ +/* + * ${Id} + * + * Copyright (c) 2014, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.simsilica.arboreal; + +import com.jme3.app.Application; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.scene.Geometry; +import com.jme3.scene.SceneGraphVisitorAdapter; +import com.jme3.scene.Spatial; +import com.jme3.system.JmeSystem; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.simsilica.lemur.Button; +import com.simsilica.lemur.Command; +import com.simsilica.lemur.Container; +import com.simsilica.lemur.HAlignment; +import com.simsilica.lemur.event.BaseAppState; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileFilter; +import org.progeeks.json.JsonParser; +import org.progeeks.json.JsonPrinter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Manages the file-related actions. + * + * @author Paul Speed + */ +public class FileActionsState extends BaseAppState { + + static Logger log = LoggerFactory.getLogger(FileActionsState.class); + + private Container buttons; + + public FileActionsState() { + } + + @Override + protected void initialize( Application app ) { + + buttons = new Container(); + + Button saveParms = buttons.addChild(new Button("Save Parms", "glass")); + saveParms.addClickCommands(new SaveTreeParameters()); + saveParms.setTextHAlignment(HAlignment.Center); + + Button loadParms = buttons.addChild(new Button("Load Parms", "glass"), 1); + loadParms.addClickCommands(new LoadTreeParameters()); + loadParms.setTextHAlignment(HAlignment.Center); + + Button saveJ3o = buttons.addChild(new Button("Export j3o", "glass"), 2); + saveJ3o.addClickCommands(new SaveJ3o()); + saveJ3o.setTextHAlignment(HAlignment.Center); + + Button saveTreeAtlas = buttons.addChild(new Button("Save Tree Atlas", "glass")); + saveTreeAtlas.addClickCommands(new SaveTreeAtlas()); + saveTreeAtlas.setTextHAlignment(HAlignment.Center); + } + + @Override + protected void cleanup( Application app ) { + } + + @Override + protected void enable() { + getState(TreeOptionsState.class).getContents().addChild(buttons); + } + + @Override + protected void disable() { + getState(TreeOptionsState.class).getContents().removeChild(buttons); + } + + /** + * Filters out the stuff that we probably don't want to... or + * really shouldn't be saving in a j3o. We should also remove + * the Impostor LODs at least until there is a way to save and/or + * embed the impostor image... but I don't right now. + */ + protected Spatial filterClone( Spatial tree ) { + Spatial result = tree.deepClone(); + result.depthFirstTraversal(new SceneGraphVisitorAdapter() { + @Override + public void visit( Geometry g ) { + if( g.getName().startsWith("wire:") ) { + g.removeFromParent(); + } + } + }); + return result; + } + + private Map lastRoots = new HashMap(); + protected File chooseFile( final String description, final boolean save, String... extensions ) { + //final String ext = (!extension.startsWith(".") ? "." : "") + extension.toLowerCase(); + final String[] exts = new String[extensions.length]; + for( int i = 0; i < exts.length; i++ ) { + exts[i] = (!extensions[i].startsWith(".") ? "." : "") + extensions[i].toLowerCase(); + } + + File lastRoot = lastRoots.get(exts[0]); + if( lastRoot == null ) { + lastRoot = new File("."); + } + + log.info("Creating file chooser dialog..."); + final JFileChooser openDialog = new JFileChooser(); + + openDialog.setDialogTitle("Choose Location"); + if( save ) { + openDialog.setDialogType(JFileChooser.SAVE_DIALOG); + } else { + openDialog.setDialogType(JFileChooser.OPEN_DIALOG); + } + openDialog.setFileFilter(new FileFilter() { + + @Override + public boolean accept( File file ) { + if( file.isDirectory() ) { + return true; + } + String s = file.getName().toLowerCase(); + for( String e : exts ) { + if( s.endsWith(e) ) { + return true; + } + } + return false; + } + + @Override + public String getDescription() { + return description; + } + }); + openDialog.setCurrentDirectory(lastRoot); + + log.info("Opening file chooser dialog..."); + + final int[] dialogResult = new int[1]; //JFileChooser.CANCEL_OPTION ; + if( !SwingUtilities.isEventDispatchThread() ) { + try { + SwingUtilities.invokeAndWait( new Runnable() { + @Override + public void run() { + if( save ) { + dialogResult[0] = openDialog.showSaveDialog(null); + } else { + dialogResult[0] = openDialog.showOpenDialog(null); + } + } + }); + } catch( Exception e ) { + throw new RuntimeException("Error invoking", e); + } + } else { + if( save ) { + dialogResult[0] = openDialog.showSaveDialog(null); + } else { + dialogResult[0] = openDialog.showOpenDialog(null); + } + } + + if( dialogResult[0] != JFileChooser.APPROVE_OPTION ) { + return null; + } + + File result = openDialog.getSelectedFile(); + lastRoots.put(exts[0], result.getParentFile()); + + if( save && !result.getName().toLowerCase().endsWith(exts[0]) ) { + result = new File(result.getParent(), result.getName() + exts[0]); + } + + return result; + } + + protected void writeJson( File f, Map map ) throws IOException { + + FileWriter out = new FileWriter(f); + try { + JsonPrinter json = new JsonPrinter(); + json.write(map, out); + } finally { + out.close(); + } + } + + protected Map readJson( File f ) throws IOException { + FileReader in = new FileReader(f); + try { + JsonParser json = new JsonParser(); + return (Map)json.parse(in); + } finally { + in.close(); + } + } + + public void saveJ3o( File f ) throws IOException { + BinaryExporter exporter = BinaryExporter.getInstance(); + log.info("Writing:" + f); + exporter.save(filterClone(getState(ForestGridState.class).getMainTreeNode()), f); + } + + public void saveTreeParameters( File f ) throws IOException { + TreeParameters treeParameters = getState(TreeParametersState.class).getTreeParameters(); + Map map = treeParameters.toMap(); + log.info("Writing:" + f); + writeJson(f, map); + } + + public void loadTreeParameters( File f ) throws IOException { + Map map = readJson(f); + TreeParameters treeParameters = getState(TreeParametersState.class).getTreeParameters(); + treeParameters.fromMap(map); + getState(TreeParametersState.class).refreshTreePanels(); + getState(ForestGridState.class).rebuild(); + } + + public void savePng( File f, Image img ) throws IOException { + OutputStream out = new FileOutputStream(f); + try { + JmeSystem.writeImageFile(out, "png", img.getData(0), img.getWidth(), img.getHeight()); + } finally { + out.close(); + } + } + + public void saveTreeAtlas( File f ) throws IOException { + + Image diffuse = getState(AtlasGeneratorState.class).getDiffuseMap(); + savePng(f, diffuse); + + String normalName = f.getName(); + if( normalName.toLowerCase().endsWith(".png") ) { + normalName = normalName.substring(0, normalName.length() - ".png".length()); + } + f = new File(f.getParentFile(), normalName + "-normals.png"); + + Image normal = getState(AtlasGeneratorState.class).getNormalMap(); + savePng(f, normal); + } + + private class SaveJ3o implements Command