Perspective warping of texture applied to quad

With CardMaker it is possible to create a non-rectangular quad, by passing in individual vertices for the four corners. However, since it creates the quad as two triangles, and the UVs are interpolated across those triangles using barycentric coordinates, this can result in a very unsatisfying texture mapping (see image on the left). There are several other ways to interpolate textures across a quad, one of them is via perspective warping (as shown on the right).

This is detailed further in this blog post. The solution is to use an additional texture coordinate, by which the other components are divided after interpolation. In the fixed-function pipeline, you can do this by specifying the texcoord value as a 4-D vector, with the w coordinate containing this additional divisor. In a custom shader, you need to use textureProj and pass in this extra coordinate as third coordinate.

CardMaker does not currently support 4-D texture coordinates (but I am considering adding this). Here is the code showing how to generate the quad manually instead:

from direct.showbase.ShowBase import ShowBase
from panda3d.core import *

base = ShowBase()

# Generate a checkerboard texture
img = PNMImage(8, 8, 1)
for x in range(8):
    for y in range(8):
        img.set_gray(x, y, (x + y) % 2)

tex = Texture()
tex.load(img)
tex.set_minfilter(Texture.FT_nearest)
tex.set_magfilter(Texture.FT_nearest)

# Create a normal trapezoid
cm = CardMaker("")
cm.set_frame((-1, 0, 0), (1, 0, 0), (0.5, 0, 1), (-0.25, 0, 1))
cm.set_uv_range((0, 0), (1, 1))
card = aspect2d.attach_new_node(cm.generate())
card.set_scale(0.5)
card.set_pos(-0.5, 0, 0)
card.set_texture(tex)

# Create one with 4D UVs - CardMaker doesn't support this at the moment
format = GeomVertexFormat()
format.add_array(GeomVertexArrayFormat('vertex', 3, Geom.NT_float32, Geom.C_point,
                                       'normal', 3, Geom.NT_float32, Geom.C_normal,
	                                   'texcoord', 4, Geom.NT_float32, Geom.C_texcoord))
format = GeomVertexFormat.register_format(format)
vdata = GeomVertexData('trapezoid', format, Geom.UH_static)
vdata.unclean_set_num_rows(4)

vwriter = GeomVertexWriter(vdata, 'vertex')
nwriter = GeomVertexWriter(vdata, 'normal')
twriter = GeomVertexWriter(vdata, 'texcoord')

vtx = (
    Point3(-1, 0, 0),
    Point3(1, 0, 0),
    Point3(0.5, 0, 1),
    Point3(-0.25, 0, 1),
)
normal = Vec3(0, 1, 0)
uvs = (
    Point2(0, 0),
    Point2(1, 0),
    Point2(1, 1),
    Point2(0, 1),
)

# Calculate the intersection between the diagonals
intersection = Point3()
LPlane(vtx[0], vtx[2], vtx[0] + normal).intersects_line(intersection, vtx[1], vtx[3])

for i in range(4):
    vwriter.set_data3(vtx[i])
    nwriter.set_data3(vtx[i])

    # Calculate distance to opposing point
    diag = (vtx[i] - vtx[i - 2]).length()

    # Calculate distance to intersection of opposing vertex
    dist = (vtx[i - 2] - intersection).length()

    twriter.set_data4(VBase4(*uvs[i], 0, 1) * (diag / dist))
    print(VBase4(*uvs[i], 0, 1) * (diag / dist))

tris = GeomTriangles(Geom.UH_static)
tris.add_vertices(0, 1, 3)
tris.add_vertices(1, 2, 3)

geom = Geom(vdata)
geom.add_primitive(tris)

gnode = GeomNode('trapezoid')
gnode.add_geom(geom)

card = aspect2d.attach_new_node(gnode)
card.set_scale(0.5)
card.set_pos(0.5, 0, 0)
card.set_texture(tex)

base.run()
2 Likes