I am currently writing a small library for 3D wireframes, because I have an idea for a 3D wireframe game, and I couldn’t find any libraries for 3D wireframes in p5.js. Right now everything, except rotation, works perfectly fine. Adding translation and scaling would be trivial.
So here’s my problem: When I am rotating a cube (though I suppose that the problem is general for any 3D shape), the library appears to:
- First project the 3D shape onto the screen as a 2D projection.
- Then, rotate that 2D projection as a 3D shape.
However, this is what I want the library to do:
- Rotate the 3D shape as a 3D shape.
- Project that 3D shape onto a 2D screen.
Just in case you wanted to see how the library’s rotation looks: Upload files for free - https___preview.p5js.org_subtra3t_present_Zyv1jp5tg - Google Chrome 2021-09-29 10-31-06.mp4 - ufile.io
Note: Most code on this thread (especially the
rotateThreeDPoint
function) is unoptimized and could probably be written to be much faster… However, surprisingly, most code (includingrotateThreeDPoint
!) is fast, and generally appears to be not laggy. So I please ask you to not suggest me optimizations, since this is a prototype (pun unintended), and I’ll focus on optimizations later, or when the library appears to be genuinely slow.Another note: This library uses something called Perspective 3D to project 3D shapes onto a 2D screen. This means that farther objects (meaning with a higher
z
value are rendered smaller (meaning closer to the origin). There are probably better ways to project 3D shapes onto 2D screens, but this is the only way that seemed intuitive to me.Yet another note: This library interprets coordinates to be relative to the origin, the center of the screen.
You’re probably tired of these notes: The library uses complex multiplication to rotate 2D points, and rotates components of a 3D point to achieve 3D rotation.
My files:
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<script src="cartesian.js"></script>
<script src="rotation.js"></script>
<script src="threeD.js"></script>
<script src="threeDShape.js"></script>
<script src="threeDShapes.js"></script>
<script src="sketch.js"></script>
</body>
</html>
cartesian.js
:
function fromCartesian(x, y, width, height) {
return [
x + (width / 2),
y + (height / 2)
];
}
rotation.js
:
function multiplyComplexNumbers(c1, c2) {
return {
"real" : (c1.real * c2.real) - (c1.imaginary * c2.imaginary),
"imaginary" : (c1.real * c2.imaginary) + (c1.imaginary * c2.real)
};
}
function rotateTwoDPoint(x, y, turn) {
const _PI = Math.PI || 3.141592653589793;
return [
multiplyComplexNumbers(
{
"real" : x,
"imaginary" : y
},
{
"real" : Math.cos(turn * 2 * _PI),
"imaginary" : Math.sin(turn * 2 * _PI)
}
).real,
multiplyComplexNumbers(
{
"real" : x,
"imaginary" : y
},
{
"real" : Math.cos(turn * 2 * _PI),
"imaginary" : Math.sin(turn * 2 * _PI)
}
).imaginary
];
}
function rotateThreeDPointX(x, y, z, xt) {
return [
x,
rotateTwoDPoint(y, z, xt)[0],
rotateTwoDPoint(y, z, xt)[1]
];
}
function rotateThreeDPointY(x, y, z, yt) {
return [
rotateTwoDPoint(x, z, yt)[0],
y,
rotateTwoDPoint(x, z, yt)[1]
];
}
function rotateThreeDPointZ(x, y, z, zt) {
return [
rotateTwoDPoint(x, z, zt)[0],
rotateTwoDPoint(x, y, zt)[1],
z
];
}
function rotateThreeDPoint(x, y, z, xt, yt, zt) {
return rotateThreeDPointZ(rotateThreeDPointY(rotateThreeDPointX(x, y, z, xt)[0], rotateThreeDPointX(x, y, z, xt)[1], rotateThreeDPointX(x, y, z, xt)[2], yt)[0], rotateThreeDPointY(rotateThreeDPointX(x, y, z, xt)[0], rotateThreeDPointX(x, y, z, xt)[1], rotateThreeDPointX(x, y, z, xt)[2], yt)[1], rotateThreeDPointY(rotateThreeDPointX(x, y, z, xt)[0], rotateThreeDPointX(x, y, z, xt)[1], rotateThreeDPointX(x, y, z, xt)[2], yt)[2], zt);
}
function rotateThreeDShape(shape, xt, yt, zt) {
return shape.map(points => {
if (typeof points === "object") {
return points.map(point_ => rotateThreeDPoint(point_[0], point_[1], point_[2], xt, yt, zt));
}
return rotateThreeDPoint(points[0], points[1], points[2], xt, yt, zt);
})
}
threeD.js
function threeDPoint(x, y, z, fov) {
return [
x / z * fov,
y / z * fov
];
}
function threeDLine(x1, y1, z1, x2, y2, z2, fov, width, height, pointFunction, lineFunction) {
lineFunction(
fromCartesian(
pointFunction(x1, y1, z1, fov)[0],
pointFunction(x1, y1, z1, fov)[1],
width,
height
)[0],
fromCartesian(
pointFunction(x1, y1, z1, fov)[0],
pointFunction(x1, y1, z1, fov)[1],
width,
height
)[1],
fromCartesian(
pointFunction(x2, y2, z2, fov)[0],
pointFunction(x2, y2, z2, fov)[1],
width,
height
)[0],
fromCartesian(
pointFunction(x2, y2, z2, fov)[0],
pointFunction(x2, y2, z2, fov)[1],
width,
height
)[1]
);
}
threeDShape.js
:
function drawThreeDShape(data, point, line, fov, width, height, pointFunction, lineFunction) {
for (let i = 0; i < data.length; i += 1) {
if (typeof(data[i]) === "object") {
let previousPoint = data[i][0];
for (let j = 1; j < data[i].length; j += 1) {
line(
previousPoint[0], previousPoint[1], previousPoint[2],
data[i][j][0] , data[i][j][1] , data[i][j][2],
fov, width, height, point, lineFunction
);
previousPoint = data[i][j];
}
line(
data[i][data[i].length - 1][0], data[i][data[i].length - 1][1], data[i][data[i].length - 1][2],
data[i][0][0], data[i][0][1], data[i][0][2],
fov, width, height, point, lineFunction
)
}
else {
pointFunction(
fromCartesian(
point(
data[i][0],
data[i][1],
data[i][2]
)
)[0],
fromCartesian(
point(
data[i][0],
data[i][1],
data[i][2]
)
)[1]
);
}
}
}
threeDShapes.js
:
function threeDCube(l) {
return [
[
[
0 - l,
0 - l,
1
],
[
0 - l,
l,
1
],
[
l,
l,
1
],
[
l,
0 - l,
1
]
],
[
[
0 - l,
0 - l,
2
],
[
0 - l,
l,
2
],
[
l,
l,
2
],
[
l,
0 - l,
2
]
],
[
[
0 - l,
0 - l,
1
],
[
0 - l,
0 - l,
2
]
],
[
[
l,
0 - l,
1
],
[
l,
0 - l,
2
]
],
[
[
0 - l,
l,
1
],
[
0 - l,
l,
2
]
],
[
[
l,
l,
1
],
[
l,
l,
2
]
]
];
}
sketch.js
function setup() {
createCanvas(512, 512);
strokeWeight(2);
stroke(255, 255, 255);
background(0);
frameRate(64);
}
function draw() {
background(0);
drawThreeDShape(
rotateThreeDShape(threeDCube(width / 4), 1, 1, (frameCount % 1024) / 1024),
threeDPoint, threeDLine, 1, width, height, point, line
);
}