You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX 3D渲染异常:视角切换时出现奇怪裁剪/Z缓冲问题

问题描述

我正在开发一个基于Perlin噪声生成网格的小型项目,网格生成功能正常,但渲染环节存在异常。从+Z方向视角观察时画面显示正常,但切换到-Z方向视角时,画面出现奇怪的显示问题。

Main类代码

package ch.imgajeed.world_generator

import javafx.application.Application
import javafx.beans.property.SimpleDoubleProperty
import javafx.scene.DepthTest
import javafx.scene.Group
import javafx.scene.PerspectiveCamera
import javafx.scene.Scene
import javafx.scene.input.ScrollEvent
import javafx.scene.paint.Color
import javafx.scene.shape.DrawMode
import javafx.scene.transform.Rotate
import javafx.stage.Stage

const val SCREEN_WIDTH = 1000.0
const val SCREEN_HEIGHT = 800.0

var screenInFocus = true

class Main : Application() {

    var anchorX = 0.0
    var anchorY = 0.0
    var anchorAngleX = 0.0
    var anchorAngleY = 0.0
    var angleX = SimpleDoubleProperty(20.0)
    var angleY = SimpleDoubleProperty(90.0)

    override fun start(stage: Stage) {
        stage.title = "World Generator"
        setupRenderer(stage)

        stage.show()
    }

    private fun setupRenderer(stage: Stage) {
        // val heightList = pngToHeightList("src/main/resources/ch/imgajeed/world_generator/Test-Height-Map.png", 0.5f)
        val generator = MapGenerator(resolution = 1000)
        generator.modifiers.add(PerlinNoise(0.0,300f, 0.5f))
        generator.modifiers.add(PerlinNoise(1.0,200f, 5f))
        generator.modifiers.add(PerlinNoise(2.0,100f, 2f))
        generator.modifiers.add(PerlinNoise(3.0,50f, 5f))
        generator.modifiers.add(PerlinNoise(4.0,10f, 15f))
        generator.modifiers.add(PerlinNoise(5.0,1f, 150f))
        generator.modifiers.add(PerlinNoise(6.0,0.1f, 250f))
        val heightList = generator.generate()

        val terrain3d = Terrain3d(300f, 300f, 1)

        val obj = terrain3d.generateMesh(heightList,true, false, 4)
        obj.drawMode = DrawMode.FILL

        val group = Group()
        group.children.add(obj)
        group.depthTest = DepthTest.ENABLE

        val camera = PerspectiveCamera()

        val scene = Scene(group, SCREEN_WIDTH, SCREEN_HEIGHT)
        scene.fill = Color.BLACK
        scene.camera = camera

        camera.translateX = -(SCREEN_WIDTH / 2)
        camera.translateY = -(SCREEN_HEIGHT / 2)
        camera.translateZ = 0.0

        camera.nearClip = 0.00000001
        camera.farClip = 1000.0

        initMouseController(group, scene, stage)

        stage.scene = scene
    }

    private fun initMouseController(group: Group, scene: Scene, stage: Stage) {
        val rotatorX = Rotate(0.0, Rotate.X_AXIS)
        val rotatorY = Rotate(0.0, Rotate.Y_AXIS)
        rotatorX.angleProperty().bind(angleX)
        rotatorY.angleProperty().bind(angleY)

        group.transforms.addAll(rotatorX, rotatorY)

        scene.setOnMousePressed { event ->
            anchorX = event.sceneX
            anchorY = event.sceneY
            anchorAngleX = angleX.get()
            anchorAngleY = angleY.get()
        }

        scene.setOnMouseDragged { event ->
            angleX.set(anchorAngleX - (anchorY - event.sceneY))
            angleY.set(anchorAngleY + anchorX - event.sceneX)

            if (angleX.get() < 1) {
                angleX.set(1.0)
            }
        }

        stage.addEventHandler(ScrollEvent.SCROLL) { event ->
            val movement = event.deltaY
            group.translateZ -= movement
        }
    }
}

fun main() {
    Application.launch(Main::class.java)
}

Terrain类代码

package ch.imgajeed.world_generator

import javafx.scene.shape.MeshView
import javafx.scene.shape.TriangleMesh
import java.awt.Color
import java.io.File
import javax.imageio.ImageIO

class Terrain3d(var width: Float = 5f, var height: Float = 5f, var resolution: Int = 5) {
    fun generateMesh(heightList: ArrayList<ArrayList<Float>>, hlr: Boolean = false, doubleSided: Boolean = false, divider: Int = 1): MeshView {
        if (heightList.size != heightList[0].size) {
            error("HeightList size must be a square.")
        }

        if (hlr) {
            resolution = heightList.size / divider
        }

        if (heightList.size / divider != resolution || heightList[0].size / divider != resolution) {
            error("Size of HeightList and resolution does not match. If you want to use the HeightLists resolution set hlr to true.")
        }

        val vertices = arrayListOf<Float>()
        val texCoords = arrayListOf<Float>()
        val faces = arrayListOf<Int>()

        val tileWidth = (width / resolution)
        val tileHeight = (height / resolution)

        for (x in 0 until resolution) {
            for (y in 0 until resolution) {
                vertices.addAll(
                    arrayListOf(
                        x * tileWidth - width / 2f, -heightList[y * divider][x * divider], y * tileHeight - height / 2f
                    )
                )
                texCoords.addAll(
                    arrayListOf(
                        (x.toFloat() / resolution.toFloat()), (y.toFloat() / resolution.toFloat())
                    )
                )
            }
        }

        var faceCountA = 0
        var faceCountB = 0

        for (i in 0 until vertices.size / 3) {
            val ps = vertices.size / 3

            val a1: Int = i
            val a2: Int = i + resolution
            val a3: Int = i + 1

            val a1OK = ((a1 + 1) % resolution != 0) && a1 < ps - resolution
            val a2OK = a2 < ps
            val a3OK = a3 < ps

            if (a1OK && a2OK && a3OK) {
                faces.add(a1)
                faces.add(a1)
                faces.add(a2)
                faces.add(a2)
                faces.add(a3)
                faces.add(a3)

                if (doubleSided) {
                    // other side
                    faces.add(a3)
                    faces.add(a3)
                    faces.add(a2)
                    faces.add(a2)
                    faces.add(a1)
                    faces.add(a1)
                }

                faceCountA += 1
            }


            val b1: Int = i
            val b2: Int = i - resolution
            val b3: Int = i - 1

            val b1OK = b1 > resolution && b1 % resolution != 0
            val b2OK = b2 > 0 && b2 < ps - resolution
            val b3OK = b3 > 0 && b3 < ps

            if (b1OK && b2OK && b3OK) {
                faces.add(b1)
                faces.add(b1)
                faces.add(b2)
                faces.add(b2)
                faces.add(b3)
                faces.add(b3)

                if (doubleSided) {
                    // other side
                    faces.add(b3)
                    faces.add(b3)
                    faces.add(b2)
                    faces.add(b2)
                    faces.add(b1)
                    faces.add(b1)
                }

                faceCountB += 1
            }
        }

        println("Resolution: $resolution")
        println("Vertices: ${vertices.size}")
        println("TexCoords: ${texCoords.size}")
        println("Faces: ${faces.size}")

        val mesh = TriangleMesh()
        for (vertex in vertices) {
            mesh.points.addAll(vertex)
        }
        for (coord in texCoords) {
            mesh.texCoords.addAll(coord)
        }
        for (face in faces) {
            mesh.faces.addAll(face)
        }

        mesh.normals.addAll(0f)
        mesh.normals.addAll(-1f)
        mesh.normals.addAll(0f)

        val meshView = MeshView()
        meshView.mesh = mesh

        println("Terrain mesh created")


        return meshView
    }
}

fun pngToHeightList(png: String, scaler: Float = 1f): ArrayList<ArrayList<Float>> {
    val img = ImageIO.read(File(png))

    val heightList = arrayListOf<ArrayList<Float>>()

    for (x in 0 until img.width) {
        val line = arrayListOf<Float>()
        for (y in 0 until img.height) {
            val color = Color(img.getRGB(x, y))
            val graylevel: Float = color.red.toFloat() * scaler
            line.add(graylevel)
        }
        heightList.add(line)
    }

   return heightList
}

问题原因分析

  1. 单面渲染限制:调用generateMesh时doubleSided参数设为false,网格仅渲染正面;同时手动给所有面设置了固定法线(0f, -1f, 0f),该法线仅朝向单一方向,当从-Z视角观察时,大量面的法线背向相机,被渲染引擎剔除。
  2. 顶点顺序判定:JavaFX的TriangleMesh默认以逆时针顶点顺序为正面,切换到-Z视角后,部分面的顶点顺序变为顺时针,被判定为背面而不渲染。

修复方案

方案一:启用双面渲染(快速解决)

修改Main类中generateMesh的调用参数,将doubleSided设为true,自动生成每个面的背面:

val obj = terrain3d.generateMesh(heightList, true, true, 4)

优点:实现简单;缺点:面数翻倍,对性能有轻微影响。

方案二:生成动态法线(最优解)

移除固定法线,为每个面计算正确的法线方向,确保法线始终朝向观察者的反方向:

  1. 删除Terrain类中固定法线的代码:
// 移除以下代码
mesh.normals.addAll(0f)
mesh.normals.addAll(-1f)
mesh.normals.addAll(0f)
  1. 在生成faces后添加法线计算逻辑:
// 计算每个面的法线
val normals = arrayListOf<Float>()
for (i in 0 until faces.size step 6) {
    // 获取三个顶点的索引
    val v1Idx = faces[i] * 3
    val v2Idx = faces[i+2] * 3
    val v3Idx = faces[i+4] * 3

    // 获取顶点坐标
    val v1x = vertices[v1Idx]
    val v1y = vertices[v1Idx+1]
    val v1z = vertices[v1Idx+2]

    val v2x = vertices[v2Idx]
    val v2y = vertices[v2Idx+1]
    val v2z = vertices[v2Idx+2]

    val v3x = vertices[v3Idx]
    val v3y = vertices[v3Idx+1]
    val v3z = vertices[v3Idx+2]

    // 计算向量
    val vec1x = v2x - v1x
    val vec1y = v2y - v1y
    val vec1z = v2z - v1z

    val vec2x = v3x - v1x
    val vec2y = v3y - v1y
    val vec2z = v3z - v1z

    // 叉乘计算法线
    val nx = vec1y * vec2z - vec1z * vec2y
    val ny = vec1z * vec2x - vec1x * vec2z
    val nz = vec1x * vec2y - vec1y * vec2x

    // 归一化法线
    val length = Math.sqrt((nx*nx + ny*ny + nz*nz).toDouble()).toFloat()
    val normalizedNx = nx / length
    val normalizedNy = ny / length
    val normalizedNz = nz / length

    // 为三个顶点添加相同法线
    normals.addAll(listOf(normalizedNx, normalizedNy, normalizedNz))
    normals.addAll(listOf(normalizedNx, normalizedNy, normalizedNz))
    normals.addAll(listOf(normalizedNx, normalizedNy, normalizedNz))
}

// 将法线添加到mesh
mesh.normals.addAll(normals)
  1. 验证顶点顺序:确保生成面的顶点顺序为逆时针(从正面观察),若顺序错误则调整顶点排列顺序。

方案三:禁用背面剔除

通过设置MeshView的属性强制渲染所有面:

import javafx.scene.shape.CullFace

// 在生成obj后添加
obj.cullFace = CullFace.NONE

优点:无需修改网格数据;缺点:会渲染背面,性能略逊于法线方案。

内容的提问来源于stack exchange,提问作者ImGajeed76

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 20:39:20