November 17, 2020

Coding5 - 3D Graphics From Scratch

In this unit, we will try to use simple 2D drawing methods to create basic 3D graphics, which can also help you better understand, how the 3D graphics system creates perspective.


1.Beginning

Let’s start with a video:

This is the first 3D computer graphics applied to movies, created by artist Larry Cuba, he and John Whitney are both very important people in the field of experimental film and computer graphics.


2.2D Perspective Projection

In order to draw a 3D scene, we need to create depth. So we need to understand the idea of perspective and projection.
We can create a simple perspective projection through code:

First, we create a bunch of random coordinates. They are many 3D vectors. We also call them vertices. When we create them in 3D, we assign x, y and z coordinates to each vertex, and the z coordinate is the vertex. Provides depth.

We start with a bunch of 3D points, x,y,z; then we use z to work out new positions for x and y. So we are going to scale the x and y coordinates of each vertex based on a value we derive from the z coordinate of each vertex.

We get this new value by adding the Field of View to the z coordinate, and dividing the FOV by the result.


3. Field of View

We could just use the z coordinate to directly scale the x and y values, as well as the size of the point we will draw. However, this really doesn’t work very well, and don’t simulate real world experience as well as we might like.

In order to make this a bit better, we generate an intermediate value called a Field of View. Field of view is basically a term that means ‘what the camera can see’, but it also describes the extent to which we understand size and distance in a visual scene.

One simple way to emulate field of view mathematically to scale a vertex is to generate a value, add the z coordinate of the vertex, and divide by the starting value.

This produces a factor that represents the ratio of the field of view to the vertex z position. There are other ways of doing this which are slightly better, but we will come on to those later.

scale = for / (fov + z3d);

x = x * scale;

y = y * scale;

z = z;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
var canvas = document.querySelector("canvas");
var width = window.innerWidth;
var height = window.innerHeight;
var context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;

var fov = 200;
// This array is used to create an individual 3D vertex, represented by a 3D vector xyz
var point = [];
// This is where we stash all the vertices
var points = [];
// This is temp storage for when we draw each 3D vertex
var point3d = [];

var HALF_WIDTH = width / 2;
var HALF_HEIGHT = height / 2;

var numPoints = 1000;
// This is temp storage for all the individual vertex coordinates in the vector
var x3d = 0;
var y3d = 0;
var z3d = 0;

// This generates a static random starfield.
for (var i = 0; i < numPoints; i++) {
// randomly produce a number between 0 and 1
// for each x, y and z coordinate
// scale it (using width or height)
// then centre it using width or hight
point = [(Math.random() * width / 2) - width / 4, (Math.random() * height / 2) - height / 4, (Math.random() * width / 2) - width / 4];
// add the vertex
points.push(point);
}

// Now we draw the static random starfield
function draw() {

// This just clears the screen and paints it black
context.fillStyle = "rgb(0,0,0)";
context.fillRect(0, 0, width, height);
// This loop takes a bunch of 3D vertices and draws them using a 2D perspective projection
for (var i = 0; i < numPoints; i++) {

// Get a vertex
point3d = points[i];

// // Get the z coordinate - this is the depth
z3d = point3d[2];

// if the z coordinate for this vertex is beyond Fielf of View (FOV),
// Add half the width to move it back. This makes an endless starfield.
// feel free to disable this if you want...
if (z3d < -fov) z3d += HALF_WIDTH;

// replace the original z position with this new z position.
point3d[2] = z3d;

// Now get all the coordinates in to separate values to make them easier to wrangle
x3d = point3d[0];
y3d = point3d[1];
z3d = point3d[2];

// Decide on the size of the point by taking the FOV and dividing it by the FOV + the z pos
var scale = fov / (fov + z3d);
// Now create the 2D perspective projection.
// create a 2D x and y position by multiplying the x and y coordinates by the scale
// Add half the width and height to translate the coordinates to the origin.
var x2d = (x3d * scale) + HALF_WIDTH;
var y2d = (y3d * scale) + HALF_HEIGHT;

// Draw a square of size 'scale', in position x2d, y2d.

context.fillStyle = "rgb(255,255,255)";
context.fillRect(x2d, y2d,scale,scale);

//That really is it...

}
requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

4.Unit Vector

The unit vector gives you a normalised vector with a length of 1. It doesn’t tell you how far away things are; instead, it tells you what direction you need to go in to get to them. It provides this information as a cartesian coordinate.

How can we calculate the unit vector?

Get the difference v between points a and b; this is just an element wise subtraction.

Then, divide each element in v by the magnitude of v.(Remember, v is the difference in three dimensions)

The magnitude of v is the same as the distance between a and b.


5.Vertices Faces and Primitives

Vertices are collections of 3D position vectors; a 3D object usually has a number of vertices. We use these vertices to describe the faces of a 3D object, often do this by using the vertices to specify triangles; we can also use ‘quads’, which we can think of as planes.

Planes are very basic 2D shapes in 3D space; planes have four vertices, they are usually square, but don’t have to be. Planes have 1 face, or 2faces if you want to see both side. When you do that ,it is no longer 2D. It becomes an infinitely thin 3D object. In openGL, we need to specify that a collection of vertices is a face. We also need to tell the renderer how to render that face.

3D shapes are sometimes called Primitives. Some easy primitives to generate are the Platonic solids, platonic solids are interesting because each face of a platonic solid is the same size and shape.


6.Scaling, Rotating and Translating with Matrices

We can use matrix transformations to do this all for us.

This is what openGL does behind the scenes. It has a bunch of matrix operations that you can set up with a single command; this handles the whole pipeline.

Order of Transformations

In openGL, depth is usually represented along the z axis; in order to render 3D objects properly in openGL, you need to use something called a “Depth Buffer”, or z buffer. The z buffer is disabled by default, which means when you start coding in openGL, you can not draw depth properly.

The z buffer knows what the current z value is for each pixel on the screen; if the z value in a 3D object is greater than the z value in the buffer for that pixel, the z buffer draws the pixel. To enable the depth buffer in openFrameworks, you would do this:

ofEnableDepthTest();

7.Works

Below I will show some drawing examples:

3D Knot Exploration by Nan


Superformula by Froyo


Butterfly curve by SSQ

I also share some other formulas, which can replace my code to explore more interesting things!


蝶形曲線 butterfly curve

x= Math.exp(Math.cos(j))-2Math.cos(4j)-Math.pow(Math.sin(j/12),5)*Math.sin(j)

y= Math.exp(Math.cos(j))-2Math.cos(4j)-Math.pow(Math.sin(j/12),5)*Math.cos(j)

*Final:**var point = [(Math.exp(Math.cos(spacingj))-2Math.cos(spacing4j)-Math.pow(Math.sin(spacingj/12),5))Math.sin(spacingj) * s,(Math.exp(Math.cos(spacingj))-2Math.cos(spacing4j)-Math.pow(Math.sin(spacingj/12),5))Math.cos(spacingj) s,z];

**Wiki:**https://en.wikipedia.org/wiki/Butterfly_curve_(transcendental)


紡錘線 dumbbell Curve

x=a*j

y=a*Math.pow(j,2)*Math.sqrt(1-Math.pow(j,2))


三尖瓣線 tricuspoid

x= 2aMath.cos(j)+aMath.cos(2aj);

y= 2aMath.sin(j)-aMath.sin(2aj);

Final: var point = [(2Math.cos(spacingj)+Math.cos(2spacingj))* s,(2Math.sin(spacingj)-Math.sin(2spacingj))* s,z];

**Wiki:**https://en.wikipedia.org/wiki/Deltoid_curve


擺線 epicycloid

x= 2Math.cos(j) - Math.cos(2j);

y= 2Math.sin(j) - Math.sin(2j);

*Final:**var point = [(2Math.cos(j) - Math.cos(2j)) s,(2Math.sin(j) - Math.sin(2j))* s,z];

// when a=2b, nephroid

x= 3Math.cos(j) - Math.cos(3j);

y= 3Math.sin(j) - Math.sin(3j);

*Final:**var point = [(3Math.cos(j) - Math.cos(3j)) s,(3Math.sin(j) - Math.sin(3j))* s,z];

歸納與總結:

var side= 5; // Q為任意的大於3的數都可以生成角度。

var point = [(sideMath.cos(j) + Math.cos(sidej))* s,(sideMath.sin(j) - Math.sin(sidej))* s,z];


Web Sketchpad

x= Math.cos(14j) +Math.cos(14j)/2+Math.sin(3*j)/3

y= Math.sin(14j) +Math.sin(14j)/2+Math.cos(3*j)/3

*Final:**var point = [(Math.cos(20j) +Math.cos(20j)/2+Math.sin(1j)/3)* s,(Math.sin(20j) +Math.sin(20j)/2+Math.cos(1j)/3) s,z];

(我也不知道為啥沒有生成游泳圈的樣子)= =


x=((1+Math.pow(Math.sin(j),2))*Math.cos(j))

y=((Math.pow(Math.sin(j),2)-1)*Math.sin(j))

// 添加項數:Math.round(Math.random()) 隨機抽取掉50%的點,投射到(0, 0, z)上

Curves Defined by Parametric Equations


Other example from Wiki

x= Math.cos(80*j) -Math.cos(j)*Math.sin(j)

y= 2Math.sin(j)-Math.sin(80j)

x= Math.cos(9j)-Math.pow(Math.cos(100j),3);

y= Math.sin(200j)-Math.pow(Math(9j),4);


雙曲八面體 Hyperbolic Octahedron

x= Math.pow(Math.cos(j/2)*Math.cos(j),3);

y= Math.pow(Math.sin(j/2)*Math.cos(j),3);

z= Math.pow(Math.sin(j),3)

example from wolfram


心形線 heart Curve

x= Math.cos(2j)+Math.cos(6j)/2+Math.sin(4*j)/3;

y= Math.sin(2j)+Math.sin(6j)/2+Math.cos(4*j)/3;

About this Post

This post is written by Siqi Shu, licensed under CC BY-NC 4.0.