Last active
December 29, 2022 21:31
-
-
Save bbbradsmith/a65212dbbb917b5bc449995f6333de66 to your computer and use it in GitHub Desktop.
baricentric_perspective rendering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A recreation of: | |
# https://commons.wikimedia.org/wiki/File:Perspective_correct_texture_mapping.jpg | |
# | |
# To give a simple code demonstration of perspective correction, | |
# and simple barycentric triangle coordinates. | |
# | |
# Note that this is a "naive" implementation, | |
# and the resulting floating point errors will produce some rough edges on the checkerboard texture. | |
# It would be more efficient and more numerically stable to rasterize the triangle instead, | |
# but this demonstrates a simple way to generate coordinates without the additional complexity of rasterization. | |
# A simple 2x anti-aliasing is added to make it look slightly nicer (see: scale). | |
# | |
# References: | |
# https://en.wikipedia.org/wiki/Texture_mapping#Affine_texture_mapping | |
# https://en.wikipedia.org/wiki/Barycentric_coordinate_system | |
import PIL.Image | |
import math | |
colbg = (180,179,210) | |
col0 = ( 0, 0, 0) | |
col1 = (255,255,255) | |
colodd = (255, 0, 0) | |
coleven = ( 0,255, 0) | |
edge = 0.05 # edge highlight width | |
check = 0.125 # checkerboard width (0 for red-green) | |
scale = 2 # simple anti-aliasing (1 = off) | |
img = PIL.Image.new("RGB",(800*scale,300*scale),colbg) | |
triangles = [ | |
( 50, 50,1),(250, 50,1),( 50,250,1), # flat square | |
( 50,250,1),(250, 50,1),(250,250,1), | |
(350,100,1),(450,100,1),(300,250,1), # flat trapezoid | |
(300,250,1),(450,100,1),(500,250,1), | |
(600,100,2),(700,100,2),(550,250,1), # perspective square, already projected with Z intact | |
(550,250,1),(700,100,2),(750,250,1), | |
] | |
texture = [ # texture coordinates for even/odd triangles (u,v,1) | |
(0,0,1),(1,0,1),(0,1,1), | |
(0,1,1),(1,0,1),(1,1,1) ] | |
def vec_sum(a,b): # vector add | |
return tuple([a[i]+b[i] for i in range(len(a))]) | |
def vec_sub(a,b): # vector subtract | |
return tuple([a[i]-b[i] for i in range(len(a))]) | |
def vec_mul(v,m): # vector * scalar | |
return tuple([x * m for x in v]) | |
def vec2_cross(a,b): # 2D cross product | |
return a[0] * b[1] - a[1] * b[0] | |
def triangle_area(t): # 2D area of x,y (ignores z) | |
u = vec_sub(t[1],t[0]) | |
v = vec_sub(t[2],t[0]) | |
return vec2_cross(u,v) / 2 | |
def bary_lerp(v,w): # sum of 3 vectors * 3 weights | |
return vec_sum(vec_sum( | |
vec_mul(v[0],w[0]), | |
vec_mul(v[1],w[1])), | |
vec_mul(v[2],w[2])) | |
def blend(ca,cb): # 50% blend of 2 colours | |
return tuple([(ca[i]+cb[i])//2 for i in range(len(ca))]) | |
for i in range(0,len(triangles),3): | |
t = triangles[i:i+3] | |
uv = texture[i%6:(i%6)+3] | |
coledge = coleven if (i % 6) == 0 else colodd # edge highlight colour even/odd triangles | |
x0 = min(x for (x,y,z) in t) * scale | |
x1 = max(x for (x,y,z) in t) * scale | |
y0 = min(y for (x,y,z) in t) * scale | |
y1 = max(y for (x,y,z) in t) * scale | |
a = triangle_area(t) # area of full triangle | |
for y in range(y0,y1+1): | |
for x in range(x0,x1+1): | |
# barycentric coordinates: area of edge + point triangle, divided by full area | |
tp = (x/scale,y/scale,1) | |
e0 = triangle_area([t[1],t[2],tp]) / a | |
e1 = triangle_area([t[2],t[0],tp]) / a | |
e2 = triangle_area([t[0],t[1],tp]) / a | |
if e0 < 0 or e1 < 0 or e2 < 0 or e0 > 1 or e1 > 1 or e2 > 1: | |
continue | |
# perspective correction | |
uv_z = [vec_mul(uv[j],1/t[j][2]) for j in range(3)] # divide uv/z before interpolation | |
l = bary_lerp(uv_z,(e0,e1,e2)) # linearly interpolate u/z, v/z, 1/z | |
(u,v) = (l[0]/l[2], l[1]/l[2]) # divide by interpolated 1/z | |
# checkerboard texture | |
if check != 0: | |
col = col0 if 0 == ((int(u/check) ^ int(v/check)) & 1) else col1 | |
if e0 < edge or e1 < edge or e2 < edge: # edge highlight | |
col = blend(col,coledge) | |
else: # red-green UV visualization | |
col = (int(u*255),int(v*255),0) | |
img.putpixel((x,y),col) | |
img = img.resize((img.width//scale,img.height//scale),PIL.Image.Resampling.BILINEAR) | |
img.save("baricentric_perspective.png") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment