我编写了一些JS代码,用于将3D线框球体绘制到HTML5画布中。
我从this post开始,并使用Qt3D vertices generation对球形网格进行了改进。 JS代码在顶点上执行2次传递:第一次显示环,第二次显示切片。通常,OpenGL会自动将所有顶点与三角形连接起来。
我保持切片/环可配置,但我对转换代码有疑问,例如当我沿X轴旋转球体时。
所以,从基础开始。这是一个1遍,4个环,4个切片,没有变换:
似乎一切都很好。现在2遍,10环,10片,没有转变:
还是不错的,但如果我在X轴上旋转30°,顶部和底部顶点(显然只是Y位置)会搞砸。
我怀疑旋转功能或投影功能有问题。
有人可以帮我弄清楚这里发生了什么吗?
(注意我不想使用Three.js导致我的目标是在QML应用程序中移植它)
这是完整的代码。
var sphere = new Sphere3D();
var rotation = new Point3D();
var distance = 1000;
var lastX = -1;
var lastY = -1;
function Point3D() {
this.x = 0;
this.y = 0;
this.z = 0;
}
function Sphere3D(radius) {
this.vertices = new Array();
this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
this.rings = 10;
this.slices = 10;
this.numberOfVertices = 0;
var M_PI_2 = Math.PI / 2;
var dTheta = (Math.PI * 2) / this.slices;
var dPhi = Math.PI / this.rings;
// Iterate over latitudes (rings)
for (var lat = 0; lat < this.rings + 1; ++lat) {
var phi = M_PI_2 - lat * dPhi;
var cosPhi = Math.cos(phi);
var sinPhi = Math.sin(phi);
// Iterate over longitudes (slices)
for (var lon = 0; lon < this.slices + 1; ++lon) {
var theta = lon * dTheta;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
p = this.vertices[this.numberOfVertices] = new Point3D();
p.x = this.radius * cosTheta * cosPhi;
p.y = this.radius * sinPhi;
p.z = this.radius * sinTheta * cosPhi;
this.numberOfVertices++;
}
}
}
function rotateX(point, radians) {
var y = point.y;
point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateY(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateZ(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}
function projection(xy, z, xyOffset, zOffset, distance) {
return ((distance * xy) / (z - zOffset)) + xyOffset;
}
function strokeSegment(index, ctx, width, height) {
var x, y;
var p = sphere.vertices[index];
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
x = projection(p.x, p.z, width / 2.0, 100.0, distance);
y = projection(p.y, p.z, height / 2.0, 100.0, distance);
if (lastX == -1 && lastY == -1) {
lastX = x;
lastY = y;
return;
}
if (x >= 0 && x < width && y >= 0 && y < height) {
if (p.z < 0) {
ctx.strokeStyle = "gray";
} else {
ctx.strokeStyle = "white";
}
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
lastX = x;
lastY = y;
}
}
function render() {
var canvas = document.getElementById("sphere3d");
var width = canvas.getAttribute("width");
var height = canvas.getAttribute("height");
var ctx = canvas.getContext('2d');
var p = new Point3D();
ctx.fillStyle = "black";
ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
// draw each vertex to get the first sphere skeleton
for (i = 0; i < sphere.numberOfVertices; i++) {
strokeSegment(i, ctx, width, height);
}
// now walk through rings to draw the slices
for (i = 0; i < sphere.slices + 1; i++) {
for (var j = 0; j < sphere.rings + 1; j++) {
strokeSegment(i + (j * (sphere.slices + 1)), ctx, width, height);
}
}
}
function init() {
rotation.x = Math.PI / 6;
render();
}
canvas {
background: black;
display: block;
}
<body onLoad="init();">
<canvas id="sphere3d" width="500" height="500">
Your browser does not support HTML5 canvas.
</canvas>
</body>
答案 0 :(得分:1)
您的问题是您的stroke.vertices []数组的内容正在您的strokeSegment()调用中被修改,因此当您在每个点上第二次调用时,旋转将被应用两次。因此,在strokeSegment()中,您需要替换:
var p = sphere.vertices[index];
使用:
var p = new Point3D();
p.x = sphere.vertices[index].x;
p.y = sphere.vertices[index].y;
p.z = sphere.vertices[index].z;
然后它完美地工作如下所示:
var sphere = new Sphere3D();
var rotation = new Point3D();
var distance = 1000;
var lastX = -1;
var lastY = -1;
function Point3D() {
this.x = 0;
this.y = 0;
this.z = 0;
}
function Sphere3D(radius) {
this.vertices = new Array();
this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
this.rings = 10;
this.slices = 10;
this.numberOfVertices = 0;
var M_PI_2 = Math.PI / 2;
var dTheta = (Math.PI * 2) / this.slices;
var dPhi = Math.PI / this.rings;
// Iterate over latitudes (rings)
for (var lat = 0; lat < this.rings + 1; ++lat) {
var phi = M_PI_2 - lat * dPhi;
var cosPhi = Math.cos(phi);
var sinPhi = Math.sin(phi);
// Iterate over longitudes (slices)
for (var lon = 0; lon < this.slices + 1; ++lon) {
var theta = lon * dTheta;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
p = this.vertices[this.numberOfVertices] = new Point3D();
p.x = this.radius * cosTheta * cosPhi;
p.y = this.radius * sinPhi;
p.z = this.radius * sinTheta * cosPhi;
this.numberOfVertices++;
}
}
}
function rotateX(point, radians) {
var y = point.y;
point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateY(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateZ(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}
function projection(xy, z, xyOffset, zOffset, distance) {
return ((distance * xy) / (z - zOffset)) + xyOffset;
}
function strokeSegment(index, ctx, width, height) {
var x, y;
var p = new Point3D();
p.x = sphere.vertices[index].x;
p.y = sphere.vertices[index].y;
p.z = sphere.vertices[index].z;
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
x = projection(p.x, p.z, width / 2.0, 100.0, distance);
y = projection(p.y, p.z, height / 2.0, 100.0, distance);
if (lastX == -1 && lastY == -1) {
lastX = x;
lastY = y;
return;
}
if (x >= 0 && x < width && y >= 0 && y < height) {
if (p.z < 0) {
ctx.strokeStyle = "gray";
} else {
ctx.strokeStyle = "white";
}
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
lastX = x;
lastY = y;
}
}
function render() {
var canvas = document.getElementById("sphere3d");
var width = canvas.getAttribute("width");
var height = canvas.getAttribute("height");
var ctx = canvas.getContext('2d');
var p = new Point3D();
ctx.fillStyle = "black";
ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
// draw each vertex to get the first sphere skeleton
for (i = 0; i < sphere.numberOfVertices; i++) {
strokeSegment(i, ctx, width, height);
}
// now walk through rings to draw the slices
for (i = 0; i < sphere.slices + 1; i++) {
for (var j = 0; j < sphere.rings + 1; j++) {
strokeSegment(i + (j * (sphere.slices + 1)), ctx, width, height);
}
}
}
function init() {
rotation.x = Math.PI / 3;
render();
}
canvas {
background: black;
display: block;
}
<body onLoad="init();">
<canvas id="sphere3d" width="500" height="500">
Your browser does not support HTML5 canvas.
</canvas>
</body>
答案 1 :(得分:1)
简短回答
该错误发生在strokeSegment
函数
function strokeSegment(index, ctx, width, height) {
var x, y;
var p = sphere.vertices[index];
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
...
错误是所有rotate
函数都在原位修改p
,因此修改了sphere.vertices
中存储的值!所以修复它的方法就是克隆这一点:
function strokeSegment(index, ctx, width, height) {
var x, y;
var p0 = sphere.vertices[index];
var p = new Point3D();
p.x = p0.x;
p.y = p0.y;
p.z = p0.z;
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
...
您可以在https://plnkr.co/edit/zs5ZxbglFxo9cbwA6MI5?p=preview
找到包含固定代码的演示更长时间
在我发现这个问题之前,我使用了你的代码,我觉得有所改进。 https://plnkr.co/edit/tpTZ8GH9eByVARUIYZBi?p=preview
提供了改进版本var sphere = new Sphere3D();
var rotation = new Point3D(0, 0, 0);
var distance = 1000;
var EMPTY_VALUE = Number.MIN_VALUE;
function Point3D(x, y, z) {
if (arguments.length == 3) {
this.x = x;
this.y = y;
this.z = z;
}
else if (arguments.length == 1) {
fillPointFromPoint(this, x); // 1 argument means point
}
else {
clearPoint(this); // no arguments mean creat empty
}
}
function fillPointFromPoint(target, src) {
target.x = src.x;
target.y = src.y;
target.z = src.z;
}
function clearPoint(p) {
p.x = EMPTY_VALUE;
p.y = EMPTY_VALUE;
p.z = EMPTY_VALUE;
}
function Sphere3D(radius) {
this.radius = (typeof(radius) == "undefined" || typeof(radius) != "number") ? 20.0 : radius;
this.innerRingsCount = 9; // better be odd so we have explicit Equator
this.slicesCount = 8;
var M_PI_2 = Math.PI / 2;
var dTheta = (Math.PI * 2) / this.slicesCount;
var dPhi = Math.PI / this.innerRingsCount;
this.rings = [];
// always add both poles
this.rings.push([new Point3D(0, this.radius, 0)]);
// Iterate over latitudes (rings)
for (var lat = 0; lat < this.innerRingsCount; ++lat) {
var phi = M_PI_2 - lat * dPhi - dPhi / 2;
var cosPhi = Math.cos(phi);
var sinPhi = Math.sin(phi);
console.log("lat = " + lat + " phi = " + (phi / Math.PI) + " sinPhi = " + sinPhi);
var vertices = [];
// Iterate over longitudes (slices)
for (var lon = 0; lon < this.slicesCount; ++lon) {
var theta = lon * dTheta;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
var p = new Point3D();
p.x = this.radius * cosTheta * cosPhi;
p.y = this.radius * sinPhi;
p.z = this.radius * sinTheta * cosPhi;
vertices.push(p);
}
this.rings.push(vertices);
}
// always add both poles
this.rings.push([new Point3D(0, -this.radius, 0)]);
}
function rotateX(point, radians) {
var y = point.y;
point.y = (y * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (y * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateY(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.z * Math.sin(radians) * -1.0);
point.z = (x * Math.sin(radians)) + (point.z * Math.cos(radians));
}
function rotateZ(point, radians) {
var x = point.x;
point.x = (x * Math.cos(radians)) + (point.y * Math.sin(radians) * -1.0);
point.y = (x * Math.sin(radians)) + (point.y * Math.cos(radians));
}
function projection(xy, z, xyOffset, zOffset, distance) {
return ((distance * xy) / (z - zOffset)) + xyOffset;
}
var lastP = new Point3D();
var firstP = new Point3D();
function startRenderingPortion() {
clearPoint(lastP);
clearPoint(firstP);
}
function closeRenderingPortion(ctx, width, height) {
strokeSegmentImpl(ctx, firstP.x, firstP.y, firstP.z, width, height);
clearPoint(lastP);
clearPoint(firstP);
}
function strokeSegmentImpl(ctx, x, y, z, width, height) {
if (x >= 0 && x < width && y >= 0 && y < height) {
// as we work with floating point numbers, there might near zero that != 0
// choose gray if one of two points is definitely (z < 0) and other has (z <= 0)
// Note also that in term of visibility this is a wrong logic! Line is invisible
// only if it is shadowed by another polygon and this depends on relative "Z" not
// absolute values
var eps = 0.01;
if (((z < -eps) && (lastP.z < eps))
|| ((z < eps) && (lastP.z < -eps))) {
ctx.strokeStyle = "gray";
} else {
ctx.strokeStyle = "white";
}
if ((x === lastP.x) && (y == lastP.y)) {
ctx.beginPath();
// draw single point
ctx.moveTo(x, y);
ctx.lineTo(x + 1, y + 1);
ctx.stroke();
ctx.closePath();
} else {
ctx.beginPath();
ctx.moveTo(lastP.x, lastP.y);
ctx.lineTo(x, y);
ctx.stroke();
ctx.closePath();
}
lastP.x = x;
lastP.y = y;
lastP.z = z;
}
}
function strokeSegment(p0, ctx, width, height) {
var p = new Point3D(p0); // clone original point to not mess it up with rotation!
rotateX(p, rotation.x);
rotateY(p, rotation.y);
rotateZ(p, rotation.z);
var x, y;
x = projection(p.x, p.z, width / 2.0, 100.0, distance);
y = projection(p.y, p.z, height / 2.0, 100.0, distance);
if (lastP.x === EMPTY_VALUE && lastP.y === EMPTY_VALUE) {
lastP = new Point3D(x, y, p.z);
fillPointFromPoint(firstP, lastP);
return;
}
strokeSegmentImpl(ctx, x, y, p.z, width, height);
}
function renderSphere(ctx, width, height, sphere) {
var i, j;
var vertices;
// draw each vertex to get the first sphere skeleton
for (i = 0; i < sphere.rings.length; i++) {
startRenderingPortion();
vertices = sphere.rings[i];
for (j = 0; j < vertices.length; j++) {
strokeSegment(vertices[j], ctx, width, height);
}
closeRenderingPortion(ctx, width, height);
}
// now walk through rings to draw the slices
for (i = 0; i < sphere.slicesCount; i++) {
startRenderingPortion();
for (j = 0; j < sphere.rings.length; j++) {
vertices = sphere.rings[j];
var p = vertices[i % vertices.length];// for top and bottom vertices.length = 1
strokeSegment(p, ctx, width, height);
}
//closeRenderingPortion(ctx, width, height); // don't close back!
}
}
function render() {
var canvas = document.getElementById("sphere3d");
var width = canvas.getAttribute("width");
var height = canvas.getAttribute("height");
var ctx = canvas.getContext('2d');
ctx.fillStyle = "black";
ctx.clearRect(0, 0, width, height);
ctx.fillRect(0, 0, width, height);
renderSphere(ctx, width, height, sphere);
}
function init() {
rotation.x = Math.PI / 6;
//rotation.y = Math.PI / 6;
rotation.z = Math.PI / 6;
render();
}
主要变化是:
vertices
数组中明确地分离了普通数组rings
,并明确地将两个极点添加到它中。 rings
的分离让我更频繁地清除lastX / Y,以通过引入startRenderingPortion
来避免一些虚假的行。 closeRenderingPortion
逻辑上相似的closePath
。使用这种方法,我能够删除所需的重复点。renderSphere
或clearPoint
),但我更改了Point3D
构造函数以支持3种模式:(x ,y,z),指向,空。var EMPTY_VALUE = Number.MIN_VALUE;
使用更明确的标记值。 -1
是可能的值另请注意,您的灰色/白色选择存在潜在的错误,我没有修复。我认为你的颜色应该反映出&#34;隐形&#34; Z > 0
vs Z < 0
的行和简单逻辑无法正确解决此问题。实际上,如果场景中的其他东西遮挡了单行,则它可能只是部分可见。