Skip to content

Instantly share code, notes, and snippets.

@turgenevivan
Last active June 13, 2022 02:01
Show Gist options
  • Save turgenevivan/2db23f84d1cc09a980b09ce0ada0a642 to your computer and use it in GitHub Desktop.
Save turgenevivan/2db23f84d1cc09a980b09ce0ada0a642 to your computer and use it in GitHub Desktop.
tinyrenderer.js
<!--
(y)
|
|
(0,0)------ > (x)
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Gamedev Canvas Workshop</title>
<style>
* { padding: 0; margin: 0; }
canvas { background: rgb(17, 1, 7); display: block; margin: 0 auto; }
</style>
</head>
<body>
<canvas id="myCanvas" width="500" height="500"></canvas>
<script> // namespace rr
var rr = {}
rr.p = function (x, y) {
return new Point(x, y)
}
rr.EPSILON = 0.0001
rr.isZero = function(v) {
return (Math.abs(v) < rr.EPSILON)
}
rr.between = function(min_, b, max_) {
return (min_ - rr.EPSILON <= b) && (b <= max_ + rr.EPSILON);
}
rr.eq = function (a,b) {
return rr.isZero(a-b)
}
</script>
<script> // read file
function readTextFile(file) {
var rawFile = new XMLHttpRequest();
rawFile.open("GET", file, false);
// rawFile.onreadystatechange = function ()
// {
// if(rawFile.readyState === 4)
// {
// if(rawFile.status === 200 || rawFile.status == 0)
// {
// allText = rawFile.responseText;
// // alert(allText);
// }
// }
// }
rawFile.send(null);
return rawFile.responseText;
}
</script>
<script>
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
}
</script>
<script> // model
class ObjModel {
constructor() {
this.clear()
}
clear() {
this.vArray = [[]]
this.fArray = []
}
parse(lines) {
for (let index = 0; index < lines.length; index++) {
var linedata = lines[index]
var datas = linedata.split(" ")
if (datas[0] == 'v') {
this.vArray.push([
parseFloat(datas[1]),
parseFloat(datas[2]),
parseFloat(datas[3])
])
}
else if (datas[0] == 'f') {
this.fArray.push([
toIntArray(datas[1].split("/")),
toIntArray(datas[2].split("/")),
toIntArray(datas[3].split("/"))]
)
}
}
}
vert(index) {
return this.vArray[index]
}
}
</script>
<script>
// JavaScript code goes here
var canvas = document.getElementById("myCanvas")
var ctx = canvas.getContext("2d")
var width = canvas.width
var height = canvas.height
var fps = 1000.0 / 60;
// https://www.w3schools.com/jsref/canvas_createimagedata.asp
var imgData = ctx.createImageData(width, height);
var screen = imgData.data;
// var modelText = readTextFile("obj/african_head.obj")
// var lines = modelText.split('\n')
// var obj = new ObjModel();
// obj.parse(lines)
function segment_intersection(x1, y1, x2, y2, x3, y3, x4, y4) {
return segmentsIntr(
rr.p(x1, y1),
rr.p(x2, y2),
rr.p(x3, y3),
rr.p(x4, y4),
)
}
// https://www.iteye.com/blog/fins-1522259
function segmentsIntr3(a, b, c, d) {
/** 1 解线性方程组, 求线段交点. **/
// 如果分母为0 则平行或共线, 不相交
var denominator = (b.y - a.y) * (d.x - c.x) - (a.x - b.x) * (c.y - d.y);
if (denominator == 0) {
return false;
}
// 线段所在直线的交点坐标 (x , y)
var x = ((b.x - a.x) * (d.x - c.x) * (c.y - a.y)
+ (b.y - a.y) * (d.x - c.x) * a.x
- (d.y - c.y) * (b.x - a.x) * c.x) / denominator;
var y = -((b.y - a.y) * (d.y - c.y) * (c.x - a.x)
+ (b.x - a.x) * (d.y - c.y) * a.y
- (d.x - c.x) * (b.y - a.y) * c.y) / denominator;
/** 2 判断交点是否在两条线段上 **/
if (
// 交点在线段1上
(x - a.x) * (x - b.x) <= 0 && (y - a.y) * (y - b.y) <= 0
// 且交点也在线段2上
&& (x - c.x) * (x - d.x) <= 0 && (y - c.y) * (y - d.y) <= 0
) {
// 返回交点p
return {
x: x,
y: y
}
}
//否则不相交
return false
}
function segmentsIntr2(a, b, c, d) {
// @FIXME: black line
if (onSegment(a,b,c)) { // c on a,b
return c;
}
if (onSegment(a,b,d)) { // d on a,b
return d;
}
//线段ab的法线N1
var nx1 = (b.y - a.y), ny1 = (a.x - b.x);
//线段cd的法线N2
var nx2 = (d.y - c.y), ny2 = (c.x - d.x);
//两条法线做叉乘, 如果结果为0, 说明线段ab和线段cd平行或共线,不相交
var denominator = nx1 * ny2 - ny1 * nx2;
if (denominator == 0) {
return false;
}
//在法线N2上的投影
var distC_N2 = nx2 * c.x + ny2 * c.y;
var distA_N2 = nx2 * a.x + ny2 * a.y - distC_N2;
var distB_N2 = nx2 * b.x + ny2 * b.y - distC_N2;
// 点a投影和点b投影在点c投影同侧 (对点在线段上的情况,本例当作不相交处理);
if (distA_N2 * distB_N2 >= 0) {
return false;
}
//
//判断点c点d 和线段ab的关系, 原理同上
//
//在法线N1上的投影
var distA_N1 = nx1 * a.x + ny1 * a.y;
var distC_N1 = nx1 * c.x + ny1 * c.y - distA_N1;
var distD_N1 = nx1 * d.x + ny1 * d.y - distA_N1;
if (distC_N1 * distD_N1 >= 0) {
return false;
}
//计算交点坐标
var fraction = distA_N2 / denominator;
var dx = fraction * ny1,
dy = -fraction * nx1;
return { x: a.x + dx, y: a.y + dy };
}
function onSegment(a,b,p) {
// (y-y1)/(y0-y1)=(x-x1)/(x0-x1)
// => (y-y1)(x0-x1)=(y0-y1)(x-x1)
var left = (p.y-b.y) * (a.x-b.x)
var right = (a.y-b.y) * (p.x-b.x)
return rr.eq(left, right)
}
function segmentsIntr(a, b, c, d) {
if (onSegment(a,b,c)) { // c on a,b
return c;
}
if (onSegment(a,b,d)) { // d on a,b
return d;
}
// 三角形abc 面积的2倍
var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);
// 三角形abd 面积的2倍
var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x);
// 面积符号相同则两点在线段同侧,不相交 (对点在线段上的情况,本例当作不相交处理);
if (area_abc * area_abd >= 0) {
return false;
}
// 三角形cda 面积的2倍
var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x);
// 三角形cdb 面积的2倍
// 注意: 这里有一个小优化.不需要再用公式计算面积,而是通过已知的三个面积加减得出.
var area_cdb = area_cda + area_abc - area_abd;
if (area_cda * area_cdb >= 0) {
return false;
}
//计算交点坐标
var t = area_cda / (area_abd - area_abc);
var dx = t * (b.x - a.x),
dy = t * (b.y - a.y);
return { x: a.x + dx, y: a.y + dy };
}
function int(v) {
return Math.floor(v)
}
function to_xy(index) // index: uint8 index, not pixel index
{
index = index / 4
var y = index / width
y = Math.floor(y)
var x = index - (y * width)
return [x, y]
}
function to_index(x, y) {
var k = x + (height - y - 1) * width // (height - y - 1): flip
k = k * 4 // 1pixel = (rgba)
return k
}
function toIntArray(array) {
for (let index = 0; index < array.length; index++) {
const element = array[index];
array[index] = parseInt(element)
}
return array
}
function putpixel(x, y, r, g, b, a) {
x = int(x)
y = int(y)
if (x < 0 || x > width || y < 0 || y > height)
{
return;
}
var index = to_index(x, y)
screen[index] = r
screen[index + 1] = g
screen[index + 2] = b
screen[index + 3] = a
}
function line(x0, y0, x1, y1, r, g, b, a) {
x0 = int(x0)
y0 = int(y0)
x1 = int(x1)
y1 = int(y1)
// https://baike.baidu.com/item/%E4%B8%A4%E7%82%B9%E5%BC%8F/577664#1
// (y-y1)/(y0-y1)=(x-x1)/(x0-x1)
var use_x = Math.abs(x0 - x1) > Math.abs(y0 - y1) ? true : false
if (use_x) {
var step = (x0 > x1) ? -1 : 1
var t = 1.0 / (x0 - x1)
for (var x = x0; x != x1; x = x + step) {
var k = (x - x1) * t // (x-x1)/(x0-x1)
var y = k * (y0 - y1) + y1
y = int(y)
putpixel(x, y, r, g, b, a)
}
putpixel(x1, y1, r, g, b, a)//draw the last point
}
else {
var step = (y0 > y1) ? -1 : 1
var t = 1.0 / (y0 - y1)
for (var y = y0; y != y1; y = y + step) {
var k = (y - y1) * t // (y-y1)/(y0-y1)
var x = k * (x0 - x1) + x1
x = int(x)
putpixel(x, y, r, g, b, a)
}
putpixel(x1, y1, r, g, b, a)//draw the last point
}
}
function seg_mid(x0, y0, x1, y1) {
return [int((x0 + x1) * 0.5 + 0.5), int((y0 + y1) * 0.5 + 0.5)]
}
function float_eq(v1, v2) {
return Math.abs(v1 - v2) < 0.001;
}
function fill_triangle(x0, y0, x1, y1, x2, y2, x3, y3, r, g, b, a) {
if (Math.abs(x0 - x1) <= 1 && Math.abs(y0 - y1) <= 1) {
return;
}
var [midx, midy] = seg_mid(x0, y0, x1, y1) // middle x,y
var [mx1, my1] = seg_mid(x2, y2, x3, y3)
line(midx, midy, mx1, my1, r, g, b, a)
fill_triangle(x0, y0, midx, midy, mx1, my1, x3, y3, r, g, b, a)
fill_triangle(x1, y1, midx, midy, mx1, my1, x2, y2, r, g, b, a)
}
function line_scane(t0, t1, t2, r, g, b, a) {
for (let h = 0; h < height; h++) {
p0 = segment_intersection(
0, h,
width, h,
t0[0], t0[1],
t1[0], t1[1]
)
p1 = segment_intersection(
0, h,
width, h,
t1[0], t1[1],
t2[0], t2[1]
)
p2 = segment_intersection(
0, h,
width, h,
t2[0], t2[1],
t0[0], t0[1]
)
if (p0 != false && p1 != false) {
line(p0.x, p0.y, p1.x, p1.y, r, g, b, a)
}
if (p0 && p2) {
line(p0.x, p0.y, p2.x, p2.y, r, g, b, a)
}
if (p1 && p2) {
line(p1.x, p1.y, p2.x, p2.y, r, g, b, a)
}
}
}
function triangle2(t0, t1, t2, r, g, b, a) {
// fill_triangle(t0[0],t0[1], t1[0],t1[1], t1[0],t1[1], t2[0],t2[1], r,g,b,a)
line(t0[0], t0[1], t1[0], t1[1], r, g, b, a)
line(t1[0], t1[1], t2[0], t2[1], r, g, b, a)
line(t2[0], t2[1], t0[0], t0[1], r, g, b, a)
line_scane(t0, t1, t2, r, g, b, a)
}
function triangle(t0, t1, t2, r, g, b, a) {
if (t0[1] > t1[1]) [t1,t0] = [t0,t1]
if (t0[1] > t2[1]) [t2,t0] = [t0,t2]
if (t1[1] > t2[1]) [t2,t1] = [t1,t2]
// t0,t1,t2
var x0 = t0[0]
var y0 = t0[1]
var x1 = t1[0]
var y1 = t1[1]
// down
for (let y = t0[1]; y < t1[1]; y++) {
var Ax, Ay, Bx,By
{
var k = (y - y1) / (y0 - y1) // (y-y1)/(y0-y1)
var x = k * (x0 - x1) + x1
x = int(x)
Ax = x
Ay = y
// putpixel(x, y, r, g, b, a)
}
{
var k = (y - t2[1]) / (y0 - t2[1])
var x = k * (x0 - t2[0]) + t2[0]
x = int(x)
// putpixel(x, y, r, g, b, a)
Bx = x
By = y
}
line(Ax,Ay,Bx,By,r,g,b,a)
}
// up
for (let y = t1[1]; y < t2[1]; y++) {
var Ax, Ay, Bx,By
{
var k = (y - t2[1]) / (t1[1] - t2[1]) // (y-y1)/(y0-y1)
var x = k * (t1[0] - t2[0]) + t2[0]
x = int(x)
// putpixel(x, y, r, g, b, a)
Ax = x
Ay = y
}
{
var k = (y - t2[1]) / (t0[1] - t2[1]) // (y-y1)/(y0-y1)
var x = k * (x0 - t2[0]) + t2[0]
x = int(x)
// putpixel(x, y, r, g, b, a)
Bx = x
By = y
}
line(Ax,Ay,Bx,By,r,g,b,a)
}
}
function test_triangle() {
var t0 = [[10, 70], [50, 160], [70, 80]]
var t1 = [[180, 50], [150, 1], [70, 180]]
var t2 = [[180, 150], [120, 160], [130, 180]]
triangle(t0[0], t0[1], t0[2], 255, 0, 0, 255)
triangle(t1[0], t1[1], t1[2], 0, 255, 0, 255)
triangle(t2[0], t2[1], t2[2], 0, 0, 255, 255)
}
function renderer() {
ctx.putImageData(imgData, 0, 0);
}
// mainloop
setInterval(renderer, fps)
test_triangle()
</script>
</body>
</html>
<!--
https://stackoverflow.com/questions/42410080/draw-an-exisiting-arraybuffer-into-a-canvas-without-copying
// create custom array view and fill with some random data
// var array = new Uint32Array(width * height);
// for(var i=0; i < array.length; i++) {
// array[i] = 0xff000000 | (Math.sin(i*0.0001) * 0xffffff);
// }
// var iData = new ImageData(new Uint8ClampedArray(array.buffer), width, height);
// ctx.putImageData(iData, 0, 0);
-->
@turgenevivan
Copy link
Author

draw model: with some issue, there lack of some lines at left bottom:

chrome_mepA1LZMud

@turgenevivan
Copy link
Author

    function fill_triangle(x0,y0,x1,y1, x2,y2,x3,y3, r,g,b,a)
    {
        if (Math.abs(x0-x1) <= 1 && Math.abs(y0-y1) <= 1)
        {
            return;
        }
        var [midx,midy] = seg_mid(x0,y0,x1,y1) // middle x,y
        var [mx1,my1] = seg_mid(x2,y2,x3,y3)

        line(midx,midy,mx1,my1, r,g,b,a)
        fill_triangle(x0,y0,midx,midy, mx1,my1,x3,y3, r,g,b,a)
        fill_triangle(x1,y1,midx,midy, mx1,my1,x2,y2, r,g,b,a)
    }

image

尷尬,😅,中段數值插值辦法。

@turgenevivan
Copy link
Author

turgenevivan commented Jun 8, 2022

行掃描大法:

    function line_scane(t0, t1, t2, r,g,b,a)
    {
        for (let h = 0; h < height; h++)  // 可以優化
        {
            p0 = segment_intersection(
                0, h,
                width, h,
                t0[0], t0[1],
                t1[0], t1[1]
            )
            p1 = segment_intersection(
                0, h,
                width, h,
                t1[0], t1[1],
                t2[0], t2[1]
            )
            p2 = segment_intersection(
                0, h,
                width, h,
                t2[0], t2[1],
                t0[0], t0[1]
            )

            if (p0 != false && p1 != false)
            {
                line(p0.x,p0.y, p1.x,p1.y, r,g,b,a)
            }
            if (p0 && p2)
            {
                line(p0.x,p0.y, p2.x,p2.y, r,g,b,a)
            }
            if (p1 && p2)
            {
                line(p1.x,p1.y, p2.x,p2.y, r,g,b,a)
            }
        }
    }

image

@turgenevivan
Copy link
Author

點是否在線段上:計算機不喜歡除法。

    function onSegment(a,b,p) {

        // (y-y1)/(y0-y1)=(x-x1)/(x0-x1)
        // => (y-y1)(x0-x1)=(y0-y1)(x-x1)
        var left = (p.y-b.y) * (a.x-b.x)
        var right = (a.y-b.y) * (p.x-b.x)
        return rr.eq(left, right)
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment