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 }
问题原因分析
- 单面渲染限制:调用
generateMesh时doubleSided参数设为false,网格仅渲染正面;同时手动给所有面设置了固定法线(0f, -1f, 0f),该法线仅朝向单一方向,当从-Z视角观察时,大量面的法线背向相机,被渲染引擎剔除。 - 顶点顺序判定:JavaFX的
TriangleMesh默认以逆时针顶点顺序为正面,切换到-Z视角后,部分面的顶点顺序变为顺时针,被判定为背面而不渲染。
修复方案
方案一:启用双面渲染(快速解决)
修改Main类中generateMesh的调用参数,将doubleSided设为true,自动生成每个面的背面:
val obj = terrain3d.generateMesh(heightList, true, true, 4)
优点:实现简单;缺点:面数翻倍,对性能有轻微影响。
方案二:生成动态法线(最优解)
移除固定法线,为每个面计算正确的法线方向,确保法线始终朝向观察者的反方向:
- 删除Terrain类中固定法线的代码:
// 移除以下代码 mesh.normals.addAll(0f) mesh.normals.addAll(-1f) mesh.normals.addAll(0f)
- 在生成
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)
- 验证顶点顺序:确保生成面的顶点顺序为逆时针(从正面观察),若顺序错误则调整顶点排列顺序。
方案三:禁用背面剔除
通过设置MeshView的属性强制渲染所有面:
import javafx.scene.shape.CullFace // 在生成obj后添加 obj.cullFace = CullFace.NONE
优点:无需修改网格数据;缺点:会渲染背面,性能略逊于法线方案。
内容的提问来源于stack exchange,提问作者ImGajeed76
相关产品推荐
相关产品推荐

