Homework0: Basic Ray Tracer

花了2天时间,写了个基础版本的ray tracer

# coding=utf-8
'''
@author: guopei
'''
import taichi as ti

ti.init(arch=ti.gpu)



n = 640
ScreenWidth = n
ScreenHeight = n
ScreenBuffer = ti.Vector(3, dt=ti.f32, shape=(ScreenWidth, ScreenHeight))

# 屏幕射线
ScreenRayOrigins = ti.Vector(3, dt=ti.f32, shape=(ScreenWidth, ScreenHeight))
ScreenRayDirs = ti.Vector(3, dt=ti.f32, shape=(ScreenWidth, ScreenHeight))
ScreenRaySpecularRate = ti.var(dt = ti.i32, shape=(ScreenWidth, ScreenHeight))
# 屏幕射线是否有效
ScreenRayValid = ti.var(dt=ti.i32, shape=(ScreenWidth, ScreenHeight))
# 球的数量
SphereNum = 5
#材质标记 x:diffuse, y:specular, z:ambient
MaterialFlags = ti.Vector(3, dt=ti.i32, shape=(SphereNum))
DiffuseColors = ti.Vector(3, dt=ti.f32, shape=(SphereNum))
# 反射强度
SpecularRate = ti.var(dt = ti.f32, shape=(SphereNum))
# 环境光颜色
AmbientColors = ti.Vector(3, dt=ti.f32, shape=(SphereNum))
# 球体位置
SphereCenters = ti.Vector(3, dt=ti.f32, shape=(SphereNum))
# 球体半径
SphereRadius = ti.var(dt = ti.f32, shape=(SphereNum))


#---------------------------------------------------------
# camera参数
gCameraPos = ti.Vector(3, dt=ti.f32, shape=())
gCameraLookAt = ti.Vector(3, dt=ti.f32, shape=())

# camera fov
Fov = 3.1415926 * 0.25
# camera 宽高比
WidthHeightRatio = 1.0/1.0 


BottomLeftCorner = ti.Vector(3, dt=ti.f32, shape=())
ProjPlaneRight = ti.Vector(3, dt=ti.f32, shape=())
ProjPlaneUp = ti.Vector(3, dt=ti.f32, shape=())
ProjPlaneWidth = ti.var(dt=ti.f32, shape=())
ProjPlaneHeight = ti.var(dt=ti.f32, shape=())
#---------------------------------------------------------
# 光源定义
# 光源数量
LightNumber = 2
PointLightPositions = ti.Vector(3, dt=ti.f32, shape=(LightNumber))
PointLightColors = ti.Vector(3, dt=ti.f32, shape=(LightNumber))


#---------------------------------------------------------

gCameraPos[None] = (0,0,0)
gCameraLookAt[None] = (0,0,1)


#---------------------------------------------------------
# 球列表

SphereCenters[0] = (1,0,5)
SphereRadius[0] = 1
MaterialFlags[0] = (1, 0, 1)
DiffuseColors[0] = (1,1, 0.5)
SpecularRate[0] = 1
AmbientColors[0] = (0.3,0.3,0.3)

SphereCenters[1] = (-2.5,0,5)
SphereRadius[1] = 2
MaterialFlags[1] = (0, 1, 1 )
DiffuseColors[1] = (0.5,1, 1)
SpecularRate[1] = 1
AmbientColors[1] = (0.1,0.1,0.1)

SphereCenters[2] = (0,2,5)
SphereRadius[2] = 1
MaterialFlags[2] = (1, 0, 1 )
DiffuseColors[2] = (1,0.2, 0.2)
SpecularRate[2] = 1
AmbientColors[2] = (0.1,0.1,0.1)

SphereCenters[3] = (-2.5,4,5)
SphereRadius[3] = 1
MaterialFlags[3] = (1, 0, 1 )
DiffuseColors[3] = (0.2,1, 0.2)
SpecularRate[3] = 1
AmbientColors[3] = (0.1,0.1,0.1)

SphereCenters[4] = (-2.5,-4,5)
SphereRadius[4] = 1
MaterialFlags[4] = (1, 0, 1 )
DiffuseColors[4] = (0.2,0.2, 1)
SpecularRate[4] = 1
AmbientColors[4] = (0.1,0.1,0.1)

#SphereCenters[5] = (10, 0,5)
#SphereRadius[5] = 7
#MaterialFlags[5] = (0, 1, 1 )
#DiffuseColors[5] = (0.2,0.2, 1)
#SpecularRate[5] = 1
#AmbientColors[5] = (0.1,0.1,0.1)

#---------------------------------------------------------
# 光源列表
PointLightPositions[0] = (0,0,3)
PointLightColors[0] = (1, 1, 1)

PointLightPositions[1] = (1,0,5)
PointLightColors[1] = (4, 1, 1)




# 构造camera
@ti.kernel
def BuildCameraSetting():
    #Forward = func1()    
    Forward = (gCameraLookAt[None] - gCameraPos[None]).normalized()
    #Up = ti.Vector((0, 1, 0))
    #Right = Up.cross( Forward)
    HalfScreenWidth = ti.tan(Fov)
    HalfScreenHeight = HalfScreenWidth / WidthHeightRatio
    
    ProjPlaneRight[None] = ti.Vector((0,1,0)).cross(Forward)
    ProjPlaneUp[None] = Forward.cross(ProjPlaneRight[None])
    
    ProjPlaneWidth[None] = HalfScreenWidth * 2
    ProjPlaneHeight[None] = HalfScreenHeight * 2
    BottomLeftCorner[None] = gCameraPos[None] + Forward - ProjPlaneRight[None] * HalfScreenWidth - ProjPlaneUp[None] * HalfScreenHeight 
    
    #print("Fov", Fov, "Forward:",Forward, "HalfScreenWidth:", HalfScreenWidth, "BottomLeftCorner,", BottomLeftCorner[None])
    pass

@ti.func
def BuildRay(u, v):
    target_pt = BottomLeftCorner[None] + ProjPlaneRight[
        None] * ProjPlaneWidth[None] * u + ProjPlaneUp[
        None] * ProjPlaneWidth[None] * v
    return (gCameraPos[None], (target_pt - gCameraPos[None]).normalized())

@ti.func
def Reflect(v, n):
    return 2 * v.dot(n) * n - v

@ti.func
def IntersectWithSphere( ray_origin, ray_dir, sphere_center, sphere_radius):
    '''
          计算射线与球的交点
    @param ray_origin :ti.Vector
    '''
    hit_point = ti.Vector((0.0,0.0,0.0))
    hit_normal = ti.Vector((0.0,0.0,1.0))
    IsHit = False
    t = 0
    dist = (ray_origin - sphere_center).norm()
    if dist > sphere_radius:
        origin2center = sphere_center - ray_origin
        #计算投影向量
        proj_vec = origin2center.dot(ray_dir) * ray_dir;
        vertical_vec = proj_vec - origin2center
        d = vertical_vec.norm()
        
        if d < sphere_radius:
            # 两个交点,只取最近的那个
            dist = ti.sqrt(sphere_radius ** 2 - d ** 2)
            nor_negproj = (-proj_vec).normalized()
            hit_point = ray_origin + origin2center + vertical_vec + nor_negproj * dist
            
            hit_normal = (hit_point - sphere_center).normalized()
            is_samedir = ray_dir.dot(hit_normal) >= 0
            if is_samedir:
                IsHit = False
            else:
                IsHit = True
                t = (hit_point - ray_origin).norm()
            #print("IntersectWithSphere", "t", t, "hit_point", hit_point, "hit_normal", hit_normal, "ray_origin", ray_origin,
            #      "ray_dir", ray_dir, "origin2center", origin2center, "vertical_vec",vertical_vec,
            #      "nor_negproj", nor_negproj, "dist", dist, "hit_point", hit_point, "sphere_center", hit_point)
            
            pass
        elif d == sphere_radius:
            # 1个交点
            
            hit_point = ray_origin + origin2center + vertical_vec
            hit_normal = (hit_point - sphere_center).normalized()
            is_samedir = ray_dir.dot(hit_normal) >= 0
            if is_samedir:
                IsHit = False
            else:
                IsHit = True
                t = (hit_point - ray_origin).norm()
            pass
        else:
            # 没有交点
            pass
    else:
        IsHit = False    
        
    return (IsHit, t,hit_point, hit_normal)

@ti.func
def IntersectForShadow(ray_origin, ray_dir):
    tmin = 9999999
    is_hit = False
    for k in range(SphereNum):
        (hit, t, _, _) = IntersectWithSphere(ray_origin, ray_dir, SphereCenters[
                    k], SphereRadius[k])
        if hit and t < tmin:
            is_hit = True            
            tmin = t
            
        pass
    return (is_hit, tmin)
@ti.func
def IntersectWithSpheres(ray_origin, ray_dir):
    tmin = 9999999
    hitpoint_min = ti.Vector((0.0,0.0,0.0))
    hitnormal_min = ti.Vector((0.0,0.0,1.0))
    is_hit = False
    hit_idx = -1
    for k in range(SphereNum):
        (hit, t, hit_point, hit_normal) = IntersectWithSphere(ray_origin, ray_dir, SphereCenters[
                    k], SphereRadius[k])
        
        if hit and t < tmin:
            is_hit = True
            hitpoint_min = hit_point
            hitnormal_min = hit_normal
            tmin = t
            hit_idx = k
        pass
    return (is_hit, hit_idx, hitpoint_min, hitnormal_min, tmin)


#ti.func
def CalcUV(i, j, screen_width, screen_height):
    '''
            计算uv
    '''
    u = (i + 0.5) / screen_width
    v = (j + 0.5) / screen_height
    return (u,v) 
    


@ti.kernel
def BuildRays():
    for i, j in ScreenBuffer:
        (u,v) = CalcUV(i,j, ScreenWidth, ScreenHeight)
        (ray_origin, ray_dir) = BuildRay(u,v)
        ScreenRayOrigins[i,j] = ray_origin
        ScreenRayDirs[i,j] = ray_dir
        ScreenRaySpecularRate[i,j] = 1.0
        ScreenRayValid[i,j]=1
    
@ti.func
def ProcRay(i, j):
    ray_origin = ScreenRayOrigins[i,j]
    ray_dir = ScreenRayDirs[i,j]
    
    (is_hit, hit_idx, hitpoint_min, hitnormal_min, t) = IntersectWithSpheres(
        ray_origin, ray_dir)
    if is_hit:
        #print("hit! ray_origin", ray_origin, "ray_dir", ray_dir)
        Shade(i, j, hit_idx, hitpoint_min, hitnormal_min)
    pass


@ti.func
def Shade(i, j, sphere_idx, hit_point, hit_normal):
    if MaterialFlags[sphere_idx][0] == 1:
        # diffuse
        # 遍历所有光源,看看能不能击中光源,能的话,应用这个光源
        
        for k in range(LightNumber):
            light_pos = PointLightPositions[k]
            distance = (light_pos - hit_point).norm()
            ray_dir = (light_pos - hit_point).normalized()
            (is_hit, t) = IntersectForShadow(hit_point, ray_dir)
            if not is_hit or (is_hit and t >= distance):
                # 没有击中球,或者击中的球比光源还要远,那么这个光源能照到当前位置                
                color = ShadeForDiffuse(hit_point, hit_normal, sphere_idx, k) * ScreenRaySpecularRate[i, j]                
                ScreenBuffer[i,j] += color
        ScreenRayValid[i,j] = 0 
    if MaterialFlags[sphere_idx][1] == 1:
        # 镜面反射
        # 首先计算新的反射率
        new_specrate = SpecularRate[sphere_idx] * ScreenRaySpecularRate[i, j]        
        if new_specrate < 0.01:
            # 如果这个反射率太低了,那么就不再构造反射射线了
            ScreenRaySpecularRate[i, j] = 0
            ScreenRayValid[i,j] = 0
        else:
            # 当前这个位置反射率还够高,构造反射射线
            ScreenRaySpecularRate[i, j] = new_specrate
            ScreenRayOrigins[i,j] = hit_point
            ray_dir = ScreenRayDirs[i,j]
            ScreenRayDirs[i,j] = Reflect(-ray_dir, hit_normal)     
            ScreenRayValid[i,j] = 1       
        pass
    
    if MaterialFlags[sphere_idx][2] == 1:
        # 环境光
        ScreenBuffer[i,j] += AmbientColors[sphere_idx]
    pass

@ti.func
def ShadeForDiffuse(hit_point, hit_normal, sphere_idx, light_idx):
    light_dir = ( PointLightPositions[light_idx] - hit_point ).normalized()
    ndotl = light_dir.dot(hit_normal)
    if ndotl < 0:
        ndotl = 0
    
    return DiffuseColors[sphere_idx] * PointLightColors[light_idx] * ndotl  

@ti.kernel
def Render():
    for i,j in ScreenBuffer:
        # 最多反射5次
        for _ in range(5):
            if ScreenRayValid[i,j] != 0:
                ProcRay(i, j)        
        pass
    

#func1()
gui = ti.GUI("screen", (ScreenWidth, ScreenHeight))
# 构造 camera
BuildCameraSetting()
#构造所有射线
BuildRays()
# 清空back buffer
ScreenBuffer.fill(0)
Render()
while True:
    if gui.get_event(ti.GUI.PRESS):
        if gui.event.key == ti.GUI.LMB:
            x, y = gui.get_cursor_pos()
            gCameraLookAt[None][0] = x * 4 - 2
            gCameraLookAt[None][1] = y * 2 - 1                
            # 构造 camera
            BuildCameraSetting()
            #构造所有射线
            BuildRays()
            # 清空back buffer
            ScreenBuffer.fill(0)
            Render()                
    gui.set_image(ScreenBuffer.to_numpy())
    gui.show()
print("done")

4 个赞