<!--
!!html_title Texture mapping demo - Computer Graphics from scratch
-->
# Texture mapping demo
This demo implements [texture mapping](../14-textures.html). You can choose whether to use
perspective-correct interpolated depth values (1/z) or not, to see the difference it has on the output.
<div class="centered">
<canvas id="canvas" width=600 height=600 style="border: 1px grey solid"></canvas>
<table class="cgfs-demo-controls">
<tr>
<td><b>Depth buffer values</b></td>
<td class="text-left">
<input type="radio" name="perspective-correct-depth" onClick="SetUsePerspectiveCorrectDepth(false)"><label for="pcd-off">Linear (z)</label><br>
<input type="radio" name="perspective-correct-depth" onClick="SetUsePerspectiveCorrectDepth(true)" checked><label for="pcd-on">Perspective-correct (1/z)</label>
</td>
</tr>
</table>
</div>
<script>
"use strict";
// ======================================================================
// Low-level canvas access.
// ======================================================================
let canvas = document.getElementById("canvas");
let canvas_context = canvas.getContext("2d");
let canvas_buffer = canvas_context.getImageData(0, 0, canvas.width, canvas.height);
// A color.
function Color(r, g, b) {
return {
r, g, b,
mul: function(n) { return new Color(this.r*n, this.g*n, this.b*n); },
};
}
// The PutPixel() function.
function PutPixel(x, y, color) {
x = canvas.width/2 + (x | 0);
y = canvas.height/2 - (y | 0) - 1;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return;
}
let offset = 4*(x + canvas_buffer.width*y);
canvas_buffer.data[offset++] = color.r;
canvas_buffer.data[offset++] = color.g;
canvas_buffer.data[offset++] = color.b;
canvas_buffer.data[offset++] = 255; // Alpha = 255 (full opacity)
}
// Displays the contents of the offscreen buffer into the canvas.
function UpdateCanvas() {
canvas_context.putImageData(canvas_buffer, 0, 0);
}
// ======================================================================
// Depth buffer.
// ======================================================================
let depth_buffer = Array();
depth_buffer.length = canvas.width * canvas.height;
function UpdateDepthBufferIfCloser(x, y, inv_z) {
x = canvas.width/2 + (x | 0);
y = canvas.height/2 - (y | 0) - 1;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return false;
}
let offset = x + canvas.width*y;
if (depth_buffer[offset] == undefined || depth_buffer[offset] < inv_z) {
depth_buffer[offset] = inv_z;
return true;
}
return false;
}
function ClearAll() {
canvas.width = canvas.width;
depth_buffer = Array();
depth_buffer.length = canvas.width * canvas.height;
}
// ======================================================================
// Data model.
// ======================================================================
// A Point.
function Pt(x, y, h) {
return {x, y, h};
}
// A 3D vertex.
function Vertex(x, y, z) {
return {
x, y, z,
add: function(v) { return new Vertex(this.x + v.x, this.y + v.y, this.z + v.z); },
sub: function(v) { return new Vertex(this.x - v.x, this.y - v.y, this.z - v.z); },
mul: function(n) { return new Vertex(this.x*n, this.y*n, this.z*n); },
dot: function(vec) { return this.x*vec.x + this.y*vec.y + this.z*vec.z; },
length: function() { return Math.sqrt(this.dot(this)); },
}
}
// A 4D vertex (a 3D vertex in homogeneous coordinates).
function Vertex4(arg1, y, z, w) {
if (y == undefined) {
this.x = arg1.x;
this.y = arg1.y;
this.z = arg1.z;
this.w = arg1.w | 1;
} else {
this.x = arg1;
this.y = y;
this.z = z;
this.w = w;
}
this.add = function(v) { return new Vertex4(this.x + v.x, this.y + v.y, this.z + v.z); };
this.sub = function(v) { return new Vertex4(this.x - v.x, this.y - v.y, this.z - v.z, this.w - v.w); };
this.mul = function(n) { return new Vertex4(this.x*n, this.y*n, this.z*n, this.w); };
this.dot = function(vec) { return this.x*vec.x + this.y*vec.y + this.z*vec.z; };
this.cross = function(v2) { return new Vertex4(this.y*v2.z - this.z*v2.y, this.z*v2.x - this.x*v2.z, this.x*v2.y - this.y*v2.x); };
this.length = function() { return Math.sqrt(this.dot(this)); };
}
// A 4x4 matrix.
function Mat4x4(data) {
return {data};
}
const Identity4x4 = new Mat4x4([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]);
// A Triangle.
function Triangle(indexes, color, normals, texture, uvs) {
return {indexes, color, normals, texture, uvs}
}
// A Model.
function Model(vertices, triangles, bounds_center, bounds_radius) {
return {vertices, triangles, bounds_center, bounds_radius};
}
// An Instance.
function Instance(model, position, orientation, scale) {
this.model = model;
this.position = position;
this.orientation = orientation || Identity4x4;
this.scale = scale || 1.0;
this.transform = MultiplyMM4(MakeTranslationMatrix(this.position), MultiplyMM4(this.orientation, MakeScalingMatrix(this.scale)));
}
// The Camera.
function Camera(position, orientation) {
this.position = position;
this.orientation = orientation;
this.clipping_planes = [];
}
// A Clipping Plane.
function Plane(normal, distance) {
return {normal, distance};
}
// A Light.
const LT_AMBIENT = 0;
const LT_POINT = 1;
const LT_DIRECTIONAL = 2;
function Light(type, intensity, vector) {
return {type, intensity, vector};
}
// A Texture.
function Texture(url) {
this.image = new Image();
this.image.src = url;
let texture = this;
this.image.onload = function() {
texture.iw = texture.image.width;
texture.ih = texture.image.height;
texture.canvas = document.createElement("canvas");
texture.canvas.width = texture.iw;
texture.canvas.height = texture.ih;
let c2d = texture.canvas.getContext("2d");
c2d.drawImage(texture.image, 0, 0, texture.iw, texture.ih);
texture.pixel_data = c2d.getImageData(0, 0, texture.iw, texture.ih);
};
this.getTexel = function(u, v) {
let iu = (u*this.iw) | 0;
let iv = (v*this.ih) | 0;
let offset = (iv*this.iw*4 + iu*4);
return new Color(
this.pixel_data.data[offset + 0],
this.pixel_data.data[offset + 1],
this.pixel_data.data[offset + 2]
);
};
}
// ======================================================================
// Linear algebra and helpers.
// ======================================================================
// Makes a transform matrix for a rotation around the OY axis.
function MakeOYRotationMatrix(degrees) {
let cos = Math.cos(degrees*Math.PI/180.0);
let sin = Math.sin(degrees*Math.PI/180.0);
return new Mat4x4([[cos, 0, -sin, 0],
[ 0, 1, 0, 0],
[sin, 0, cos, 0],
[ 0, 0, 0, 1]])
}
// Makes a transform matrix for a translation.
function MakeTranslationMatrix(translation) {
return new Mat4x4([[1, 0, 0, translation.x],
[0, 1, 0, translation.y],
[0, 0, 1, translation.z],
[0, 0, 0, 1]]);
}
// Makes a transform matrix for a scaling.
function MakeScalingMatrix(scale) {
return new Mat4x4([[scale, 0, 0, 0],
[ 0, scale, 0, 0],
[ 0, 0, scale, 0],
[ 0, 0, 0, 1]]);
}
// Multiplies a 4x4 matrix and a 4D vector.
function MultiplyMV(mat4x4, vec4) {
let result = [0, 0, 0, 0];
let vec = [vec4.x, vec4.y, vec4.z, vec4.w];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
result[i] += mat4x4.data[i][j]*vec[j];
}
}
return new Vertex4(result[0], result[1], result[2], result[3]);
}
// Multiplies two 4x4 matrices.
function MultiplyMM4(matA, matB) {
let result = new Mat4x4([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
for (let k = 0; k < 4; k++) {
result.data[i][j] += matA.data[i][k]*matB.data[k][j];
}
}
}
return result;
}
// Transposes a 4x4 matrix.
function Transposed(mat) {
let result = new Mat4x4([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
result.data[i][j] = mat.data[j][i];
}
}
return result;
}
// ======================================================================
// Rasterization code.
// ======================================================================
// Scene setup.
let viewport_size = 1;
let projection_plane_z = 1;
function Interpolate(i0, d0, i1, d1) {
if (i0 == i1) {
return [d0];
}
let values = [];
let a = (d1 - d0) / (i1 - i0);
let d = d0;
for (let i = i0; i <= i1; i++) {
values.push(d);
d += a;
}
return values;
}
function DrawLine(p0, p1, color) {
let dx = p1.x - p0.x, dy = p1.y - p0.y;
if (Math.abs(dx) > Math.abs(dy)) {
// The line is horizontal-ish. Make sure it's left to right.
if (dx < 0) { let swap = p0; p0 = p1; p1 = swap; }
// Compute the Y values and draw.
let ys = Interpolate(p0.x, p0.y, p1.x, p1.y);
for (let x = p0.x; x <= p1.x; x++) {
PutPixel(x, ys[(x - p0.x) | 0], color);
}
} else {
// The line is verical-ish. Make sure it's bottom to top.
if (dy < 0) { let swap = p0; p0 = p1; p1 = swap; }
// Compute the X values and draw.
let xs = Interpolate(p0.y, p0.x, p1.y, p1.x);
for (let y = p0.y; y <= p1.y; y++) {
PutPixel(xs[(y - p0.y) | 0], y, color);
}
}
}
function DrawWireframeTriangle(p0, p1, p2, color) {
DrawLine(p0, p1, color);
DrawLine(p1, p2, color);
DrawLine(p0, p2, color);
}
// Converts 2D viewport coordinates to 2D canvas coordinates.
function ViewportToCanvas(p2d) {
return new Pt(
p2d.x * canvas.width / viewport_size | 0,
p2d.y * canvas.height / viewport_size | 0);
}
// Converts 2D canvas coordinates to 2D viewport coordinates.
function CanvasToViewport(p2d) {
return new Pt(
p2d.x * viewport_size / canvas.width,
p2d.y * viewport_size / canvas.height);
}
function ProjectVertex(v) {
return ViewportToCanvas(new Pt(
v.x * projection_plane_z / v.z,
v.y * projection_plane_z / v.z));
}
function UnProjectVertex(x, y, inv_z) {
let oz = 1.0 / inv_z;
let ux = x*oz / projection_plane_z;
let uy = y*oz / projection_plane_z;
let p2d = CanvasToViewport(Pt(ux, uy));
return new Vertex(p2d.x, p2d.y, oz);
}
// Sort the points from bottom to top.
// Technically, sort the indexes to the vertex indexes in the triangle from bottom to top.
function SortedVertexIndexes(vertex_indexes, projected) {
let indexes = [0, 1, 2];
if (projected[vertex_indexes[indexes[1]]].y < projected[vertex_indexes[indexes[0]]].y) { let swap = indexes[0]; indexes[0] = indexes[1]; indexes[1] = swap; }
if (projected[vertex_indexes[indexes[2]]].y < projected[vertex_indexes[indexes[0]]].y) { let swap = indexes[0]; indexes[0] = indexes[2]; indexes[2] = swap; }
if (projected[vertex_indexes[indexes[2]]].y < projected[vertex_indexes[indexes[1]]].y) { let swap = indexes[1]; indexes[1] = indexes[2]; indexes[2] = swap; }
return indexes;
}
function ComputeTriangleNormal(v0, v1, v2) {
let v0v1 = v1.sub(v0);
let v0v2 = v2.sub(v0);
return v0v1.cross(v0v2);
}
function ComputeIllumination(vertex, normal, camera, lights) {
let illumination = 0;
for (let l = 0; l < lights.length; l++) {
let light = lights[l];
if (light.type == LT_AMBIENT) {
illumination += light.intensity;
continue;
}
let vl;
if (light.type == LT_DIRECTIONAL) {
let cameraMatrix = Transposed(camera.orientation);
let rotated_light = MultiplyMV(cameraMatrix, new Vertex4(light.vector));
vl = rotated_light;
} else if (light.type == LT_POINT) {
let cameraMatrix = MultiplyMM4(Transposed(camera.orientation), MakeTranslationMatrix(camera.position.mul(-1)));
let transformed_light = MultiplyMV(cameraMatrix, new Vertex4(light.vector));
vl = vertex.mul(-1).add(transformed_light);
}
// Diffuse component.
let cos_alpha = vl.dot(normal) / (vl.length() * normal.length());
if (cos_alpha > 0) {
illumination += cos_alpha * light.intensity;
}
// Specular component.
let reflected = normal.mul(2*normal.dot(vl)).sub(vl);
let view = camera.position.sub(vertex);
let cos_beta = reflected.dot(view) / (reflected.length() * view.length());
if (cos_beta > 0) {
let specular = 50;
illumination += Math.pow(cos_beta, specular) * light.intensity;
}
}
return illumination;
}
const SM_FLAT = 0;
const SM_GOURAUD = 1;
const SM_PHONG = 2;
let ShadingModel = SM_PHONG;
let UseVertexNormals = true;
let UsePerspectiveCorrectDepth = true;
function EdgeInterpolate(y0, v0, y1, v1, y2, v2) {
let v01 = Interpolate(y0, v0, y1, v1);
let v12 = Interpolate(y1, v1, y2, v2);
let v02 = Interpolate(y0, v0, y2, v2);
v01.pop();
let v012 = v01.concat(v12);
return [v02, v012];
}
function RenderTriangle(triangle, vertices, projected, camera, lights, orientation) {
// Compute triangle normal. Use the unsorted vertices, otherwise the winding of the points may change.
let normal = ComputeTriangleNormal(vertices[triangle.indexes[0]], vertices[triangle.indexes[1]], vertices[triangle.indexes[2]]);
// Backface culling.
let vertex_to_camera = vertices[triangle.indexes[0]].mul(-1); // Should be Subtract(camera.position, vertices[triangle.indexes[0]])
if (vertex_to_camera.dot(normal) <= 0) {
return;
}
// Sort by projected point Y.
let indexes = SortedVertexIndexes(triangle.indexes, projected);
var [i0, i1, i2] = indexes;
var [v0, v1, v2] = [ vertices[triangle.indexes[i0]], vertices[triangle.indexes[i1]], vertices[triangle.indexes[i2]] ];
// Get attribute values (X, 1/Z) at the vertices.
let p0 = projected[triangle.indexes[i0]];
let p1 = projected[triangle.indexes[i1]];
let p2 = projected[triangle.indexes[i2]];
// Compute attribute values at the edges.
var [x02, x012] = EdgeInterpolate(p0.y, p0.x, p1.y, p1.x, p2.y, p2.x);
var [iz02, iz012] = EdgeInterpolate(p0.y, 1.0/v0.z, p1.y, 1.0/v1.z, p2.y, 1.0/v2.z);
if (triangle.texture) {
if (UsePerspectiveCorrectDepth) {
var [uz02, uz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].x / v0.z,
p1.y, triangle.uvs[i1].x / v1.z,
p2.y, triangle.uvs[i2].x / v2.z);
var [vz02, vz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].y / v0.z,
p1.y, triangle.uvs[i1].y / v1.z,
p2.y, triangle.uvs[i2].y / v2.z);
} else {
var [uz02, uz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].x,
p1.y, triangle.uvs[i1].x,
p2.y, triangle.uvs[i2].x);
var [vz02, vz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].y,
p1.y, triangle.uvs[i1].y,
p2.y, triangle.uvs[i2].y);
}
}
if (UseVertexNormals) {
let transform = MultiplyMM4(Transposed(camera.orientation), orientation);
var normal0 = MultiplyMV(transform, new Vertex4(triangle.normals[i0]));
var normal1 = MultiplyMV(transform, new Vertex4(triangle.normals[i1]));
var normal2 = MultiplyMV(transform, new Vertex4(triangle.normals[i2]));
} else {
var normal0 = normal;
var normal1 = normal;
var normal2 = normal;
}
let intensity;
if (ShadingModel == SM_FLAT) {
// Flat shading: compute lighting for the entire triangle.
let center = Vertex((v0.x + v1.x + v2.x)/3.0, (v0.y + v1.y + v2.y)/3.0, (v0.z + v1.z + v2.z)/3.0);
intensity = ComputeIllumination(center, normal0, camera, lights);
} else if (ShadingModel == SM_GOURAUD) {
// Gouraud shading: compute lighting at the vertices, and interpolate.
let i0 = ComputeIllumination(v0, normal0, camera, lights);
let i1 = ComputeIllumination(v1, normal1, camera, lights);
let i2 = ComputeIllumination(v2, normal2, camera, lights);
var [i02, i012] = EdgeInterpolate(p0.y, i0, p1.y, i1, p2.y, i2);
} else if (ShadingModel == SM_PHONG) {
// Phong shading: interpolate normal vectors.
var [nx02, nx012] = EdgeInterpolate(p0.y, normal0.x, p1.y, normal1.x, p2.y, normal2.x);
var [ny02, ny012] = EdgeInterpolate(p0.y, normal0.y, p1.y, normal1.y, p2.y, normal2.y);
var [nz02, nz012] = EdgeInterpolate(p0.y, normal0.z, p1.y, normal1.z, p2.y, normal2.z);
}
// Determine which is left and which is right.
let m = (x02.length/2) | 0;
if (x02[m] < x012[m]) {
var [x_left, x_right] = [x02, x012];
var [iz_left, iz_right] = [iz02, iz012];
var [i_left, i_right] = [i02, i012];
var [nx_left, nx_right] = [nx02, nx012];
var [ny_left, ny_right] = [ny02, ny012];
var [nz_left, nz_right] = [nz02, nz012];
var [uz_left, uz_right] = [uz02, uz012];
var [vz_left, vz_right] = [vz02, vz012];
} else {
var [x_left, x_right] = [x012, x02];
var [iz_left, iz_right] = [iz012, iz02];
var [i_left, i_right] = [i012, i02];
var [nx_left, nx_right] = [nx012, nx02];
var [ny_left, ny_right] = [ny012, ny02];
var [nz_left, nz_right] = [nz012, nz02];
var [uz_left, uz_right] = [uz012, uz02];
var [vz_left, vz_right] = [vz012, vz02];
}
// Draw horizontal segments.
for (let y = p0.y; y <= p2.y; y++) {
var [xl, xr] = [x_left[y - p0.y] | 0, x_right[y - p0.y] | 0];
// Interpolate attributes for this scanline.
var [zl, zr] = [iz_left[y - p0.y], iz_right[y - p0.y]];
let zscan = Interpolate(xl, zl, xr, zr);
if (ShadingModel == SM_GOURAUD) {
var [il, ir] = [i_left[y - p0.y], i_right[y - p0.y]];
let iscan = Interpolate(xl, il, xr, ir);
} else if (ShadingModel == SM_PHONG) {
var [nxl, nxr] = [nx_left[y - p0.y], nx_right[y - p0.y]];
var [nyl, nyr] = [ny_left[y - p0.y], ny_right[y - p0.y]];
var [nzl, nzr] = [nz_left[y - p0.y], nz_right[y - p0.y]];
var nxscan = Interpolate(xl, nxl, xr, nxr);
var nyscan = Interpolate(xl, nyl, xr, nyr);
var nzscan = Interpolate(xl, nzl, xr, nzr);
}
if (triangle.texture) {
var uzscan = Interpolate(xl, uz_left[y - p0.y], xr, uz_right[y - p0.y]);
var vzscan = Interpolate(xl, vz_left[y - p0.y], xr, vz_right[y - p0.y]);
}
for (let x = xl; x <= xr; x++) {
let inv_z = zscan[x - xl];
if (UpdateDepthBufferIfCloser(x, y, inv_z)) {
if (ShadingModel == SM_FLAT) {
// Just use the per-triangle intensity.
} else if (ShadingModel == SM_GOURAUD) {
intensity = iscan[x-xl];
} else if (ShadingModel == SM_PHONG) {
let vertex = UnProjectVertex(x, y, inv_z);
let normal = Vertex(nxscan[x - xl], nyscan[x - xl], nzscan[x - xl]);
intensity = ComputeIllumination(vertex, normal, camera, lights);
}
let color;
if (triangle.texture) {
if (UsePerspectiveCorrectDepth) {
var u = uzscan[x - xl] / zscan[x - xl];
var v = vzscan[x - xl] / zscan[x - xl];
} else {
var u = uzscan[x - xl];
var v = vzscan[x - xl];
}
color = triangle.texture.getTexel(u, v);
} else {
color = triangle.color;
}
PutPixel(x, y, color.mul(intensity));
}
}
}
}
// Clips a triangle against a plane. Adds output to triangles and vertices.
function ClipTriangle(triangle, plane, triangles, vertices) {
let v0 = vertices[triangle.indexes[0]];
let v1 = vertices[triangle.indexes[1]];
let v2 = vertices[triangle.indexes[2]];
let in0 = plane.normal.dot(v0) + plane.distance > 0;
let in1 = plane.normal.dot(v1) + plane.distance > 0;
let in2 = plane.normal.dot(v2) + plane.distance > 0;
let in_count = in0 + in1 + in2;
if (in_count == 0) {
// Nothing to do - the triangle is fully clipped out.
} else if (in_count == 3) {
// The triangle is fully in front of the plane.
triangles.push(triangle);
} else if (in_count == 1) {
// The triangle has one vertex in. Output is one clipped triangle.
} else if (in_count == 2) {
// The triangle has two vertices in. Output is two clipped triangles.
}
}
function TransformAndClip(clipping_planes, model, scale, transform) {
// Transform the bounding sphere, and attempt early discard.
let center = MultiplyMV(transform, new Vertex4(model.bounds_center));
let radius = model.bounds_radius*scale;
for (let p = 0; p < clipping_planes.length; p++) {
let distance = clipping_planes[p].normal.dot(center) + clipping_planes[p].distance;
if (distance < -radius) {
return null;
}
}
// Apply modelview transform.
let vertices = [];
for (let i = 0; i < model.vertices.length; i++) {
vertices.push(MultiplyMV(transform, new Vertex4(model.vertices[i])));
}
// Clip the entire model against each successive plane.
let triangles = model.triangles.slice();
for (let p = 0; p < clipping_planes.length; p++) {
let new_triangles = []
for (let i = 0; i < triangles.length; i++) {
ClipTriangle(triangles[i], clipping_planes[p], new_triangles, vertices);
}
triangles = new_triangles;
}
return Model(vertices, triangles, center, model.bounds_radius);
}
function RenderModel(model, camera, lights, orientation) {
let projected = [];
for (let i = 0; i < model.vertices.length; i++) {
projected.push(ProjectVertex(new Vertex4(model.vertices[i])));
}
for (let i = 0; i < model.triangles.length; i++) {
RenderTriangle(model.triangles[i], model.vertices, projected, camera, lights, orientation);
}
}
function RenderScene(camera, instances, lights) {
let cameraMatrix = MultiplyMM4(Transposed(camera.orientation), MakeTranslationMatrix(camera.position.mul(-1)));
for (let i = 0; i < instances.length; i++) {
let transform = MultiplyMM4(cameraMatrix, instances[i].transform);
let clipped = TransformAndClip(camera.clipping_planes, instances[i].model, instances[i].scale, transform);
if (clipped != null) {
RenderModel(clipped, camera, lights, instances[i].orientation);
}
}
}
// ----- Sphere model generator -----
function GenerateSphere(divs, color) {
let vertices = [];
let triangles = [];
let delta_angle = 2.0*Math.PI / divs;
// Generate vertices and normals.
for (let d = 0; d < divs + 1; d++) {
let y = (2.0 / divs) * (d - divs/2);
let radius = Math.sqrt(1.0 - y*y);
for (let i = 0; i < divs; i++) {
let vertex = new Vertex(radius*Math.cos(i*delta_angle), y, radius*Math.sin(i*delta_angle));
vertices.push(vertex);
}
}
// Generate triangles.
for (let d = 0; d < divs; d++) {
for (let i = 0; i < divs; i++) {
let i0 = d*divs + i;
let i1 = (d+1)*divs + (i+1)%divs;
let i2 = divs*d + (i+1)%divs;
let tri0 = [i0, i1, i2];
let tri1 = [i0, i0+divs, i1];
triangles.push(Triangle(tri0, color, [vertices[tri0[0]], vertices[tri0[1]], vertices[tri0[2]]]));
triangles.push(Triangle(tri1, color, [vertices[tri1[0]], vertices[tri1[1]], vertices[tri1[2]]]));
}
}
return new Model(vertices, triangles, new Vertex(0, 0, 0), 1.0);
}
// ----- Cube model -----
const vertices = [
new Vertex(1, 1, 1),
new Vertex(-1, 1, 1),
new Vertex(-1, -1, 1),
new Vertex(1, -1, 1),
new Vertex(1, 1, -1),
new Vertex(-1, 1, -1),
new Vertex(-1, -1, -1),
new Vertex(1, -1, -1)
];
const RED = new Color(255, 0, 0);
const GREEN = new Color(0, 255, 0);
const BLUE = new Color(0, 0, 255);
const YELLOW = new Color(255, 255, 0);
const PURPLE = new Color(255, 0, 255);
const CYAN = new Color(0, 255, 255);
const wood_texture = new Texture("./crate-texture.jpg");
const triangles = [
new Triangle([0, 1, 2], RED, [new Vertex( 0, 0, 1), new Vertex( 0, 0, 1), new Vertex( 0, 0, 1)], wood_texture, [new Pt(0, 0), new Pt(1, 0), new Pt(1, 1)]),
new Triangle([0, 2, 3], RED, [new Vertex( 0, 0, 1), new Vertex( 0, 0, 1), new Vertex( 0, 0, 1)], wood_texture, [new Pt(0, 0), new Pt(1, 1), new Pt(0, 1)]),
new Triangle([4, 0, 3], GREEN, [new Vertex( 1, 0, 0), new Vertex( 1, 0, 0), new Vertex( 1, 0, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 0), new Pt(1, 1)]),
new Triangle([4, 3, 7], GREEN, [new Vertex( 1, 0, 0), new Vertex( 1, 0, 0), new Vertex( 1, 0, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 1), new Pt(0, 1)]),
new Triangle([5, 4, 7], BLUE, [new Vertex( 0, 0, -1), new Vertex( 0, 0, -1), new Vertex( 0, 0, -1)], wood_texture, [new Pt(0, 0), new Pt(1, 0), new Pt(1, 1)]),
new Triangle([5, 7, 6], BLUE, [new Vertex( 0, 0, -1), new Vertex( 0, 0, -1), new Vertex( 0, 0, -1)], wood_texture, [new Pt(0, 0), new Pt(1, 1), new Pt(0, 1)]),
new Triangle([1, 5, 6], YELLOW, [new Vertex(-1, 0, 0), new Vertex(-1, 0, 0), new Vertex(-1, 0, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 0), new Pt(1, 1)]),
new Triangle([1, 6, 2], YELLOW, [new Vertex(-1, 0, 0), new Vertex(-1, 0, 0), new Vertex(-1, 0, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 1), new Pt(0, 1)]),
new Triangle([1, 0, 5], PURPLE, [new Vertex( 0, 1, 0), new Vertex( 0, 1, 0), new Vertex( 0, 1, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 0), new Pt(1, 1)]),
new Triangle([5, 0, 4], PURPLE, [new Vertex( 0, 1, 0), new Vertex( 0, 1, 0), new Vertex( 0, 1, 0)], wood_texture, [new Pt(0, 1), new Pt(1, 1), new Pt(0, 0)]),
new Triangle([2, 6, 7], CYAN, [new Vertex( 0, -1, 0), new Vertex( 0, -1, 0), new Vertex( 0, -1, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 0), new Pt(1, 1)]),
new Triangle([2, 7, 3], CYAN, [new Vertex( 0, -1, 0), new Vertex( 0, -1, 0), new Vertex( 0, -1, 0)], wood_texture, [new Pt(0, 0), new Pt(1, 1), new Pt(0, 1)]),
];
const cube = new Model(vertices, triangles, new Vertex(0, 0, 0), Math.sqrt(3));
// ----------
const instances = [
new Instance(cube, new Vertex(-1.5, 0, 7), Identity4x4, 0.75),
new Instance(cube, new Vertex(1.25, 2.5, 7.5), MakeOYRotationMatrix(195)),
new Instance(cube, new Vertex(1.75, 0, 5), MakeOYRotationMatrix(-30)),
];
const camera = new Camera(new Vertex(-3, 1, 2), MakeOYRotationMatrix(-30));
let s2 = 1.0 / Math.sqrt(2);
camera.clipping_planes = [
new Plane(new Vertex( 0, 0, 1), -1), // Near
new Plane(new Vertex( s2, 0, s2), 0), // Left
new Plane(new Vertex(-s2, 0, s2), 0), // Right
new Plane(new Vertex( 0, -s2, s2), 0), // Top
new Plane(new Vertex( 0, s2, s2), 0), // Bottom
];
const lights = [
new Light(LT_AMBIENT, 0.2),
new Light(LT_DIRECTIONAL, 0.2, Vertex(-1, 0, 1)),
new Light(LT_POINT, 0.6, Vertex(-3, 2, -10))
];
function SetUsePerspectiveCorrectDepth(use_perspective_correct_depth) {
UsePerspectiveCorrectDepth = use_perspective_correct_depth;
Render();
}
function Render() {
ClearAll();
// This lets the browser clear the canvas before blocking to render the scene.
setTimeout(function(){
RenderScene(camera, instances, lights);
UpdateCanvas();
}, 0);
}
if (wood_texture.image.complete) {
Render();
} else {
wood_texture.image.addEventListener("load", Render);
}
</script>
<!--
!!html_title Texture filtering demo - Computer Graphics from scratch
-->
# Texture filtering demo
This work-in-progress demo implements [texture filtering](../14-textures.html#bilinear-filtering). Congratulations for finding it :)
<div class="centered">
<canvas id="canvas" width=600 height=600 style="border: 1px grey solid"></canvas>
<table class="cgfs-demo-controls">
<tr>
<td><b>Depth buffer values</b></td>
<td class="text-left">
<input type="radio" name="perspective-correct-depth" onClick="SetUsePerspectiveCorrectDepth(false)"><label for="pcd-off">Linear (z)</label><br>
<input type="radio" name="perspective-correct-depth" onClick="SetUsePerspectiveCorrectDepth(true)" checked><label for="pcd-on">Perspective-correct (1/z)</label>
</td>
</tr>
</table>
</div>
<script>
// ======================================================================
// Low-level canvas access.
// ======================================================================
var canvas = document.getElementById("canvas");
var canvas_context = canvas.getContext("2d");
var canvas_buffer = canvas_context.getImageData(0, 0, canvas.width, canvas.height);
var canvas_pitch = canvas_buffer.width * 4;
// The PutPixel() function.
var PutPixel = function(x, y, color) {
x = canvas.width/2 + (x | 0);
y = canvas.height/2 - (y | 0) - 1;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return;
}
var offset = 4*x + canvas_pitch*y;
canvas_buffer.data[offset++] = color[0];
canvas_buffer.data[offset++] = color[1];
canvas_buffer.data[offset++] = color[2];
canvas_buffer.data[offset++] = 255; // Alpha = 255 (full opacity)
}
// Displays the contents of the offscreen buffer into the canvas.
var UpdateCanvas = function() {
canvas_context.putImageData(canvas_buffer, 0, 0);
}
// ======================================================================
// Depth buffer.
// ======================================================================
var depth_buffer = Array();
depth_buffer.length = canvas.width * canvas.height;
var UpdateDepthBufferIfCloser = function(x, y, inv_z) {
x = canvas.width/2 + (x | 0);
y = canvas.height/2 - (y | 0) - 1;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return false;
}
var offset = x + canvas.width*y;
if (depth_buffer[offset] == undefined || depth_buffer[offset] < inv_z) {
depth_buffer[offset] = inv_z;
return true;
}
return false;
}
var ClearAll = function() {
canvas.width = canvas.width;
depth_buffer = Array();
depth_buffer.length = canvas.width * canvas.height;
}
// ======================================================================
// Data model.
// ======================================================================
// A Point.
var Pt = function(x, y, h) {
if (!(this instanceof Pt)) { return new Pt(x, y, h); }
this.x = x;
this.y = y;
this.h = h;
}
// A 3D vertex.
var Vertex = function(x, y, z) {
if (!(this instanceof Vertex)) { return new Vertex(x, y, z); }
this.x = x;
this.y = y;
this.z = z;
}
// A 4D vertex (a 3D vertex in homogeneous coordinates).
var Vertex4 = function(arg1, y, z, w) {
if (!(this instanceof Vertex4)) { return new Vertex4(arg1, y, z, w); }
if (arg1 instanceof Vertex) {
this.x = arg1.x;
this.y = arg1.y;
this.z = arg1.z;
this.w = 1;
} else if (arg1 instanceof Vertex4) {
this.x = arg1.x;
this.y = arg1.y;
this.z = arg1.z;
this.w = arg1.w;
} else {
this.x = arg1;
this.y = y;
this.z = z;
this.w = w;
}
}
// A 4x4 matrix.
var Mat4x4 = function(data) {
if (!(this instanceof Mat4x4)) { return new Mat4x4(data); }
this.data = data;
}
var Identity4x4 = Mat4x4([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]);
// A Triangle.
var Triangle = function(indexes, color, normals, texture, uvs) {
if (!(this instanceof Triangle)) { return new Triangle(indexes, color, normals, texture, uvs); }
this.indexes = indexes;
this.color = color;
this.normals = normals;
this.texture = texture;
this.uvs = uvs;
}
// A Model.
var Model = function(vertices, triangles, bounds_center, bounds_radius) {
if (!(this instanceof Model)) { return new Model(vertices, triangles, bounds_center, bounds_radius); }
this.vertices = vertices;
this.triangles = triangles;
this.bounds_center = bounds_center;
this.bounds_radius = bounds_radius;
}
// An Instance.
var Instance = function(model, position, orientation, scale) {
if (!(this instanceof Instance)) { return new Instance(model, position, orientation, scale); }
this.model = model;
this.position = position;
this.orientation = orientation || Identity4x4;
this.scale = scale || 1.0;
this.transform = MultiplyMM4(MakeTranslationMatrix(this.position), MultiplyMM4(this.orientation, MakeScalingMatrix(this.scale)));
}
// The Camera.
var Camera = function(position, orientation) {
if (!(this instanceof Camera)) { return new Camera(position, orientation); }
this.position = position;
this.orientation = orientation;
this.clipping_planes = [];
}
// A Clipping Plane.
var Plane = function(normal, distance) {
if (!(this instanceof Plane)) { return new Plane(normal, distance); }
this.normal = normal;
this.distance = distance;
}
// A Light.
var LT_AMBIENT = 0;
var LT_POINT = 1;
var LT_DIRECTIONAL = 2;
var Light = function(type, intensity, vector) {
if (!(this instanceof Light)) { return new Light(type, intensity, vector); }
this.type = type;
this.intensity = intensity;
this.vector = vector;
}
// A Texture.
var Texture = function(url) {
if (!(this instanceof Texture)) { return new Texture(url); }
this.image = new Image();
this.image.src = url;
var texture = this;
this.image.onload = function() {
texture.iw = texture.image.width;
texture.ih = texture.image.height;
texture.canvas = document.createElement("canvas");
texture.canvas.width = texture.iw;
texture.canvas.height = texture.ih;
var c2d = texture.canvas.getContext("2d");
c2d.drawImage(texture.image, 0, 0, texture.iw, texture.ih);
texture.pixel_data = c2d.getImageData(0, 0, texture.iw, texture.ih);
}
}
GT_POINT = 0,
GT_BILINEAR = 1,
Texture.prototype.getTexel = function(u, v, filter) {
if (filter == GT_POINT) {
var iu = (u*this.iw) | 0;
var iv = (v*this.ih) | 0;
var offset = (iv*this.iw*4 + iu*4);
return [
this.pixel_data.data[offset + 0],
this.pixel_data.data[offset + 1],
this.pixel_data.data[offset + 2]
];
}
if (filter == GT_BILINEAR) {
var u = u*this.iw - 0.5;
var v = v*this.ih - 0.5;
var iu = u | 0;
var iv = v | 0;
var ublend = u - iu;
var vblend = v - iv;
var uother = 1.0 - ublend;
var vother = 1.0 - vblend;
var offset00 = (iv*this.iw*4 + iu*4);
var offset10 = offset00 + 4;
var offset01 = offset00 + this.iw*4;
var offset11 = offset01 + this.iw*4;
var r = (this.pixel_data.data[offset00+0]*uother + this.pixel_data.data[offset10+0]*ublend) * vother +
(this.pixel_data.data[offset01+0]*uother + this.pixel_data.data[offset11+0]*ublend) * vblend;
return [r, r, r];
}
}
// ======================================================================
// Linear algebra and helpers.
// ======================================================================
// Computes k * vec.
var Multiply = function(k, vec) {
return Vertex(k*vec.x, k*vec.y, k*vec.z);
}
// Computes dot product.
var Dot = function(v1, v2) {
return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}
// Computes cross product.
var Cross = function(v1, v2) {
return Vertex(
v1.y*v2.z - v1.z*v2.y,
v1.z*v2.x - v1.x*v2.z,
v1.x*v2.y - v1.y*v2.x);
}
// Computes v1 + v2.
var Add = function(v1, v2) {
return Vertex(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
}
// Computes vector magnitude.
var Magnitude = function(v1) {
return Math.sqrt(Dot(v1, v1));
}
// Makes a transform matrix for a rotation around the OY axis.
var MakeOYRotationMatrix = function(degrees) {
var cos = Math.cos(degrees*Math.PI/180.0);
var sin = Math.sin(degrees*Math.PI/180.0);
return Mat4x4([[cos, 0, -sin, 0],
[ 0, 1, 0, 0],
[sin, 0, cos, 0],
[ 0, 0, 0, 1]])
}
// Makes a transform matrix for a translation.
var MakeTranslationMatrix = function(translation) {
return Mat4x4([[1, 0, 0, translation.x],
[0, 1, 0, translation.y],
[0, 0, 1, translation.z],
[0, 0, 0, 1]]);
}
// Makes a transform matrix for a scaling.
var MakeScalingMatrix = function(scale) {
return Mat4x4([[scale, 0, 0, 0],
[ 0, scale, 0, 0],
[ 0, 0, scale, 0],
[ 0, 0, 0, 1]]);
}
// Multiplies a 4x4 matrix and a 4D vector.
var MultiplyMV = function(mat4x4, vec4) {
var result = [0, 0, 0, 0];
var vec = [vec4.x, vec4.y, vec4.z, vec4.w];
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
result[i] += mat4x4.data[i][j]*vec[j];
}
}
return Vertex4(result[0], result[1], result[2], result[3]);
}
// Multiplies two 4x4 matrices.
var MultiplyMM4 = function(matA, matB) {
var result = Mat4x4([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]);
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
for (var k = 0; k < 4; k++) {
result.data[i][j] += matA.data[i][k]*matB.data[k][j];
}
}
}
return result;
}
// Transposes a 4x4 matrix.
var Transposed = function(mat) {
var result = Mat4x4([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]);
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
result.data[i][j] = mat.data[j][i];
}
}
return result;
}
var Clamp = function(value) {
if (value < 0) { return 0; }
if (value > 255) { return 255; }
return value;
}
// Adds two colors.
var AddColor = function(c1, c2) {
return [Clamp(c1[0] + c2[0]), Clamp(c1[1] + c2[1]), Clamp(c1[2] + c2[2])];
}
var MultiplyColor = function(color, k) {
return [Clamp(color[0]*k), Clamp(color[1]*k), Clamp(color[2]*k)];
}
// ======================================================================
// Rasterization code.
// ======================================================================
// Scene setup.
var viewport_size = 1;
var projection_plane_z = 1;
var Interpolate = function(i0, d0, i1, d1) {
if (i0 == i1) {
return [d0];
}
var values = [];
var a = (d1 - d0) / (i1 - i0);
var d = d0;
for (var i = i0; i <= i1; i++) {
values.push(d);
d += a;
}
return values;
}
var DrawLine = function(p0, p1, color) {
var dx = p1.x - p0.x, dy = p1.y - p0.y;
if (Math.abs(dx) > Math.abs(dy)) {
// The line is horizontal-ish. Make sure it's left to right.
if (dx < 0) { var swap = p0; p0 = p1; p1 = swap; }
// Compute the Y values and draw.
var ys = Interpolate(p0.x, p0.y, p1.x, p1.y);
for (var x = p0.x; x <= p1.x; x++) {
PutPixel(x, ys[(x - p0.x) | 0], color);
}
} else {
// The line is verical-ish. Make sure it's bottom to top.
if (dy < 0) { var swap = p0; p0 = p1; p1 = swap; }
// Compute the X values and draw.
var xs = Interpolate(p0.y, p0.x, p1.y, p1.x);
for (var y = p0.y; y <= p1.y; y++) {
PutPixel(xs[(y - p0.y) | 0], y, color);
}
}
}
var DrawWireframeTriangle = function(p0, p1, p2, color) {
DrawLine(p0, p1, color);
DrawLine(p1, p2, color);
DrawLine(p0, p2, color);
}
// Converts 2D viewport coordinates to 2D canvas coordinates.
var ViewportToCanvas = function(p2d) {
return Pt((p2d.x * canvas.width / viewport_size) | 0,
(p2d.y * canvas.height / viewport_size) | 0);
}
// Converts 2D canvas coordinates to 2D viewport coordinates.
var CanvasToViewport = function(p2d) {
return Pt(
(p2d.x * viewport_size / canvas.width),
(p2d.y * viewport_size / canvas.height)
);
}
var ProjectVertex = function(v) {
return ViewportToCanvas(Pt(v.x * projection_plane_z / v.z,
v.y * projection_plane_z / v.z));
}
var UnProjectVertex = function(x, y, inv_z) {
var oz = 1.0 / inv_z;
var ux = x*oz / projection_plane_z;
var uy = y*oz / projection_plane_z;
var p2d = CanvasToViewport(Pt(ux, uy));
return Vertex(p2d.x, p2d.y, oz);
}
// Sort the points from bottom to top.
// Technically, sort the indexes to the vertex indexes in the triangle from bottom to top.
var SortedVertexIndexes = function(vertex_indexes, projected) {
indexes = [0, 1, 2];
if (projected[vertex_indexes[indexes[1]]].y < projected[vertex_indexes[indexes[0]]].y) { var swap = indexes[0]; indexes[0] = indexes[1]; indexes[1] = swap; }
if (projected[vertex_indexes[indexes[2]]].y < projected[vertex_indexes[indexes[0]]].y) { var swap = indexes[0]; indexes[0] = indexes[2]; indexes[2] = swap; }
if (projected[vertex_indexes[indexes[2]]].y < projected[vertex_indexes[indexes[1]]].y) { var swap = indexes[1]; indexes[1] = indexes[2]; indexes[2] = swap; }
return indexes;
}
var ComputeTriangleNormal = function(v0, v1, v2) {
var v0v1 = Add(v1, Multiply(-1, v0));
var v0v2 = Add(v2, Multiply(-1, v0));
return Cross(v0v1, v0v2);
}
var ComputeIllumination = function(vertex, normal, camera, lights) {
var illumination = 0;
for (var l = 0; l < lights.length; l++) {
var light = lights[l];
if (light.type == LT_AMBIENT) {
illumination += light.intensity;
continue;
}
var vl;
if (light.type == LT_DIRECTIONAL) {
var cameraMatrix = Transposed(camera.orientation);
var rotated_light = MultiplyMV(cameraMatrix, Vertex4(light.vector));
vl = rotated_light;
} else if (light.type == LT_POINT) {
var cameraMatrix = MultiplyMM4(Transposed(camera.orientation), MakeTranslationMatrix(Multiply(-1, camera.position)));
var transformed_light = MultiplyMV(cameraMatrix, Vertex4(light.vector));
vl = Add(transformed_light, Multiply(-1, vertex)); // light.vector - vertex
}
// Diffuse component.
var cos_alpha = Dot(vl, normal) / (Magnitude(vl) * Magnitude(normal));
if (cos_alpha > 0) {
illumination += cos_alpha * light.intensity;
}
// Specular component.
var reflected = Add(Multiply(2*Dot(normal, vl), normal), Multiply(-1, vl));
var view = Add(camera.position, Multiply(-1, vertex));
var cos_beta = Dot(reflected, view) / (Magnitude(reflected) * Magnitude(view));
if (cos_beta > 0) {
var specular = 50;
illumination += Math.pow(cos_beta, specular) * light.intensity;
}
}
return illumination;
}
var SM_FLAT = 0;
var SM_GOURAUD = 1;
var SM_PHONG = 2;
var ShadingModel = SM_PHONG;
var UseVertexNormals = true;
var UsePerspectiveCorrectDepth = true;
var EdgeInterpolate = function(y0, v0, y1, v1, y2, v2) {
var v01 = Interpolate(y0, v0, y1, v1);
var v12 = Interpolate(y1, v1, y2, v2);
var v02 = Interpolate(y0, v0, y2, v2);
v01.pop();
var v012 = v01.concat(v12);
return [v02, v012];
}
var RenderTriangle = function(triangle, vertices, projected, camera, lights, orientation) {
// Compute triangle normal. Use the unsorted vertices, otherwise the winding of the points may change.
var normal = ComputeTriangleNormal(vertices[triangle.indexes[0]], vertices[triangle.indexes[1]], vertices[triangle.indexes[2]]);
// Backface culling.
var vertex_to_camera = Multiply(-1, vertices[triangle.indexes[0]]); // Should be Subtract(camera.position, vertices[triangle.indexes[0]])
if (Dot(vertex_to_camera, normal) <= 0) {
return;
}
// Sort by projected point Y.
var indexes = SortedVertexIndexes(triangle.indexes, projected);
var [i0, i1, i2] = indexes;
var [v0, v1, v2] = [ vertices[triangle.indexes[i0]], vertices[triangle.indexes[i1]], vertices[triangle.indexes[i2]] ];
// Get attribute values (X, 1/Z, U/Z, U/Z) at the vertices.
var [p0, p1, p2] = [ projected[triangle.indexes[i0]], projected[triangle.indexes[i1]], projected[triangle.indexes[i2]] ];
// Compute attribute values at the edges.
var [x02, x012] = EdgeInterpolate(p0.y, p0.x, p1.y, p1.x, p2.y, p2.x);
var [iz02, iz012] = EdgeInterpolate(p0.y, 1.0/v0.z, p1.y, 1.0/v1.z, p2.y, 1.0/v2.z);
if (triangle.texture) {
if (UsePerspectiveCorrectDepth) {
var [uz02, uz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].x / v0.z,
p1.y, triangle.uvs[i1].x / v1.z,
p2.y, triangle.uvs[i2].x / v2.z);
var [vz02, vz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].y / v0.z,
p1.y, triangle.uvs[i1].y / v1.z,
p2.y, triangle.uvs[i2].y / v2.z);
} else {
var [uz02, uz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].x,
p1.y, triangle.uvs[i1].x,
p2.y, triangle.uvs[i2].x);
var [vz02, vz012] = EdgeInterpolate(p0.y, triangle.uvs[i0].y,
p1.y, triangle.uvs[i1].y,
p2.y, triangle.uvs[i2].y);
}
}
if (UseVertexNormals) {
var transform = MultiplyMM4(Transposed(camera.orientation), orientation);
var normal0 = MultiplyMV(transform, Vertex4(triangle.normals[i0]));
var normal1 = MultiplyMV(transform, Vertex4(triangle.normals[i1]));
var normal2 = MultiplyMV(transform, Vertex4(triangle.normals[i2]));
} else {
var normal0 = normal;
var normal1 = normal;
var normal2 = normal;
}
if (ShadingModel == SM_FLAT) {
// Flat shading: compute lighting for the entire triangle.
var center = Vertex((v0.x + v1.x + v2.x)/3.0, (v0.y + v1.y + v2.y)/3.0, (v0.z + v1.z + v2.z)/3.0);
var intensity = ComputeIllumination(center, normal0, camera, lights);
} else if (ShadingModel == SM_GOURAUD) {
// Gouraud shading: compute lighting at the vertices, and interpolate.
var i0 = ComputeIllumination(v0, normal0, camera, lights);
var i1 = ComputeIllumination(v1, normal1, camera, lights);
var i2 = ComputeIllumination(v2, normal2, camera, lights);
var [i02, i012] = EdgeInterpolate(p0.y, i0, p1.y, i1, p2.y, i2);
} else if (ShadingModel == SM_PHONG) {
// Phong shading: interpolate normal vectors.
var [nx02, nx012] = EdgeInterpolate(p0.y, normal0.x, p1.y, normal1.x, p2.y, normal2.x);
var [ny02, ny012] = EdgeInterpolate(p0.y, normal0.y, p1.y, normal1.y, p2.y, normal2.y);
var [nz02, nz012] = EdgeInterpolate(p0.y, normal0.z, p1.y, normal1.z, p2.y, normal2.z);
}
// Determine which is left and which is right.
var m = (x02.length/2) | 0;
if (x02[m] < x012[m]) {
var [x_left, x_right] = [x02, x012];
var [iz_left, iz_right] = [iz02, iz012];
var [i_left, i_right] = [i02, i012];
var [nx_left, nx_right] = [nx02, nx012];
var [ny_left, ny_right] = [ny02, ny012];
var [nz_left, nz_right] = [nz02, nz012];
var [uz_left, uz_right] = [uz02, uz012];
var [vz_left, vz_right] = [vz02, vz012];
} else {
var [x_left, x_right] = [x012, x02];
var [iz_left, iz_right] = [iz012, iz02];
var [i_left, i_right] = [i012, i02];
var [nx_left, nx_right] = [nx012, nx02];
var [ny_left, ny_right] = [ny012, ny02];
var [nz_left, nz_right] = [nz012, nz02];
var [uz_left, uz_right] = [uz012, uz02];
var [vz_left, vz_right] = [vz012, vz02];
}
// Draw horizontal segments.
for (var y = p0.y; y <= p2.y; y++) {
var [xl, xr] = [x_left[y - p0.y] | 0, x_right[y - p0.y] | 0];
// Interpolate attributes for this scanline.
var [zl, zr] = [iz_left[y - p0.y], iz_right[y - p0.y]];
var zscan = Interpolate(xl, zl, xr, zr);
if (ShadingModel == SM_GOURAUD) {
var [il, ir] = [i_left[y - p0.y], i_right[y - p0.y]];
var iscan = Interpolate(xl, il, xr, ir);
} else if (ShadingModel == SM_PHONG) {
var [nxl, nxr] = [nx_left[y - p0.y], nx_right[y - p0.y]];
var [nyl, nyr] = [ny_left[y - p0.y], ny_right[y - p0.y]];
var [nzl, nzr] = [nz_left[y - p0.y], nz_right[y - p0.y]];
var nxscan = Interpolate(xl, nxl, xr, nxr);
var nyscan = Interpolate(xl, nyl, xr, nyr);
var nzscan = Interpolate(xl, nzl, xr, nzr);
}
if (triangle.texture) {
var uzscan = Interpolate(xl, uz_left[y - p0.y], xr, uz_right[y - p0.y]);
var vzscan = Interpolate(xl, vz_left[y - p0.y], xr, vz_right[y - p0.y]);
}
for (var x = xl; x <= xr; x++) {
var inv_z = zscan[x - xl];
if (UpdateDepthBufferIfCloser(x, y, inv_z)) {
if (ShadingModel == SM_FLAT) {
// Just use the per-triangle intensity.
} else if (ShadingModel == SM_GOURAUD) {
intensity = iscan[x-xl];
} else if (ShadingModel == SM_PHONG) {
var vertex = UnProjectVertex(x, y, inv_z);
var normal = Vertex(nxscan[x - xl], nyscan[x - xl], nzscan[x - xl]);
var intensity = ComputeIllumination(vertex, normal, camera, lights);
}
if (triangle.texture) {
if (UsePerspectiveCorrectDepth) {
var u = uzscan[x - xl] / zscan[x - xl];
var v = vzscan[x - xl] / zscan[x - xl];
} else {
var u = uzscan[x - xl];
var v = vzscan[x - xl];
}
color = triangle.texture.getTexel(u, v, GT_BILINEAR);
} else {
color = triangle.color;
}
PutPixel(x, y, MultiplyColor(color, intensity));
}
}
}
}
// Clips a triangle against a plane. Adds output to triangles and vertices.
var ClipTriangle = function(triangle, plane, triangles, vertices) {
var v0 = vertices[triangle.indexes[0]];
var v1 = vertices[triangle.indexes[1]];
var v2 = vertices[triangle.indexes[2]];
var in0 = Dot(plane.normal, v0) + plane.distance > 0;
var in1 = Dot(plane.normal, v1) + plane.distance > 0;
var in2 = Dot(plane.normal, v2) + plane.distance > 0;
var in_count = in0 + in1 + in2;
if (in_count == 0) {
// Nothing to do - the triangle is fully clipped out.
} else if (in_count == 3) {
// The triangle is fully in front of the plane.
triangles.push(triangle);
} else if (in_count == 1) {
// The triangle has one vertex in. Output is one clipped triangle.
} else if (in_count == 2) {
// The triangle has two vertices in. Output is two clipped triangles.
}
}
var TransformAndClip = function(clipping_planes, model, scale, transform) {
// Transform the bounding sphere, and attempt early discard.
center = MultiplyMV(transform, Vertex4(model.bounds_center));
var radius = model.bounds_radius*scale;
for (var p = 0; p < clipping_planes.length; p++) {
var distance = Dot(clipping_planes[p].normal, center) + clipping_planes[p].distance;
if (distance < -radius) {
return null;
}
}
// Apply modelview transform.
var vertices = [];
for (var i = 0; i < model.vertices.length; i++) {
vertices.push(MultiplyMV(transform, Vertex4(model.vertices[i])));
}
// Clip the entire model against each successive plane.
var triangles = model.triangles.slice();
for (var p = 0; p < clipping_planes.length; p++) {
new_triangles = []
for (var i = 0; i < triangles.length; i++) {
ClipTriangle(triangles[i], clipping_planes[p], new_triangles, vertices);
}
triangles = new_triangles;
}
return Model(vertices, triangles, center, model.bounds_radius);
}
var RenderModel = function(model, camera, lights, orientation) {
var projected = [];
for (var i = 0; i < model.vertices.length; i++) {
projected.push(ProjectVertex(Vertex4(model.vertices[i])));
}
for (var i = 0; i < model.triangles.length; i++) {
RenderTriangle(model.triangles[i], model.vertices, projected, camera, lights, orientation);
}
}
var RenderScene = function(camera, instances, lights) {
var cameraMatrix = MultiplyMM4(Transposed(camera.orientation), MakeTranslationMatrix(Multiply(-1, camera.position)));
for (var i = 0; i < instances.length; i++) {
var transform = MultiplyMM4(cameraMatrix, instances[i].transform);
var clipped = TransformAndClip(camera.clipping_planes, instances[i].model, instances[i].scale, transform);
if (clipped != null) {
RenderModel(clipped, camera, lights, instances[i].orientation);
}
}
}
// ----- Sphere model generator -----
var GenerateSphere = function(divs, color) {
var vertices = [];
var triangles = [];
var delta_angle = 2.0*Math.PI / divs;
// Generate vertices and normals.
for (var d = 0; d < divs + 1; d++) {
var y = (2.0 / divs) * (d - divs/2);
var radius = Math.sqrt(1.0 - y*y);
for (var i = 0; i < divs; i++) {
var vertex = Vertex(radius*Math.cos(i*delta_angle), y, radius*Math.sin(i*delta_angle));
vertices.push(vertex);
}
}
// Generate triangles.
for (var d = 0; d < divs; d++) {
for (var i = 0; i < divs - 1; i++) {
var i0 = d*divs + i;
triangles.push(Triangle([i0, i0+divs+1, i0+1], color, [vertices[i0], vertices[i0+divs+1], vertices[i0+1]]));
triangles.push(Triangle([i0, i0+divs, i0+divs+1], color, [vertices[i0], vertices[i0+divs], vertices[i0+divs+1]]));
}
}
return Model(vertices, triangles, Vertex(0, 0, 0), 1.0);
}
// ----- Cube model -----
var vertices = [
Vertex(1, 1, 1),
Vertex(-1, 1, 1),
Vertex(-1, -1, 1),
Vertex(1, -1, 1),
Vertex(1, 1, -1),
Vertex(-1, 1, -1),
Vertex(-1, -1, -1),
Vertex(1, -1, -1)
];
var RED = [255, 0, 0];
var GREEN = [0, 255, 0];
var BLUE = [0, 0, 255];
var YELLOW = [255, 255, 0];
var PURPLE = [255, 0, 255];
var CYAN = [0, 255, 255];
var checkerboard_texture = Texture("./checkerboard.png");
var triangles = [
Triangle([0, 1, 2], RED, [Vertex( 0, 0, 1), Vertex( 0, 0, 1), Vertex( 0, 0, 1)], checkerboard_texture, [Pt(0, 0), Pt(1, 0), Pt(1, 1)]),
Triangle([0, 2, 3], RED, [Vertex( 0, 0, 1), Vertex( 0, 0, 1), Vertex( 0, 0, 1)], checkerboard_texture, [Pt(0, 0), Pt(1, 1), Pt(0, 1)]),
Triangle([4, 0, 3], GREEN, [Vertex( 1, 0, 0), Vertex( 1, 0, 0), Vertex( 1, 0, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 0), Pt(1, 1)]),
Triangle([4, 3, 7], GREEN, [Vertex( 1, 0, 0), Vertex( 1, 0, 0), Vertex( 1, 0, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 1), Pt(0, 1)]),
Triangle([5, 4, 7], BLUE, [Vertex( 0, 0, -1), Vertex( 0, 0, -1), Vertex( 0, 0, -1)], checkerboard_texture, [Pt(0, 0), Pt(1, 0), Pt(1, 1)]),
Triangle([5, 7, 6], BLUE, [Vertex( 0, 0, -1), Vertex( 0, 0, -1), Vertex( 0, 0, -1)], checkerboard_texture, [Pt(0, 0), Pt(1, 1), Pt(0, 1)]),
Triangle([1, 5, 6], YELLOW, [Vertex(-1, 0, 0), Vertex(-1, 0, 0), Vertex(-1, 0, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 0), Pt(1, 1)]),
Triangle([1, 6, 2], YELLOW, [Vertex(-1, 0, 0), Vertex(-1, 0, 0), Vertex(-1, 0, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 1), Pt(0, 1)]),
Triangle([1, 0, 5], PURPLE, [Vertex( 0, 1, 0), Vertex( 0, 1, 0), Vertex( 0, 1, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 0), Pt(1, 1)]),
Triangle([5, 0, 4], PURPLE, [Vertex( 0, 1, 0), Vertex( 0, 1, 0), Vertex( 0, 1, 0)], checkerboard_texture, [Pt(0, 1), Pt(1, 1), Pt(0, 0)]),
Triangle([2, 6, 7], CYAN, [Vertex( 0, -1, 0), Vertex( 0, -1, 0), Vertex( 0, -1, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 0), Pt(1, 1)]),
Triangle([2, 7, 3], CYAN, [Vertex( 0, -1, 0), Vertex( 0, -1, 0), Vertex( 0, -1, 0)], checkerboard_texture, [Pt(0, 0), Pt(1, 1), Pt(0, 1)]),
];
var cube = Model(vertices, triangles, Vertex(0, 0, 0), Math.sqrt(3));
// ----------
var sphere = GenerateSphere(15, GREEN);
var instances = [Instance(cube, Vertex(-1, 0, 3), MakeOYRotationMatrix(-15)),
Instance(cube, Vertex(5.5, 5, 20), MakeOYRotationMatrix(45)),
];
var camera = Camera(Vertex(0, 0, 0), MakeOYRotationMatrix(0));
var s2 = 1.0 / Math.sqrt(2);
camera.clipping_planes = [
Plane(Vertex( 0, 0, 1), -1), // Near
Plane(Vertex( s2, 0, s2), 0), // Left
Plane(Vertex(-s2, 0, s2), 0), // Right
Plane(Vertex( 0, -s2, s2), 0), // Top
Plane(Vertex( 0, s2, s2), 0), // Bottom
];
lights = [
Light(LT_AMBIENT, 0.2),
Light(LT_DIRECTIONAL, 0.2, Vertex(-1, 0, 1)),
Light(LT_POINT, 0.6, Vertex(-3, 2, -10))
];
var SetUsePerspectiveCorrectDepth = function(use_perspective_correct_depth) {
UsePerspectiveCorrectDepth = use_perspective_correct_depth;
Render();
}
function Render() {
ClearAll();
// This lets the browser clear the canvas before blocking to render the scene.
setTimeout(function(){
RenderScene(camera, instances, lights);
UpdateCanvas();
}, 0);
}
if (checkerboard_texture.image.complete) {
Render();
} else {
checkerboard_texture.image.addEventListener("load", Render);
}
</script>