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

LibGDX中如何从射线投射命中获取3D模型纹理坐标?

3D模型绘画界面:射线投射获取纹理坐标问题

初始问题

我用LibGDX和Bullet开发Android端的3D模型绘画界面,需要从射线投射命中的3D模型点获取对应纹理坐标,但相关文档缺失,也没找到有效解决方案。

目前已实现:显示带PNG纹理的g3db格式3D模型,能执行射线投射检测,但不知道如何将点击位置对应到纹理坐标。

初始射线投射代码:

if (Gdx.input.isTouched) {
    callback.closestHitFraction = 1f
    callback.collisionObject = null

    val ray = cam.getPickRay(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
    rayFromWorld.set(ray.origin)
    rayToWorld.set(ray.direction).scl(5f).add(ray.origin)

    bulletPhysicsSystem?.raycast(rayFromWorld, rayToWorld, callback)

    if (callback.hasHit()) {
        val collisionObject = callback.collisionObject
        if (collisionObject is btRigidBody) {
            println("Hit detected!")
        }
    }
}

更新:改用重心坐标计算UV

放弃Bullet,转而通过交点的重心坐标计算UV坐标,再转换为纹理坐标,设计流程如下:

  • 获取模型的所有三角形
  • 找到射线投射命中的三角形与交点
  • 将交点与三角形转换为重心坐标
  • 将重心坐标转换为UV值
  • 将UV值转换为实际纹理坐标
  • 在获取的纹理坐标上绘制验证效果

但结果完全不可靠:正面绘画时,绘制点出现在背面,且其他面的纹理坐标会随模型旋转出现错误。

当前实现代码:

if (Gdx.input.isTouched) {

    val ray = cam.getPickRay(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())

    val meshPart = instance!!.model.meshParts[0]

    val numVertices = meshPart.mesh.numVertices
    val vertexSize = meshPart.mesh.vertexSize / 4
    val vertices = FloatArray(numVertices * vertexSize)
    meshPart.mesh.getVertices(0, vertices.size, vertices)

    val indices = ShortArray(meshPart.mesh.numIndices)
    meshPart.mesh.getIndices(indices)

    println("Vertices: ${vertices.asList()}")
    println("Vertex size: $vertexSize")
    println("Num vertices: $numVertices")
    println("Indices: ${indices.asList()}")

    val triangles = mutableListOf<ModelTriangle>()
    for (i in indices.indices step 3) {
        val stride = meshPart.mesh.vertexSize / 4
        val vertexIndex1 = indices[i].toInt() * stride
        val vertexIndex2 = indices[i + 1].toInt() * stride
        val vertexIndex3 = indices[i + 2].toInt() * stride

        val vertex1 = Vector3(
            vertices[vertexIndex1],
            vertices[vertexIndex1 + 1],
            vertices[vertexIndex1 + 2]
        )
        val vertex2 = Vector3(
            vertices[vertexIndex2],
            vertices[vertexIndex2 + 1],
            vertices[vertexIndex2 + 2]
        )
        val vertex3 = Vector3(
            vertices[vertexIndex3],
            vertices[vertexIndex3 + 1],
            vertices[vertexIndex3 + 2]
        )

        val vertexAttributes = meshPart.mesh.vertexAttributes
        val uvOffset =
            vertexAttributes.findByUsage(VertexAttributes.Usage.TextureCoordinates).offset / 4

        val uv1 = Vector2(
            vertices[vertexIndex1 + uvOffset],
            vertices[vertexIndex1 + uvOffset + 1]
        )
        val uv2 = Vector2(
            vertices[vertexIndex2 + uvOffset],
            vertices[vertexIndex2 + uvOffset + 1]
        )
        val uv3 = Vector2(
            vertices[vertexIndex3 + uvOffset],
            vertices[vertexIndex3 + uvOffset + 1]
        )

        val uvs = arrayOf(uv1, uv2, uv3)

        val triangle = ModelTriangle(vertex1, vertex2, vertex3, uvs)
        triangles.add(triangle)
    }

    println("Triangles: $triangles")
    println("Triangles size: ${triangles.size}")

    val intersectionPoint = Vector3()
    val intersectionTriangle = triangles.find {
        Intersector.intersectRayTriangle(
            ray,
            it.v1,
            it.v2,
            it.v3,
            intersectionPoint
        )
    }

    println("Intersection point: $intersectionPoint")
    println("Is intersection found: ${intersectionTriangle != null}")

    intersectionTriangle?.let { triangle ->
        val instanceTexture =
            instance!!.materials.first().get(TextureAttribute.Diffuse) as TextureAttribute
        val uv = getIntersectionUV(intersectionPoint, triangle, triangle.uvs)
        val textureX = (uv.x * instanceTexture.textureDescription.texture.width).toInt()
        val textureY = (uv.y * instanceTexture.textureDescription.texture.height).toInt()

        println("Texture coordinates: x: $textureX, y: $textureY")

        val texturePixmap = textureToPixmap(instanceTexture.textureDescription.texture)
        drawPointOnPixmap(texturePixmap, textureX, textureY, Color.RED)
        val newTexture = Texture(texturePixmap)
        instance!!.materials.first().set(TextureAttribute.createDiffuse(newTexture))
    }

}

ModelTriangle类

data class ModelTriangle(
    val v1: Vector3,
    val v2: Vector3,
    val v3: Vector3,
    val uvs: Array<Vector2>
) {
    // equals and hashCode由AndroidStudio自动生成
}

getIntersectionUV方法

private fun getIntersectionUV(
    intersectionPoint: Vector3,
    triangle: ModelTriangle,
    uvs: Array<Vector2>
): Vector2 {
    val barycentricCoords = toBarycoord3D(
        intersectionPoint, triangle.v1, triangle.v2, triangle.v3
    )

    val isInsideTheTriangle = GeometryUtils.barycoordInsideTriangle(Vector2(barycentricCoords.x, barycentricCoords.y))
    println("Is inside the triangle: $isInsideTheTriangle")

    val uv1 = uvs[0]
    val uv2 = uvs[1]
    val uv3 = uvs[2]

    return Vector2(
        barycentricCoords.z * uv1.x + barycentricCoords.x * uv2.x + barycentricCoords.y * uv3.x,
        barycentricCoords.z * uv1.y + barycentricCoords.x * uv2.y + barycentricCoords.y * uv3.y
    )
}

toBarycoord3D方法

private fun toBarycoord3D(
    point: Vector3,
    a: Vector3,
    b: Vector3,
    c: Vector3,
): Vector3 {
    val v0 = Vector3().set(b).sub(a)
    val v1 = Vector3().set(c).sub(a)
    val v2 = Vector3().set(point).sub(a)

    val d00 = v0.dot(v0)
    val d01 = v0.dot(v1)
    val d11 = v1.dot(v1)
    val d20 = v2.dot(v0)
    val d21 = v2.dot(v1)

    val denom = d00 * d11 - d01 * d01
    val barycentricOut = Vector3()
    barycentricOut.x = (d11 * d20 - d01 * d21) / denom
    barycentricOut.y = (d00 * d21 - d01 * d20) / denom
    barycentricOut.z = 1.0f - barycentricOut.x - barycentricOut.y

    return barycentricOut
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 14:47:34