Tinyrender-Lesson 2 Triangle rasterization and back face culling

Posted grass-and-moon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tinyrender-Lesson 2 Triangle rasterization and back face culling相关的知识,希望对你有一定的参考价值。

Tinyrender-Lesson 2 Triangle rasterization and back face culling

原文:https://github.com/ssloy/tinyrenderer/wiki/Lesson-2-Triangle-rasterization-and-back-face-culling

在Lesson 1中已经可以通过划线的方式绘制三角形的边。那么如何绘制填充颜色的三角形呢?

import matplotlib.pyplot as plt
from PIL import Image
import numpy as np

def draw_line(p0, p1, image, color):
    distance = abs(np.array(p0)-np.array(p1))
    # 确定距离大的一方,来确定采样步长
    maxIndex = 0 if distance[0] > distance[1] else 1
    minIndex = 1 - maxIndex

    # 计算采样步长
    step1 = 1 if p1[maxIndex] > p0[maxIndex] else -1
    step2 = 1 if p1[minIndex] > p0[minIndex] else -1
    
    # 计算另一方,一个采样步长对应的递增步长
    dy = abs((p1[minIndex] - p0[minIndex])/(p1[maxIndex] - p0[maxIndex]))
    
    b = p0[minIndex]
    error = 0
    for a in range(int(p0[maxIndex]), int(p1[maxIndex]) + step1, step1):
        if maxIndex == 0: # 说明xy顺序没有发生变化
            image.putpixel((int(a),int(b)), color)
        else:
            image.putpixel((int(b), int(a)), color)
        error = error + dy
        if error > 0.5:
            b = b + step2
            error = error - 1.
            
def draw_triangle(t0, t1, t2, image, color):
    draw_line(t0, t1, image, color)
    draw_line(t1, t2, image, color)
    draw_line(t2, t0, image, color)
    
t0 = [[10,70], [50,160], [70,80]]
t1 = [[180,50], [150, 1], [70, 180]]
t2 = [[180,150], [120,160], [130,180]]

image = Image.new("RGB", (200, 200), (0,0,0))
draw_triangle(t0[0], t0[1], t0[2], image, (255,0,0))
draw_triangle(t1[0], t1[1], t1[2], image, (255,255,255))
draw_triangle(t2[0], t2[1], t2[2], image, (0,255,0))

fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111)
ax.imshow(image)

老派方法:扫线

  1. 对点按照y轴坐标进行排序;
  2. 从左往右,找到和三角形相交的两个点
  3. 用一条线把两个点连起来

这个逻辑很简单,关键部分是求出左边的起点和右边的终点。

def draw_triangle2(vertices, image, color):
    vertices = vertices[vertices[:,1].argsort()]
    diff_y1_y0 = vertices[1][1] - vertices[0][1] + 1
    diff_y2_y1 = vertices[2][1] - vertices[1][1] + 1
    diff_y2_y0 = vertices[2][1] - vertices[0][1] + 1
    for y in range(vertices[0][1], vertices[1][1]+1):
        ratio0 = (y - vertices[0][1])/diff_y1_y0
        ratio1 = (y - vertices[0][1])/diff_y2_y0
        p0 = vertices[0] * (1-ratio0) + vertices[1] * ratio0
        p1 = vertices[0] * (1-ratio1) + vertices[2] * ratio1
        draw_line([p0[0],y], [p1[0],y], image, color)
    for y in range(vertices[1][1], vertices[2][1]+1):
        ratio0 = (y - vertices[1][1])/diff_y2_y1
        ratio1 = (y - vertices[0][1])/diff_y2_y0
        p0 = vertices[1] * (1-ratio0) + vertices[2] * ratio0
        p1 = vertices[0] * (1-ratio1) + vertices[2] * ratio1
        draw_line([p0[0],y], [p1[0],y], image, color)

t0 = np.array([[10,70], [50,160], [70,80]])
t1 = np.array([[180,50], [150, 1], [70, 180]])
t2 = np.array([[180,150], [120,160], [130,180]])

image = Image.new("RGB", (200, 300), (0,0,0))
draw_triangle2(t0, image, (255,0,0))
draw_triangle2(t1, image, (255,255,255))
draw_triangle2(t2, image, (0,255,0))

fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111)
ax.imshow(image)

技术图片

利用重心坐标

利用重心坐标的实现,也可以参见本人另一篇文章:https://zhuanlan.zhihu.com/p/149057777

下面给出基于taichi的实现,taichi见:https://github.com/taichi-dev/taichi

import taichi as ti

ti.init(arch=ti.cpu)

n = 200
pixels = ti.Vector(3, dt=ti.f32, shape=(n,n+100))

@ti.kernel
def draw_triangle(p0i:ti.ext_arr(), p1i:ti.ext_arr(), p2i:ti.ext_arr(), colori:ti.ext_arr(), n:ti.int64):
    for i, j in pixels:
        # 计算重心坐标
        for it in range(n):
            p0 = ti.Vector([p0i[it,0], p0i[it,1]])
            p1 = ti.Vector([p1i[it,0], p1i[it,1]])
            p2 = ti.Vector([p2i[it,0], p2i[it,1]])
            color = ti.Vector([colori[it,0], colori[it,1], colori[it,2]])
            mat = ti.Matrix.cols([p0-p2,p1-p2])
            matInv = mat.inverse()
            p = ti.Vector([i-p2[0],j-p2[1]])
            pp = matInv @ p
            if pp[0] > 0 and pp[1] > 0 and 1 - pp[0] - pp[1] > 0:
                pixels[i,j] = color
        
p0 = np.array([[10, 70],[180,50]])
p1 = np.array([[50,160],[150,1]])
p2 = np.array([[70, 80],[70,180]])

color = np.array([[255,0,0],[255,255,0]])
draw_triangle(p0, p1, p2, color, 2)

fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111)
ax.imshow(pixels.to_numpy())

技术图片

这里可能比较好奇,为啥得到的图像和之前的是反的?因为对于图像而言,(m,n),m指的是宽,n指的是高。对于数组(m,n)而言,m指的是行数,n指的是列数。这个可以自己试着调整下。

下面来看下利用这个绘制函数来绘制下https://github.com/ssloy/tinyrenderer/tree/master/obj/african_head 中的african_head.obj

import taichi as ti

ti.init(arch=ti.cpu)

n = 200
pixels = ti.Vector(3, dt=ti.int32, shape=(n,n))

@ti.kernel
def draw_triangle(p0i:ti.ext_arr(), p1i:ti.ext_arr(), p2i:ti.ext_arr(), colori:ti.ext_arr(), n:ti.int64):
    for i, j in pixels:
        # 计算重心坐标
        for it in range(n):
            p0 = ti.Vector([p0i[it,0], p0i[it,1]])
            p1 = ti.Vector([p1i[it,0], p1i[it,1]])
            p2 = ti.Vector([p2i[it,0], p2i[it,1]])
            color = ti.Vector([colori[it,0], colori[it,1], colori[it,2]])
            mat = ti.Matrix.cols([p0-p2,p1-p2])
            matInv = mat.inverse()
            p = ti.Vector([i-p2[0],j-p2[1]])
            pp = matInv @ p
            if pp[0] > 0 and pp[1] > 0 and 1 - pp[0] - pp[1] > 0:
                pixels[i,j] = color

from vispy import io

verts, faces, normals, nothing = io.read_mesh("data/african_head.obj")

p0 = np.zeros([faces.shape[0], 2])
p1 = np.zeros([faces.shape[0], 2])
p2 = np.zeros([faces.shape[0], 2])
color = np.random.randint(0, 255, (faces.shape[0], 3))

for i in range(faces.shape[0]):
    p0[i] = (verts[faces[i,0]][:2] + 1) * n/2
    p1[i] = (verts[faces[i,1]][:2] + 1) * n/2
    p2[i] = (verts[faces[i,2]][:2] + 1) * n/2
    
draw_triangle(p0, p1, p2, color, faces.shape[0])

fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111)
ax.imshow(pixels.to_numpy())

技术图片

在原文中还引入了光照以及法向量,此处不进行考虑。仅对back face culling进行简单说明,三角形的三个点排序后可能是顺时针方向,也可能是逆时针方向,如果我们规定,逆时针方向为正面,那么顺时针方向的三角形就是背面,那么back face culling的意思就是不绘制顺时针方向的点。对上面的例子进行更改,如果逆时针方向,那么就是红色,如果是顺时针方向,那么就是白色。由于obj中的三角片面的顺序和正面反面无关,因此必然会得到不同颜色相交的现象。

import taichi as ti

ti.init(arch=ti.cpu)

n = 200
pixels = ti.Vector(3, dt=ti.int32, shape=(n,n))

@ti.kernel
def draw_triangle(p0i:ti.ext_arr(), p1i:ti.ext_arr(), p2i:ti.ext_arr(), colori:ti.ext_arr(), n:ti.int64):
    for i, j in pixels:
        # 计算重心坐标
        for it in range(n):
            p0 = ti.Vector([p0i[it,0], p0i[it,1]])
            p1 = ti.Vector([p1i[it,0], p1i[it,1]])
            p2 = ti.Vector([p2i[it,0], p2i[it,1]])
            color = ti.Vector([colori[it,0], colori[it,1], colori[it,2]])
            mat = ti.Matrix.cols([p0-p2,p1-p2])
            matInv = mat.inverse()
            p = ti.Vector([i-p2[0],j-p2[1]])
            pp = matInv @ p
            if pp[0] > 0 and pp[1] > 0 and 1 - pp[0] - pp[1] > 0:
                pixels[i,j] = color

from vispy import io

verts, faces, normals, nothing = io.read_mesh("data/african_head.obj")

p0 = np.zeros([faces.shape[0], 2])
p1 = np.zeros([faces.shape[0], 2])
p2 = np.zeros([faces.shape[0], 2])
color = np.zeros([faces.shape[0], 3], dtype=np.int32)

for i in range(faces.shape[0]):
    p0[i] = (verts[faces[i,0]][:2] + 1) * n/2
    p1[i] = (verts[faces[i,1]][:2] + 1) * n/2
    p2[i] = (verts[faces[i,2]][:2] + 1) * n/2
    crossdir = np.cross((verts[faces[i,1]] - verts[faces[i,0]]),(verts[faces[i,2]] - verts[faces[i,1]]))
    # 不考虑坐标变换
    if crossdir[2] > 0:
        color[i] = np.array([255,0,0], dtype=np.int32)
    else:
        color[i] = np.array([255,255,255], dtype=np.int32)
    
draw_triangle(p0, p1, p2, color, faces.shape[0])

fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111)
ax.imshow(pixels.to_numpy())

技术图片

以上是关于Tinyrender-Lesson 2 Triangle rasterization and back face culling的主要内容,如果未能解决你的问题,请参考以下文章

Tinyrender-Lesson 1 Bresenham’s Line Drawing Algorithm

model.trian()及model.eval()

LeetCode开心刷题五十一天——118. Pascal's Triangle 接触跳转表概念,不知用处 lamda逗号导致表达式加法奇怪不理解119. Pascal's Trian

[python]捕获ctrl+C事件后处理

补题多校联合训练第二场

动态规划dp小练习课堂