예제 #1
0
def render_line(model, camera, face, w0=0, w1=1):
    posa, posb = face.pos   # Position
    texa, texb = face.tex   # TexCoord
    nrma, nrmb = face.nrm   # Normal

    clra = [posa, texa, nrma]
    clrb = [posb, texb, nrmb]

    A = camera.uncook(posa)
    B = camera.uncook(posb)

    M = int(ti.floor(min(A, B) - 1))
    N = int(ti.ceil(max(A, B) + 1))
    M = ts.clamp(M, 0, ti.Vector(camera.fb.res))
    N = ts.clamp(N, 0, ti.Vector(camera.fb.res))

    B_A = (B - A).normalized()

    for X in ti.grouped(ti.ndrange((M.x, N.x), (M.y, N.y))):
        udf = abs((X - A).cross(B_A))
        if udf >= w1:
            continue

        strength = ts.smoothstep(udf, w1, w0)
        color = ts.vec3(strength)
        camera.img[X] += color
예제 #2
0
def render_particle(model, camera, index):
    scene = model.scene
    a = (model.L2C[None] @ ts.vec4(model.pos[index], 1)).xyz
    r = model.radius[index]
    A = camera.uncook(a)

    rad = camera.uncook(ts.vec3(r, r, a.z), False)

    M = int(ti.floor(A - rad))
    N = int(ti.ceil(A + rad))
    M = ts.clamp(M, 0, ti.Vector(camera.res))
    N = ts.clamp(N, 0, ti.Vector(camera.res))

    for X in ti.grouped(ti.ndrange((M.x, N.x), (M.y, N.y))):
        pos = camera.cook(float(ts.vec3(X, a.z)))
        dp = pos - a
        dp2 = dp.norm_sqr()

        if dp2 > r**2:
            continue

        dz = ti.sqrt(r**2 - dp2)
        if camera.fb.atomic_depth(X, a.z - dz):
            continue

        n = ts.vec3(dp.xy, -dz)
        normal = ts.normalize(n)
        view = ts.normalize(a + n)

        color = model.colorize(pos, normal)
        camera.fb['img'][X] = color
        camera.fb['normal'][X] = normal
예제 #3
0
 def sample(self, v):
     x = ts.clamp(int(v[0]), 0, self.wid - 1)
     y = ts.clamp(int(v[1]), 0, self.hgt - 1)
     RGBA = self.buf[x, y]
     R = float((RGBA & 0x00FF0000) >> 16) / 255.0
     G = float((RGBA & 0x0000FF00) >> 8) / 255.0
     B = float((RGBA & 0x000000FF)) / 255.0
     return ti.Vector([R, G, B])
예제 #4
0
def softray(ro, rd, hn):
    res = 1.0
    t = 0.0005
    h = 1.0
    for _ in range(0, 40):
        h = scene(ro + rd * t)
        res = ti.min(res, hn * h / t)
        t += ts.clamp(h, 0.02, 2.0)
    return ts.clamp(res, 0.0, 1.0)
예제 #5
0
def getGlow(minPDist):
    mainGlow = minPDist * 1.2
    mainGlow = pow(mainGlow, 32.0)
    mainGlow = ts.clamp(mainGlow, 0.0, 1.0)

    outerGlow = minPDist * 0.4
    outerGlow = pow(outerGlow, 2.0)
    outerGlow = ts.clamp(outerGlow, 0.0, 1.0)

    return ti.Vector([10.0, 5.0, 3.0, min(mainGlow + outerGlow, 1.0)])
예제 #6
0
 def denoise(self, alpha: ti.template()):
     ti.static_print('denoise', alpha)
     if ti.static(alpha != 0):
         for I in ti.grouped(self.buf):
             center = ts.clamp(self.buf[I])
             around = ts.clamp((self.buf[I + ts.D.x_] + self.buf[I + ts.D.X_] + self.buf[I + ts.D._x] + self.buf[I + ts.D._X]) / 4)
             #amax = ts.clamp(max(self.buf[I + ts.D.x_], self.buf[I + ts.D.X_], self.buf[I + ts.D._x], self.buf[I + ts.D._X]))
             #amin = ts.clamp(min(self.buf[I + ts.D.x_], self.buf[I + ts.D.X_], self.buf[I + ts.D._x], self.buf[I + ts.D._X]))
             #if center <= amin + throttle or center >= amax - throttle:
             self.buf[I] = center * (1 - alpha) + around * alpha
예제 #7
0
def update_display():
    for i in ti.grouped(x):
        j = i.dot(tl.vec(N, 1))
        model.pos[j] = x[i]

        xa = x[tl.clamp(i + tl.D.x_, 0, tl.vec(*NN) - 1)]
        xb = x[tl.clamp(i + tl.D.X_, 0, tl.vec(*NN) - 1)]
        ya = x[tl.clamp(i + tl.D._x, 0, tl.vec(*NN) - 1)]
        yb = x[tl.clamp(i + tl.D._X, 0, tl.vec(*NN) - 1)]
        normal = (ya - yb).cross(xa - xb).normalized()
        model.nrm[j] = normal
        model.nrm[N**2 + j] = -normal
    def sample_volume_trilinear(self, pos):
        ''' Samples volume data at `pos` and trilinearly interpolates the value

        Args:
            pos (tl.vec3): Position to sample the volume in [-1, 1]^3

        Returns:
            float: Sampled interpolated intensity
        '''
        pos = tl.clamp(((0.5 * pos) + 0.5), 0.0, 1.0) \
              * ti.static(tl.vec3(*self.volume.shape) - 1.0 - 1e-4)
        x_low, x_high, x_frac = low_high_frac(pos.x)
        y_low, y_high, y_frac = low_high_frac(pos.y)
        z_low, z_high, z_frac = low_high_frac(pos.z)

        x_high = min(x_high, ti.static(self.volume.shape[0] - 1))
        y_high = min(y_high, ti.static(self.volume.shape[1] - 1))
        z_high = min(z_high, ti.static(self.volume.shape[2] - 1))
        # on z_low
        v000 = self.volume[x_low, y_low, z_low]
        v100 = self.volume[x_high, y_low, z_low]
        x_val_y_low = tl.mix(v000, v100, x_frac)
        v010 = self.volume[x_low, y_high, z_low]
        v110 = self.volume[x_high, y_high, z_low]
        x_val_y_high = tl.mix(v010, v110, x_frac)
        xy_val_z_low = tl.mix(x_val_y_low, x_val_y_high, y_frac)
        # on z_high
        v001 = self.volume[x_low, y_low, z_high]
        v101 = self.volume[x_high, y_low, z_high]
        x_val_y_low = tl.mix(v001, v101, x_frac)
        v011 = self.volume[x_low, y_high, z_high]
        v111 = self.volume[x_high, y_high, z_high]
        x_val_y_high = tl.mix(v011, v111, x_frac)
        xy_val_z_high = tl.mix(x_val_y_low, x_val_y_high, y_frac)
        return tl.mix(xy_val_z_low, xy_val_z_high, z_frac)
예제 #9
0
파일: Bvh.py 프로젝트: zeta1999/ti-raytrace
 def UniformSampleSphere(self, u1, u2):
     z = 1.0 - 2.0 * u1
     r = ti.sqrt(ts.clamp(1.0 - z * z, 0.0, 1.0))
     phi = 2.0 * 3.1415926 * u2
     x = r * ti.cos(phi)
     y = r * ti.sin(phi)
     return ti.Vector([x, y, z])
예제 #10
0
def accel(v: ti.template(), x: ti.template(), dt):
    for i in ti.grouped(x):
        acc = x[i] * 0
        for d in ti.static(links):
            disp = x[tl.clamp(i + d, 0, tl.vec(*NN) - 1)] - x[i]
            dis = disp.norm()
            acc += disp * (dis - L) / L**2
        v[i] += stiff * acc * dt
        v[i] *= ti.exp(-damp * dt)
예제 #11
0
 def teture2D(self, u, v):
     x = ts.clamp(u * self.wid, 0.0, self.wid - 1.0)
     y = ts.clamp(v * self.hgt, 0.0, self.hgt - 1.0)
     #   lt       rt
     #    *--------*
     #    |   ↑wbt |
     #    | ← *    |
     #    | wlr    |
     #    *--------*
     #   lb       rb
     lt = ti.Vector([ti.floor(x), ti.floor(y)])
     rt = lt + ti.Vector([1, 0])
     lb = lt + ti.Vector([0, 1])
     rb = lt + ti.Vector([1, 1])
     wbt = ts.fract(y)
     wlr = ts.fract(x)
     #print(x,y,lt,wbt,wlr)
     return ts.mix(ts.mix(self.sample(lt), self.sample(rt), wlr),
                   ts.mix(self.sample(lb), self.sample(rb), wlr), wbt)
예제 #12
0
def ambocc(pos, nor):
    occ = 0.0
    sca = 1.0
    for i in range(0, 5):
        hr = 0.01 + 0.12 * float(i) / 4.0
        aopos = nor * hr + pos
        dd = scene(aopos)
        occ += -(dd - hr) * sca
        sca *= 0.95
    return ts.clamp(1.0 - 3.0 * occ, 0.0, 1.0)
예제 #13
0
def splat(data, weights, v, pos, cp):
    tot = data.shape
    dim = len(tot)

    I0 = ti.Vector.zero(int, len(tot))
    I1 = ti.Vector.zero(int, len(tot))
    w = ti.zero(pos)

    for k in ti.static(range(len(tot))):
        I0[k] = ts.clamp(int(pos[k]), 0, tot[k] - 1)
        I1[k] = ts.clamp(I0[k] + 1, 0, tot[k] - 1)
        w[k] = ts.clamp(pos[k] - I0[k], 0.0, 1.0)

    for u in ti.static(ti.grouped(ti.ndrange(*((0, 2), ) * len(tot)))):
        dpos = ti.zero(pos)
        I = ti.Vector.zero(int, len(tot))
        W = 1.0
        for k in ti.static(range(len(tot))):
            dpos[k] = (pos[k] - I0[k]) if u[k] == 0 else (pos[k] - I1[k])
            I[k] = I0[k] if u[k] == 0 else I1[k]
            W *= (1 - w[k]) if u[k] == 0 else w[k]
        data[I] += (v + cp.dot(dpos)) * W
        weights[I] += W
예제 #14
0
def substep():
    for i in ti.grouped(x):
        acc = x[i] * 0
        for d in ti.static(links):
            disp = x[tl.clamp(i + d, 0, tl.vec(*NN) - 1)] - x[i]
            length = L * float(d).norm()
            acc += disp * (disp.norm() - length) / length**2
        v[i] += stiffness * acc * dt
    for i in ti.grouped(x):
        v[i].y -= gravity * dt
        v[i] = tl.ballBoundReflect(x[i], v[i], ball_pos, ball_radius, 6)
    for i in ti.grouped(x):
        v[i] *= ti.exp(-damping * dt)
        x[i] += dt * v[i]
예제 #15
0
def render_particle(model, camera, index):
    scene = model.scene
    L2W = model.L2W
    a = model.pos[index]
    r = model.radius[index]
    a = camera.untrans_pos(L2W @ a)
    A = camera.uncook(a)

    rad = camera.uncook(ts.vec3(r, r, a.z), False)

    M = int(ti.floor(A - rad))
    N = int(ti.ceil(A + rad))
    M = ts.clamp(M, 0, ti.Vector(camera.res))
    N = ts.clamp(N, 0, ti.Vector(camera.res))

    for X in ti.grouped(ti.ndrange((M.x, N.x), (M.y, N.y))):
        pos = camera.cook(float(ts.vec3(X, a.z)))
        dp = pos - a
        dp2 = dp.norm_sqr()

        if dp2 > r**2:
            continue

        dz = ti.sqrt(r**2 - dp2)
        zindex = 1 / (a.z - dz)

        if zindex < ti.atomic_max(camera.fb['idepth'][X], zindex):
            continue

        n = ts.vec3(dp.xy, -dz)
        normal = ts.normalize(n)
        view = ts.normalize(a + n)
        color = ts.vec3(1.0)

        color = model.colorize(pos, normal, color)
        camera.fb['img'][X] = color
        camera.fb['normal'][X] = normal
예제 #16
0
def sample(field: ti.template(), P):
    '''
    Sampling a field with indices clampped into the field shape.

    :parameter field: (Tensor)
        Specify the field to sample.

    :parameter P: (Vector)
        Specify the index in field.

    :return:
        The return value is calcuated as::

            P = clamp(P, 0, vec(*field.shape) - 1)
            return field[int(P)]
    '''
    shape = ti.Vector(field.shape())
    P = ts.clamp(P, 0, shape - 1)
    return field[int(P)]
예제 #17
0
def mandelbox(z):
    offset = z
    dz = 1.0

    for _ in range(0, 10):
        # box fold
        z = ts.clamp(z, -1.0, 1.0) * 2.0 - z
        # ball fold
        r2 = ts.dot(z, z)
        if r2 < min_radius[None]:
            tmp = (fix_radius[None] / min_radius[None])
            z *= tmp
            dz *= tmp
        elif r2 < fix_radius[None]:
            tmp = fix_radius[None] / r2
            z *= tmp
            dz *= tmp

        z = scale * z + offset
        dz = dz * ti.abs(scale) + 1.0

    return ts.length(z) / ti.abs(dz)
예제 #18
0
파일: Bvh.py 프로젝트: zeta1999/ti-raytrace
 def get_random_light_prim_index(self):
     index = (int)(ts.clamp(ti.random() * self.light_count, 0.0,
                            self.light_count)) - 1
     return self.light[index]
예제 #19
0
def render_triangle(model, camera, face):
    scene = model.scene
    L2W = model.L2W
    posa, posb, posc = model.pos[face[0, 0]], model.pos[face[1, 0]], model.pos[
        face[2, 0]]
    texa, texb, texc = model.tex[face[0, 1]], model.tex[face[1, 1]], model.tex[
        face[2, 1]]
    nrma, nrmb, nrmc = model.nrm[face[0, 2]], model.nrm[face[1, 2]], model.nrm[
        face[2, 2]]
    posa = camera.untrans_pos(L2W @ posa)
    posb = camera.untrans_pos(L2W @ posb)
    posc = camera.untrans_pos(L2W @ posc)
    nrma = camera.untrans_dir(L2W.matrix @ nrma)
    nrmb = camera.untrans_dir(L2W.matrix @ nrmb)
    nrmc = camera.untrans_dir(L2W.matrix @ nrmc)

    pos_center = (posa + posb + posc) / 3
    if ti.static(camera.type == camera.ORTHO):
        pos_center = ts.vec3(0.0, 0.0, 1.0)

    dpab = posa - posb
    dpac = posa - posc
    dtab = texa - texb
    dtac = texa - texc

    normal = ts.cross(dpab, dpac)
    tan, bitan = compute_tangent(-dpab, -dpac, -dtab, -dtac)

    # NOTE: the normal computation indicates that a front-facing face should
    # be COUNTER-CLOCKWISE, i.e., glFrontFace(GL_CCW);
    # this is to be compatible with obj model loading.
    if ts.dot(pos_center, normal) <= 0:

        clra = model.vertex_shader(posa, texa, nrma, tan, bitan)
        clrb = model.vertex_shader(posb, texb, nrmb, tan, bitan)
        clrc = model.vertex_shader(posc, texc, nrmc, tan, bitan)

        A = camera.uncook(posa)
        B = camera.uncook(posb)
        C = camera.uncook(posc)
        scr_norm = 1 / ts.cross(A - C, B - A)
        B_A = (B - A) * scr_norm
        C_B = (C - B) * scr_norm
        A_C = (A - C) * scr_norm

        # screen space bounding box
        M = int(ti.floor(min(A, B, C) - 1))
        N = int(ti.ceil(max(A, B, C) + 1))
        M = ts.clamp(M, 0, ti.Vector(camera.res))
        N = ts.clamp(N, 0, ti.Vector(camera.res))
        for X in ti.grouped(ti.ndrange((M.x, N.x), (M.y, N.y))):
            # barycentric coordinates using the area method
            X_A = X - A
            w_C = ts.cross(B_A, X_A)
            w_B = ts.cross(A_C, X_A)
            w_A = 1 - w_C - w_B
            # draw
            eps = ti.get_rel_eps() * 0.2
            is_inside = w_A >= -eps and w_B >= -eps and w_C >= -eps
            if not is_inside:
                continue
            zindex = 1 / (posa.z * w_A + posb.z * w_B + posc.z * w_C)
            if zindex < ti.atomic_max(camera.fb['idepth'][X], zindex):
                continue

            clr = [
                a * w_A + b * w_B + c * w_C
                for a, b, c in zip(clra, clrb, clrc)
            ]
            camera.fb.update(X, model.pixel_shader(*clr))
예제 #20
0
def sample(data, pos):
    tot = data.shape
    # static unfold for efficiency
    if ti.static(len(data.shape) == 2):
        i, j = ts.clamp(int(pos[0]), 0,
                        tot[0] - 1), ts.clamp(int(pos[1]), 0, tot[1] - 1)
        ip, jp = ts.clamp(i + 1, 0, tot[0] - 1), ts.clamp(j + 1, 0, tot[1] - 1)
        s, t = ts.clamp(pos[0] - i, 0.0, 1.0), ts.clamp(pos[1] - j, 0.0, 1.0)
        return \
            (data[i, j] * (1 - s) + data[ip, j] * s) * (1 - t) + \
            (data[i, jp] * (1 - s) + data[ip, jp] * s) * t

    else:
        i, j, k = ts.clamp(int(pos[0]), 0, tot[0] - 1), ts.clamp(
            int(pos[1]), 0, tot[1] - 1), ts.clamp(int(pos[2]), 0, tot[2] - 1)
        ip, jp, kp = ts.clamp(i + 1, 0, tot[0] - 1), ts.clamp(
            j + 1, 0, tot[1] - 1), ts.clamp(k + 1, 0, tot[2] - 1)
        s, t, u = ts.clamp(pos[0] - i, 0.0,
                           1.0), ts.clamp(pos[1] - j, 0.0,
                                          1.0), ts.clamp(pos[2] - k, 0.0, 1.0)
        return \
            ((data[i, j, k] * (1 - s) + data[ip, j, k] * s) * (1 - t) + \
            (data[i, jp, k] * (1 - s) + data[ip, jp, k] * s) * t) * (1 - u) + \
            ((data[i, j, kp] * (1 - s) + data[ip, j, kp] * s) * (1 - t) + \
            (data[i, jp, kp] * (1 - s) + data[ip, jp, kp] * s) * t) * u
예제 #21
0
def render_triangle(model, camera, face):
    posa, posb, posc = face.pos   # Position
    texa, texb, texc = face.tex   # TexCoord
    nrma, nrmb, nrmc = face.nrm   # Normal

    pos_center = (posa + posb + posc) / 3
    if ti.static(camera.type == camera.ORTHO):
        pos_center = ts.vec3(0.0, 0.0, 1.0)

    # NOTE: the normal computation indicates that a front-facing face should
    # be COUNTER-CLOCKWISE, i.e., glFrontFace(GL_CCW);
    # this is to be compatible with obj model loading.
    if ts.dot(pos_center, ts.cross(posa - posc, posa - posb)) >= 0:
        tan, bitan = compute_tangent(posb - posa, posc - posa, texb - texa, texc - texa)  # TODO: node-ize this

        clra = [posa, texa, nrma]  # TODO: interpolate tan and bitan? merge with nrm?
        clrb = [posb, texb, nrmb]
        clrc = [posc, texc, nrmc]

        A = camera.uncook(posa)
        B = camera.uncook(posb)
        C = camera.uncook(posc)
        scr_norm = ts.cross(A - C, B - A)
        if scr_norm != 0:  # degenerate to 'line' if zero
            B_A = (B - A) / scr_norm
            A_C = (A - C) / scr_norm

            shake = ts.vec2(0.0)
            if ti.static(camera.fb.n_taa):
                for i, s in ti.static(enumerate(map(ti.Vector, TAA_SHAKES[:camera.fb.n_taa]))):
                    if camera.fb.itaa[None] == i:
                        shake = s * 0.5

            # screen space bounding box
            M = int(ti.floor(min(A, B, C) - 1))
            N = int(ti.ceil(max(A, B, C) + 1))
            M = ts.clamp(M, 0, ti.Vector(camera.fb.res))
            N = ts.clamp(N, 0, ti.Vector(camera.fb.res))
            for X in ti.grouped(ti.ndrange((M.x, N.x), (M.y, N.y))):
                # barycentric coordinates using the area method
                X_A = X - A + shake
                w_C = ts.cross(B_A, X_A)
                w_B = ts.cross(A_C, X_A)
                w_A = 1 - w_C - w_B

                # draw
                eps = ti.get_rel_eps() * 0.2
                is_inside = w_A >= -eps and w_B >= -eps and w_C >= -eps
                if not is_inside:
                    continue

                # https://gitee.com/zxtree2006/tinyrenderer/blob/master/our_gl.cpp
                if ti.static(camera.type != camera.ORTHO):
                    bclip = ts.vec3(w_A / posa.z, w_B / posb.z, w_C / posc.z)
                    bclip /= bclip.x + bclip.y + bclip.z
                    w_A, w_B, w_C = bclip

                depth = (posa.z * w_A + posb.z * w_B + posc.z * w_C)
                if camera.fb.atomic_depth(X, depth):
                    continue

                posx, texx, nrmx = [a * w_A + b * w_B + c * w_C for a, b, c in zip(clra, clrb, clrc)]
                color = ti.static(model.material.pixel_shader(model, posx, texx, nrmx, tan, bitan))
                if ti.static(isinstance(color, dict)):
                    camera.fb.update(X, color)
                else:
                    camera.fb.update(X, dict(img=color))
예제 #22
0
 def apply_grad(self, lr: float, gamma: float, max_grad: float):
     for i in self.tf_tex:
         self.tf_momentum[i] = gamma * self.tf_momentum[i] + \
                               lr * tl.clamp(self.tf_tex.grad[i], -max_grad, max_grad)
         self.tf_tex[i] -= self.tf_momentum[i]
         self.tf_tex[i] = ti.max(self.tf_tex[i], 0)
예제 #23
0
def smoothstep(edge0, edge1, x):
    t = ts.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
    return t * t * (3 - 2 * t)
예제 #24
0
def render_pixels():
    for i in range(particle_table_len[None]):
        position = particle_pos[i].xy
        pix = int(position * kResolution)
        display_image[tl.clamp(pix, 0, kResolution - 1)] += 0.3
예제 #25
0
 def SchlickFresnel(self, u):
     m = ts.clamp(1.0 - u, 0.0, 1.0)
     m2 = m * m
     return m2 * m2 * m
예제 #26
0
 def get_normal_at(self, i):
     xa = self.pos[ts.clamp(i + ts.D.x_, 0, ts.vec2(*self.shape))]
     xb = self.pos[ts.clamp(i + ts.D.X_, 0, ts.vec2(*self.shape))]
     ya = self.pos[ts.clamp(i + ts.D._x, 0, ts.vec2(*self.shape))]
     yb = self.pos[ts.clamp(i + ts.D._X, 0, ts.vec2(*self.shape))]
     return (ya - yb).cross(xa - xb).normalized()
예제 #27
0
def render_triangle(model, camera, face):
    scene = model.scene
    L2C = model.L2C[None]  # Local to Camera, i.e. ModelView in OpenGL
    posa, posb, posc = face.pos
    texa, texb, texc = face.tex
    nrma, nrmb, nrmc = face.nrm
    posa = (L2C @ ts.vec4(posa, 1)).xyz
    posb = (L2C @ ts.vec4(posb, 1)).xyz
    posc = (L2C @ ts.vec4(posc, 1)).xyz
    nrma = (L2C @ ts.vec4(nrma, 0)).xyz
    nrmb = (L2C @ ts.vec4(nrmb, 0)).xyz
    nrmc = (L2C @ ts.vec4(nrmc, 0)).xyz

    pos_center = (posa + posb + posc) / 3
    if ti.static(camera.type == camera.ORTHO):
        pos_center = ts.vec3(0.0, 0.0, 1.0)

    dpab = posa - posb
    dpac = posa - posc
    dtab = texa - texb
    dtac = texa - texc

    normal = ts.cross(dpab, dpac)

    # NOTE: the normal computation indicates that a front-facing face should
    # be COUNTER-CLOCKWISE, i.e., glFrontFace(GL_CCW);
    # this is to be compatible with obj model loading.
    if ts.dot(pos_center, normal) <= 0:
        tan, bitan = compute_tangent(-dpab, -dpac, -dtab,
                                     -dtac)  # TODO: node-ize this

        clra = model.vertex_shader(
            posa, texa, nrma, tan,
            bitan)  # TODO: interpolate tan and bitan? merge with nrm?
        clrb = model.vertex_shader(posb, texb, nrmb, tan, bitan)
        clrc = model.vertex_shader(posc, texc, nrmc, tan, bitan)

        A = camera.uncook(posa)
        B = camera.uncook(posb)
        C = camera.uncook(posc)
        scr_norm = ts.cross(A - C, B - A)
        if scr_norm != 0:  # degenerate to 'line' if zero
            B_A = (B - A) / scr_norm
            A_C = (A - C) / scr_norm

            shake = ts.vec2(0.0)
            if ti.static(camera.fb.n_taa):
                for i, s in ti.static(
                        enumerate(map(ti.Vector,
                                      TAA_SHAKES[:camera.fb.n_taa]))):
                    if camera.fb.itaa[None] == i:
                        shake = s * 0.5

            # screen space bounding box
            M = int(ti.floor(min(A, B, C) - 1))
            N = int(ti.ceil(max(A, B, C) + 1))
            M = ts.clamp(M, 0, ti.Vector(camera.fb.res))
            N = ts.clamp(N, 0, ti.Vector(camera.fb.res))
            for X in ti.grouped(ti.ndrange((M.x, N.x), (M.y, N.y))):
                # barycentric coordinates using the area method
                X_A = X - A + shake
                w_C = ts.cross(B_A, X_A)
                w_B = ts.cross(A_C, X_A)
                w_A = 1 - w_C - w_B

                # draw
                eps = ti.get_rel_eps() * 0.2
                is_inside = w_A >= -eps and w_B >= -eps and w_C >= -eps
                if not is_inside:
                    continue

                # https://gitee.com/zxtree2006/tinyrenderer/blob/master/our_gl.cpp
                if ti.static(camera.type != camera.ORTHO):
                    bclip = ts.vec3(w_A / posa.z, w_B / posb.z, w_C / posc.z)
                    bclip /= bclip.x + bclip.y + bclip.z
                    w_A, w_B, w_C = bclip

                depth = (posa.z * w_A + posb.z * w_B + posc.z * w_C)
                if camera.fb.atomic_depth(X, depth):
                    continue

                clr = [
                    a * w_A + b * w_B + c * w_C
                    for a, b, c in zip(clra, clrb, clrc)
                ]
                camera.fb.update(X, model.pixel_shader(*clr))