Last active
June 13, 2022 02:01
-
-
Save turgenevivan/2db23f84d1cc09a980b09ce0ada0a642 to your computer and use it in GitHub Desktop.
tinyrenderer.js
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
<!-- | |
(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); | |
--> |
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 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
draw model: with some issue, there lack of some lines at left bottom: