[orx-mesh, orx-mesh-generator, orx-obj-loader] Add decal and tangent tools
This commit is contained in:
@@ -10,6 +10,7 @@ kotlin {
|
||||
api(libs.openrndr.application)
|
||||
api(libs.openrndr.math)
|
||||
implementation(project(":orx-shapes"))
|
||||
api(project(":orx-mesh"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ kotlin {
|
||||
implementation(project(":orx-mesh-generators"))
|
||||
implementation(project(":orx-camera"))
|
||||
implementation(project(":orx-noise"))
|
||||
implementation(project(":orx-obj-loader"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
201
orx-mesh-generators/src/commonMain/kotlin/decal/Decal.kt
Normal file
201
orx-mesh-generators/src/commonMain/kotlin/decal/Decal.kt
Normal file
@@ -0,0 +1,201 @@
|
||||
package org.openrndr.extra.meshgenerators.decal
|
||||
|
||||
import org.openrndr.extra.mesh.*
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Create a decal mesh
|
||||
* @param projectorMatrix
|
||||
* @param size
|
||||
*/
|
||||
fun IMeshData.decal(
|
||||
projectorMatrix: Matrix44,
|
||||
size: Vector3
|
||||
): IVertexData {
|
||||
require(isTriangular())
|
||||
|
||||
val projectorMatrixInverse = projectorMatrix.inversed
|
||||
|
||||
val positions = vertexData.positions.slice(polygons.flatMap { it.positions }).map {
|
||||
(projectorMatrixInverse * (it.xyz1)).div
|
||||
}
|
||||
val normals = vertexData.normals.slice(polygons.flatMap { it.normals })
|
||||
val textureCoords = vertexData.textureCoords.slice(polygons.flatMap { it.textureCoords })
|
||||
val colors = vertexData.colors.slice(polygons.flatMap { it.colors })
|
||||
val tangents = vertexData.tangents.slice(polygons.flatMap { it.tangents })
|
||||
val bitangents = vertexData.bitangents.slice(polygons.flatMap { it.bitangents })
|
||||
|
||||
var decalVertices: IVertexData = VertexData(positions, textureCoords, colors, normals, tangents, bitangents)
|
||||
|
||||
decalVertices = decalVertices.clipToPlane(size, Vector3(1.0, 0.0, 0.0))
|
||||
decalVertices = decalVertices.clipToPlane(size, Vector3(-1.0, 0.0, 0.0))
|
||||
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, 1.0, 0.0))
|
||||
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, -1.0, 0.0))
|
||||
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, 0.0, 1.0))
|
||||
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, 0.0, -1.0))
|
||||
|
||||
|
||||
val decalMesh = MutableVertexData()
|
||||
for (i in decalVertices.positions.indices) {
|
||||
val v = decalVertices[i]
|
||||
|
||||
val w = v.copy(
|
||||
position = (projectorMatrix * v.position.xyz1).div,
|
||||
textureCoord = v.position.xy / size.xy + Vector2(0.5)
|
||||
)
|
||||
decalMesh.add(w)
|
||||
}
|
||||
return decalMesh
|
||||
}
|
||||
|
||||
fun IVertexData.clipToPlane(
|
||||
size: Vector3,
|
||||
plane: Vector3
|
||||
): IVertexData {
|
||||
val outVertices = MutableVertexData()
|
||||
val s = 0.5 * abs(size.dot(plane))
|
||||
|
||||
fun clip(
|
||||
v0: Point,
|
||||
v1: Point, p: Vector3, s: Double
|
||||
): Point {
|
||||
|
||||
val d0 = v0.position.dot(p) - s;
|
||||
val d1 = v1.position.dot(p) - s;
|
||||
|
||||
val s0 = d0 / (d0 - d1)
|
||||
|
||||
val v = Point(
|
||||
v0.position + (v1.position - v0.position) * s0,
|
||||
if (v0.textureCoord != null) {
|
||||
v0.textureCoord!! + (v1.textureCoord!! - v0.textureCoord!!) * s0
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (v0.color != null) {
|
||||
v0.color!! + (v1.color!! - v0.color!!) * s0
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (v0.normal != null) {
|
||||
v0.normal!! + (v1.normal!! - v0.normal!!) * s0
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (v0.tangent != null) {
|
||||
v0.tangent!! + (v1.tangent!! - v0.tangent!!) * s0
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (v0.bitangent != null) {
|
||||
v0.bitangent!! + (v1.bitangent!! - v0.bitangent!!) * s0
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
return v
|
||||
}
|
||||
|
||||
for (i in positions.indices step 3) {
|
||||
|
||||
val d1 = positions[i + 0].dot(plane) - s
|
||||
val d2 = positions[i + 1].dot(plane) - s
|
||||
val d3 = positions[i + 2].dot(plane) - s
|
||||
|
||||
val v1Out = d1 > 0
|
||||
val v2Out = d2 > 0
|
||||
val v3Out = d3 > 0
|
||||
|
||||
val total = (if (v1Out) 1 else 0) + (if (v2Out) 1 else 0) + (if (v3Out) 1 else 0)
|
||||
|
||||
when (total) {
|
||||
0 -> {
|
||||
outVertices.add(this[i])
|
||||
outVertices.add(this[i + 1])
|
||||
outVertices.add(this[i + 2])
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (v1Out) {
|
||||
val nV1 = this[i + 1]
|
||||
val nV2 = this[i + 2]
|
||||
val nV3 = clip(this[i], nV1, plane, s)
|
||||
val nV4 = clip(this[i], nV2, plane, s)
|
||||
|
||||
outVertices.add(nV1)
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV3)
|
||||
|
||||
outVertices.add(nV4)
|
||||
outVertices.add(nV3)
|
||||
outVertices.add(nV2)
|
||||
}
|
||||
|
||||
if (v2Out) {
|
||||
val nV1 = this[i];
|
||||
val nV2 = this[i + 2];
|
||||
val nV3 = clip(this[i + 1], nV1, plane, s)
|
||||
val nV4 = clip(this[i + 1], nV2, plane, s)
|
||||
|
||||
outVertices.add(nV3)
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV1)
|
||||
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV3)
|
||||
outVertices.add(nV4)
|
||||
}
|
||||
|
||||
if (v3Out) {
|
||||
val nV1 = this[i]
|
||||
val nV2 = this[i + 1]
|
||||
val nV3 = clip(this[i + 2], nV1, plane, s)
|
||||
val nV4 = clip(this[i + 2], nV2, plane, s)
|
||||
|
||||
outVertices.add(nV1)
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV3)
|
||||
|
||||
outVertices.add(nV4)
|
||||
outVertices.add(nV3)
|
||||
outVertices.add(nV2)
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
if (!v1Out) {
|
||||
val nV1 = this[i]
|
||||
val nV2 = clip(nV1, this[i + 1], plane, s)
|
||||
val nV3 = clip(nV1, this[i + 2], plane, s)
|
||||
outVertices.add(nV1)
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV3)
|
||||
}
|
||||
|
||||
if (!v2Out) {
|
||||
val nV1 = this[i + 1]
|
||||
val nV2 = clip(nV1, this[i + 2], plane, s)
|
||||
val nV3 = clip(nV1, this[i], plane, s)
|
||||
outVertices.add(nV1)
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV3)
|
||||
}
|
||||
|
||||
if (!v3Out) {
|
||||
val nV1 = this[i + 2]
|
||||
val nV2 = clip(nV1, this[i], plane, s)
|
||||
val nV3 = clip(nV1, this[i + 1], plane, s)
|
||||
outVertices.add(nV1)
|
||||
outVertices.add(nV2)
|
||||
outVertices.add(nV3)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
return outVertices
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.openrndr.extra.meshgenerators.normals
|
||||
|
||||
import org.openrndr.extra.mesh.IMeshData
|
||||
import org.openrndr.extra.mesh.IndexedPolygon
|
||||
import org.openrndr.extra.mesh.MeshData
|
||||
import org.openrndr.extra.mesh.VertexData
|
||||
import org.openrndr.math.Vector3
|
||||
|
||||
/**
|
||||
* Estimate per-vertex normals
|
||||
*/
|
||||
fun IMeshData.estimateNormals(): MeshData {
|
||||
val normals = MutableList(vertexData.positions.size) { Vector3.ZERO }
|
||||
|
||||
for (polygon in polygons) {
|
||||
for (p in polygon.positions) {
|
||||
normals[p] += polygon.normal(vertexData)
|
||||
}
|
||||
}
|
||||
|
||||
for (i in normals.indices) {
|
||||
normals[i] = normals[i].normalized
|
||||
}
|
||||
|
||||
return MeshData(
|
||||
VertexData(
|
||||
vertexData.positions,
|
||||
vertexData.textureCoords,
|
||||
vertexData.colors,
|
||||
normals,
|
||||
vertexData.tangents,
|
||||
vertexData.bitangents
|
||||
),
|
||||
polygons.map {
|
||||
IndexedPolygon(it.positions, it.textureCoords, it.colors, it.positions, it.tangents, it.bitangents)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign vertex normals based on face normals
|
||||
*/
|
||||
fun IMeshData.assignFaceNormals(): MeshData {
|
||||
val normals = MutableList(polygons.size) { Vector3.ZERO }
|
||||
|
||||
for (i in polygons.indices) {
|
||||
normals[i] = polygons[i].normal(vertexData)
|
||||
}
|
||||
|
||||
return MeshData(
|
||||
VertexData(
|
||||
vertexData.positions,
|
||||
vertexData.textureCoords,
|
||||
vertexData.colors,
|
||||
normals,
|
||||
vertexData.tangents,
|
||||
vertexData.bitangents
|
||||
),
|
||||
polygons.mapIndexed { index, it ->
|
||||
IndexedPolygon(it.positions, it.textureCoords, it.colors, it.positions.map {
|
||||
index
|
||||
}, it.tangents, it.bitangents)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.openrndr.extra.meshgenerators.tangents
|
||||
|
||||
import org.openrndr.extra.mesh.*
|
||||
import org.openrndr.math.Vector3
|
||||
|
||||
/**
|
||||
* Estimate tangents from normals and texture coordinates
|
||||
* https://terathon.com/blog/tangent-space.html
|
||||
*/
|
||||
fun IMeshData.estimateTangents(): MeshData {
|
||||
require(vertexData.textureCoords.isNotEmpty()) {
|
||||
"need texture coordinates to estimate tangents"
|
||||
}
|
||||
require(isTriangular()) {
|
||||
|
||||
}
|
||||
|
||||
val normals = MutableList(vertexData.positions.size) { Vector3.ZERO }
|
||||
|
||||
val tan1 = MutableList(vertexData.positions.size) { Vector3.ZERO }
|
||||
val tan2 = MutableList(vertexData.positions.size) { Vector3.ZERO }
|
||||
for (polygon in polygons) {
|
||||
val v1 = vertexData.positions[polygon.positions[0]]
|
||||
val v2 = vertexData.positions[polygon.positions[1]]
|
||||
val v3 = vertexData.positions[polygon.positions[2]]
|
||||
|
||||
val w1 = vertexData.textureCoords[polygon.textureCoords[0]]
|
||||
val w2 = vertexData.textureCoords[polygon.textureCoords[1]]
|
||||
val w3 = vertexData.textureCoords[polygon.textureCoords[2]]
|
||||
|
||||
val x1 = (v2.x - v1.x)
|
||||
val x2 = (v3.x - v1.x)
|
||||
val y1 = (v2.y - v1.y)
|
||||
val y2 = (v3.y - v1.y)
|
||||
val z1 = (v2.z - v1.z)
|
||||
val z2 = (v3.z - v1.z)
|
||||
|
||||
val s1 = (w2.x - w1.x)
|
||||
val s2 = (w3.x - w1.x)
|
||||
val t1 = (w2.y - w1.y)
|
||||
val t2 = (w3.y - w1.y)
|
||||
|
||||
var det = s1 * t2 - s2 * t1
|
||||
if (det == 0.0) det = 1.0
|
||||
val r = 1.0/ (det)
|
||||
val sdir = Vector3(
|
||||
(t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r,
|
||||
(t2 * z1 - t1 * z2) * r
|
||||
).normalized
|
||||
val tdir = Vector3(
|
||||
(s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r,
|
||||
(s1 * z2 - s2 * z1) * r
|
||||
).normalized
|
||||
|
||||
tan1[polygon.positions[0]] += sdir
|
||||
tan1[polygon.positions[1]] += sdir
|
||||
tan1[polygon.positions[2]] += sdir
|
||||
|
||||
tan2[polygon.positions[0]] += tdir
|
||||
tan2[polygon.positions[1]] += tdir
|
||||
tan2[polygon.positions[2]] += tdir
|
||||
|
||||
normals[polygon.positions[0]] += vertexData.normals[polygon.normals[0]]
|
||||
normals[polygon.positions[1]] += vertexData.normals[polygon.normals[1]]
|
||||
normals[polygon.positions[2]] += vertexData.normals[polygon.normals[2]]
|
||||
|
||||
}
|
||||
|
||||
for (a in 0 until vertexData.positions.size) {
|
||||
normals[a] = normals[a].normalized
|
||||
tan1[a] = tan1[a].normalized
|
||||
tan2[a] = tan2[a].normalized
|
||||
|
||||
val t = tan1[a]
|
||||
val n = normals[a]
|
||||
|
||||
tan1[a] = (t - n * n.dot(t)).normalized
|
||||
val w = if ((n.cross(t)).dot(tan2[a]) < 0.0f) -1.0 else 1.0
|
||||
tan2[a] = n.cross((t)).normalized * w
|
||||
}
|
||||
|
||||
return MeshData(VertexData(vertexData.positions, vertexData.textureCoords, vertexData.colors, normals, tan1, tan2),
|
||||
polygons = polygons.map {
|
||||
IndexedPolygon(
|
||||
it.positions,
|
||||
it.textureCoords,
|
||||
it.colors,
|
||||
normals = it.positions,
|
||||
tangents = it.positions,
|
||||
bitangents = it.positions
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
55
orx-mesh-generators/src/jvmDemo/kotlin/decal/DemoDecal01.kt
Normal file
55
orx-mesh-generators/src/jvmDemo/kotlin/decal/DemoDecal01.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package decal
|
||||
|
||||
import org.openrndr.application
|
||||
import org.openrndr.draw.DrawPrimitive
|
||||
import org.openrndr.draw.shadeStyle
|
||||
import org.openrndr.extra.camera.Orbital
|
||||
import org.openrndr.extra.mesh.*
|
||||
import org.openrndr.extra.objloader.loadOBJMeshData
|
||||
import org.openrndr.extra.meshgenerators.decal.decal
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.transforms.buildTransform
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Demonstrate decal generator as an object slicer
|
||||
*/
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 720
|
||||
height = 720
|
||||
}
|
||||
program {
|
||||
val obj = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData().triangulate()
|
||||
|
||||
val slices = 25
|
||||
val sliceStep = 0.1
|
||||
val sliceWidth = 0.14
|
||||
|
||||
val sliceVBs = (0 until slices).map {
|
||||
val projector = buildTransform {
|
||||
translate(0.0, 0.0, -1.0 + it * sliceStep)
|
||||
}
|
||||
val decal = obj.decal(projector, Vector3(4.0, 4.0, sliceWidth))
|
||||
val vb = decal.toVertexBuffer()
|
||||
vb
|
||||
}
|
||||
|
||||
extend(Orbital()) {
|
||||
eye = Vector3(0.0, 0.0, 2.0)
|
||||
}
|
||||
extend {
|
||||
drawer.shadeStyle = shadeStyle {
|
||||
fragmentTransform = """x_fill.rgb = v_viewNormal.rgb * 0.5 + 0.5; """
|
||||
}
|
||||
|
||||
drawer.translate(0.0, 0.0, slices * 0.5 * 0.5)
|
||||
for (i in 0 until sliceVBs.size) {
|
||||
drawer.vertexBuffer(sliceVBs[i], DrawPrimitive.TRIANGLES)
|
||||
drawer.translate(0.0, 0.0, -0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
orx-mesh-generators/src/jvmDemo/kotlin/decal/DemoDecal02.kt
Normal file
86
orx-mesh-generators/src/jvmDemo/kotlin/decal/DemoDecal02.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
package decal
|
||||
|
||||
import org.openrndr.application
|
||||
import org.openrndr.draw.DrawPrimitive
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.draw.shadeStyle
|
||||
import org.openrndr.extra.camera.Orbital
|
||||
import org.openrndr.extra.objloader.loadOBJMeshData
|
||||
import org.openrndr.extra.mesh.toVertexBuffer
|
||||
import org.openrndr.extra.meshgenerators.decal.decal
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.transforms.buildTransform
|
||||
import java.io.File
|
||||
import kotlin.math.PI
|
||||
|
||||
/**
|
||||
* Demonstrate decal generation and rendering
|
||||
*/
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 720
|
||||
height = 720
|
||||
}
|
||||
program {
|
||||
/** base object */
|
||||
val obj = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj"))
|
||||
.toMeshData() // convert from CompoundMeshData to MeshData
|
||||
.triangulate() // convert to triangles, we need this for the decal generation steps
|
||||
|
||||
/** object [VertexBuffer] */
|
||||
val objVB = obj.toVertexBuffer()
|
||||
|
||||
|
||||
/** positions for the decal projectors */
|
||||
val decalPositions = listOf(
|
||||
Vector3(0.35, 0.245, 0.8),
|
||||
Vector3(-0.35, 0.245, 0.8)
|
||||
)
|
||||
|
||||
/** decal vertex buffers */
|
||||
val decalVBs = decalPositions.map {
|
||||
val projector = buildTransform {
|
||||
translate(it)
|
||||
}
|
||||
val decal = obj.decal(projector, Vector3(2.0, 2.0, 0.5))
|
||||
val vb = decal.toVertexBuffer()
|
||||
vb
|
||||
}
|
||||
|
||||
extend(Orbital()) {
|
||||
eye = Vector3(0.0, 0.0, 2.0)
|
||||
}
|
||||
extend {
|
||||
/* draw the base mesh */
|
||||
drawer.isolated {
|
||||
drawer.shadeStyle = shadeStyle {
|
||||
fragmentTransform = """x_fill.rgb = vec3(v_viewNormal * 0.5 + 0.5); """
|
||||
}
|
||||
drawer.vertexBuffer(objVB, DrawPrimitive.TRIANGLES)
|
||||
}
|
||||
|
||||
/* draw the decals */
|
||||
drawer.isolated {
|
||||
for ((index, decal) in decalVBs.withIndex()) {
|
||||
/* offset the projection transform to avoid z-fighting */
|
||||
drawer.projection = buildTransform {
|
||||
translate(0.0, 0.0, -1e-4)
|
||||
} * drawer.projection
|
||||
|
||||
/* draw effects on the decal geometry */
|
||||
drawer.shadeStyle = shadeStyle {
|
||||
fragmentTransform = """
|
||||
float d = length(va_texCoord0.xy - vec2(0.5));
|
||||
float sd = smoothstep(-0.01, 0.01, cos(p_time + d * 3.1415 * 2.0 * 10.0));
|
||||
float l = max(0.0, va_normal.z);
|
||||
x_fill = vec4(0.0, 0.0, 0.0, l * sd * 0.5); """
|
||||
parameter("time", seconds * PI * 2 + index * PI)
|
||||
}
|
||||
drawer.vertexBuffer(decal, DrawPrimitive.TRIANGLES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user