Fehler im LOD System behoben
This commit is contained in:
@@ -267,6 +267,7 @@ public class EditorApp extends Application {
|
||||
private ToggleButton importLod2Btn;
|
||||
private Button modelImportLod1ClearBtn;
|
||||
private Button modelImportLod2ClearBtn;
|
||||
private Button modelImportLod2GenBtn;
|
||||
// Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen)
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorLights = new java.util.ArrayList<>();
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||
@@ -692,8 +693,11 @@ public class EditorApp extends Application {
|
||||
// Generieren-Buttons: nur aktiv wenn genau dieses LOD-Level noch nicht gesetzt
|
||||
if (modelEditorLod1GenBtn != null)
|
||||
modelEditorLod1GenBtn.setDisable(input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1);
|
||||
if (modelEditorLod2GenBtn != null)
|
||||
modelEditorLod2GenBtn.setDisable(input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2);
|
||||
if (modelEditorLod2GenBtn != null) {
|
||||
boolean lod1Set = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1;
|
||||
boolean lod2Set = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||
modelEditorLod2GenBtn.setDisable(!lod1Set || lod2Set);
|
||||
}
|
||||
// Importer LOD-Buttons freischalten sobald generiert
|
||||
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
||||
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
||||
@@ -707,6 +711,7 @@ public class EditorApp extends Application {
|
||||
if (importLod2Btn != null) { importLod2Btn.setSelected(lodPreview == 2); }
|
||||
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
||||
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
||||
if (modelImportLod2GenBtn != null) modelImportLod2GenBtn.setDisable(!input.modelImportHasLod1 || input.modelImportHasLod2);
|
||||
|
||||
// Modell-Import/Editor: LOD-Status
|
||||
String lodStatus = input.modelLodGenStatus;
|
||||
@@ -6037,6 +6042,10 @@ public class EditorApp extends Application {
|
||||
receiveCB.setSelected(meta.receiveShadow());
|
||||
for (CheckBox cb : new CheckBox[]{solidCB, castCB, receiveCB})
|
||||
cb.setStyle("-fx-text-fill:#ccc;");
|
||||
if (relPath.startsWith("Models/trees/")) {
|
||||
solidCB.setSelected(false);
|
||||
solidCB.setDisable(true);
|
||||
}
|
||||
|
||||
// ── Zufällige Skalierung beim Multi-Place ─────────────────────────────
|
||||
Label rndTitle = new Label("Zufallsskalierung (Multi-Place):");
|
||||
@@ -6073,16 +6082,23 @@ public class EditorApp extends Application {
|
||||
lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||
lod1ClearBtn.setOnAction(e -> {
|
||||
if (input.modelEditorHasEmbeddedLod1) {
|
||||
// ModelEditorState.removeEmbeddedLod(1) kaskadiert automatisch LOD2
|
||||
input.modelEditorRemoveEmbeddedLod = 1;
|
||||
}
|
||||
if (input.modelImportHasLod1) {
|
||||
// ModelImportState.clearLod==1 kaskadiert automatisch LOD2
|
||||
input.modelImportClearLodLevel = 1;
|
||||
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–");
|
||||
if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–");
|
||||
}
|
||||
modelEditorLod1Path = "";
|
||||
input.modelEditorLod1Path = "";
|
||||
modelEditorLod1Label.setText("(kein Pfad)");
|
||||
if (input.modelEditorLodPreview == 1) {
|
||||
// Kaskade LOD2 (Pfad + Preview)
|
||||
modelEditorLod2Path = "";
|
||||
input.modelEditorLod2Path = "";
|
||||
if (modelEditorLod2Label != null) modelEditorLod2Label.setText("(kein Pfad)");
|
||||
if (input.modelEditorLodPreview == 1 || input.modelEditorLodPreview == 2) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
@@ -6095,7 +6111,7 @@ public class EditorApp extends Application {
|
||||
modelEditorLod2GenBtn = new Button("LOD 2 generieren");
|
||||
modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
boolean lod2Exists = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||
modelEditorLod2GenBtn.setDisable(lod2Exists);
|
||||
modelEditorLod2GenBtn.setDisable(!lod1Exists || lod2Exists);
|
||||
modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85));
|
||||
|
||||
modelEditorLod2StatusLabel = new Label("–");
|
||||
@@ -6504,6 +6520,7 @@ public class EditorApp extends Application {
|
||||
modelImportLod2StatusLabel = null;
|
||||
modelImportLod1ClearBtn = null;
|
||||
modelImportLod2ClearBtn = null;
|
||||
modelImportLod2GenBtn = null;
|
||||
modelImportExportStatusLabel = null;
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportHasLod2 = false;
|
||||
@@ -6731,8 +6748,10 @@ public class EditorApp extends Application {
|
||||
modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
modelImportLod1ClearBtn.setDisable(true);
|
||||
modelImportLod1ClearBtn.setOnAction(e -> {
|
||||
// ModelImportState.clearLod==1 kaskadiert automatisch LOD2
|
||||
input.modelImportClearLodLevel = 1;
|
||||
modelImportLod1StatusLabel.setText("(nicht gesetzt)");
|
||||
modelImportLod2StatusLabel.setText("(nicht gesetzt)");
|
||||
});
|
||||
|
||||
// ── LOD 2 ─────────────────────────────────────────────────────────────
|
||||
@@ -6748,7 +6767,8 @@ public class EditorApp extends Application {
|
||||
lod2FileRB.setToggleGroup(lod2Group);
|
||||
lod2MeshRB.setSelected(true);
|
||||
|
||||
Button lod2GenBtn = new Button("LOD 2 generieren");
|
||||
modelImportLod2GenBtn = new Button("LOD 2 generieren");
|
||||
Button lod2GenBtn = modelImportLod2GenBtn;
|
||||
lod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
lod2GenBtn.setOnAction(e -> {
|
||||
if (lod2MeshRB.isSelected()) {
|
||||
|
||||
@@ -684,8 +684,13 @@ public class ModelEditorState extends BaseAppState {
|
||||
|
||||
if (toRemove.getParent() != null) toRemove.getParent().detachChild(toRemove);
|
||||
embeddedLodSpatials[lodIdx] = null;
|
||||
if (lodIdx == 1) input.modelEditorHasEmbeddedLod1 = false;
|
||||
else if (lodIdx == 2) input.modelEditorHasEmbeddedLod2 = false;
|
||||
if (lodIdx == 1) {
|
||||
input.modelEditorHasEmbeddedLod1 = false;
|
||||
// LOD2 setzt LOD1 voraus — Kaskade: LOD2 ebenfalls entfernen
|
||||
removeEmbeddedLod(2);
|
||||
} else if (lodIdx == 2) {
|
||||
input.modelEditorHasEmbeddedLod2 = false;
|
||||
}
|
||||
|
||||
if (input.modelEditorLodPreview == lodIdx) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
@@ -961,7 +966,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
int i = 0;
|
||||
while (i < s.getNumControls()) {
|
||||
com.jme3.scene.control.Control c = s.getControl(i);
|
||||
if (c instanceof ImpostorUtil.ImpostorViewControl) { i++; }
|
||||
if (c instanceof ImpostorUtil.ImpostorViewControl
|
||||
|| c instanceof com.jme3.scene.control.BillboardControl) { i++; }
|
||||
else { s.removeControl(c); }
|
||||
}
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
||||
@@ -1147,9 +1153,12 @@ public class ModelEditorState extends BaseAppState {
|
||||
log.info("[ModelEditor] LOD2 eingebettet als '{}'", l2.getName());
|
||||
}
|
||||
|
||||
// HD-Modell (Index 0) immer als sichtbar speichern – unabhängig vom aktuellen Preview-Zustand
|
||||
// HD-Modell (Index 0): sichtbar speichern und als "lod0" kennzeichnen
|
||||
// damit WorldObjectsState Fall A (eingebettetes LOD) korrekt erkennt.
|
||||
if (!rootNode.getChildren().isEmpty()) {
|
||||
rootNode.getChild(0).setCullHint(Spatial.CullHint.Inherit);
|
||||
Spatial hd = rootNode.getChild(0);
|
||||
hd.setName("lod0");
|
||||
hd.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
log.info("[ModelEditor] rootNode '{}' hat vor dem Speichern {} Kinder:",
|
||||
|
||||
@@ -106,10 +106,14 @@ public class ModelImportState extends BaseAppState {
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportLod1PreviewNode.set(null);
|
||||
input.modelLodGenStatus = "LOD1: (entfernt)";
|
||||
if (input.modelEditorLodPreview == 1) {
|
||||
if (input.modelEditorLodPreview == 1 || input.modelEditorLodPreview == 2) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
// LOD2 setzt LOD1 voraus — Kaskade
|
||||
lod2Node = null;
|
||||
input.modelImportHasLod2 = false;
|
||||
input.modelImportLod2PreviewNode.set(null);
|
||||
} else if (clearLod == 2) {
|
||||
lod2Node = null;
|
||||
input.modelImportHasLod2 = false;
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import com.jme3.scene.control.BillboardControl;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
@@ -87,17 +88,16 @@ public final class ImpostorUtil {
|
||||
applyTreeLighting(capNode, toLight);
|
||||
|
||||
Node scene = new Node("impCapScene");
|
||||
// Hauptlicht (warm, von Kamera-Seite)
|
||||
// Lichtstärken auf typisches In-Game-Tageslicht abgestimmt.
|
||||
// Das Game-FPP wendet Gamma-Korrektur an (lineare Werte erscheinen heller);
|
||||
// deshalb sind die Werte etwas niedriger als die rein visuell korrekten Editor-Werte.
|
||||
scene.addLight(new DirectionalLight(fromLight,
|
||||
new ColorRGBA(1.2f, 1.1f, 0.9f, 1f)));
|
||||
// Fülllicht (kühler, von gegenüberliegender Seite)
|
||||
new ColorRGBA(0.70f, 0.65f, 0.52f, 1f)));
|
||||
scene.addLight(new DirectionalLight(toLight.mult(-0.7f).addLocal(0f, 0.3f, 0f).normalizeLocal(),
|
||||
new ColorRGBA(0.35f, 0.35f, 0.45f, 1f)));
|
||||
// Oberlicht (Himmelslicht von oben)
|
||||
new ColorRGBA(0.16f, 0.16f, 0.22f, 1f)));
|
||||
scene.addLight(new DirectionalLight(new Vector3f(0f, -1f, 0f),
|
||||
new ColorRGBA(0.25f, 0.30f, 0.35f, 1f)));
|
||||
// Starkes Ambient: verhindert vollständig schwarze Stellen bei Lighting.j3md
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.57f, 1f)));
|
||||
new ColorRGBA(0.12f, 0.14f, 0.17f, 1f)));
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.30f, 0.30f, 0.32f, 1f)));
|
||||
scene.attachChild(capNode);
|
||||
|
||||
ViewPort vp = app.getRenderManager().createPostView("impCap_" + System.nanoTime(), capCam);
|
||||
@@ -181,17 +181,27 @@ public final class ImpostorUtil {
|
||||
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);
|
||||
// Alpha Discard (Cutout) statt Alpha Blend:
|
||||
// Transparente Pixel werden verworfen → Depth-Write bleibt aktiv → FogFilter,
|
||||
// WaterFilter und andere Post-Effects sehen die korrekte Tiefe des Impostors.
|
||||
mat.setFloat("AlphaDiscardThreshold", 0.5f);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Back);
|
||||
|
||||
Node n = new Node("lod2");
|
||||
for (int d = 0; d < DIRS; d++) {
|
||||
float angle = d * FastMath.TWO_PI / DIRS;
|
||||
float uMin = (float) d / DIRS;
|
||||
float uMax = (float)(d + 1) / DIRS;
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, angle, yOff, size, mat.clone(), uMin, uMax));
|
||||
float uMin = (float) d / DIRS;
|
||||
float uMax = (float)(d + 1) / DIRS;
|
||||
// Alle Quads in derselben Ebene (yRot=0). Der BillboardControl unten
|
||||
// dreht den gesamten Node zur Kamera; ImpostorViewControl wählt den
|
||||
// passenden UV-Streifen per Kamerawinkel.
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, 0f, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
// Opaque-Bucket: Depth-Write aktiv → korrekte Tiefen für alle Post-Effects.
|
||||
n.setQueueBucket(RenderQueue.Bucket.Opaque);
|
||||
BillboardControl bc = new BillboardControl();
|
||||
bc.setAlignment(BillboardControl.Alignment.AxialY);
|
||||
n.addControl(bc);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -358,10 +368,9 @@ public final class ImpostorUtil {
|
||||
float angle = (float) Math.atan2(dx, dz); // −π … +π
|
||||
float t = angle * dirs / FastMath.TWO_PI;
|
||||
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||
int opp = (best + dirs / 2) % dirs; // gegenüberliegendes Quad
|
||||
|
||||
for (int i = 0; i < dirs; i++) {
|
||||
quads.get(i).setCullHint(i == best || i == opp
|
||||
quads.get(i).setCullHint(i == best
|
||||
? Spatial.CullHint.Inherit
|
||||
: Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.BillboardControl;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
@@ -17,7 +18,13 @@ public final class ModelExportUtil {
|
||||
private ModelExportUtil() {}
|
||||
|
||||
public static void stripControls(Spatial s) {
|
||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||
int i = 0;
|
||||
while (i < s.getNumControls()) {
|
||||
// BillboardControl bleibt erhalten: JME3-Built-in, überlebt j3o-Roundtrip
|
||||
// und wird im Game korrekt deserialisiert (kein Camera-Feld, keine externe Klasse).
|
||||
if (s.getControl(i) instanceof BillboardControl) { i++; }
|
||||
else { s.removeControl(s.getControl(i)); }
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
n.getChildren().forEach(ModelExportUtil::stripControls);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user