2017-03-12 11 views
1

3Dワイヤフレーム球をHTML5キャンバスにプロットするためのJSコードを記述しました。HTML5 Canvas + JavaScriptワイヤフレーム球体変換の問題

私はthis postから開始し、Qt3D vertices generationを球形メッシュに使用して改良しました。 JSコードは、最初にリングを表示し、2番目にスライスを表示するために頂点上を2回通過します。通常、OpenGLはすべての頂点を自動的に三角形で接続します。

私はスライス/リングを設定可能にしていましたが、例えばX軸に沿って球を回転させるときなど、変換コードに問題があります。

だから、基本から始めて。

enter image description here

がすべて良いと思われる:ここでは1パス、4輪、4つのスライス、無変換です。今、2回のパス、10個のリング、10個のスライス、変換なし:私は(唯一明らかにY位置)をX軸、上部と下部の頂点にそれを30°回転させた場合

enter image description here

まだ良いが、取得しますうんざりする

enter image description here

私はそこに何かが間違って回転機能である、または投影機能に疑いがあります。

ここで何が起こっているのか理解してもらえますか?

(私は私の目標は、QMLアプリケーションでポートにこれです原因Three.jsを使用したくない

ここでは完全なコードです。

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>

答えて

1

ショート

var p = sphere.vertices[index]; 

で:だから、strokeSegment()であなたは交換する必要があります答え

バグが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(); 
} 

で提供されています主な変更点は以下のとおりです。

  • 私は明示的に配列ringsの配列でプレーンな配列verticesを分離し、また、明示的に両極を追加します。
  • ringsの分離は、startRenderingPortionを導入することによって、いくつかの偽の行を避けるためにより多くの場合lastX/Yをクリアすることを可能にしました。
  • また、closePathと論理的に似ているcloseRenderingPortionも紹介しました。この方法を使用して、必要なポイントの重複を取り除くことができました。
  • 一般的に私はあなたのコード(renderSphereまたはclearPointを参照)でやっているようにOOP-ishスタイルを避けようとしましたが、Point3Dコンストラクタを3つのモード(x、y、z)、ポイント、空をサポートするように変更しました。
  • 空のlastX/Yのマーカー値をより明示的に使用するvar EMPTY_VALUE = Number.MIN_VALUE;-1は可能な値です

私が修正しなかった灰色/白色の選択に潜在的なバグがあることにも注意してください。私はあなたの色が "不可視"の線を反映する必要があり、単純な論理Z > 0Z < 0はこの問題を正しく解決しないと仮定します。実際には、シーン内の他のものによって隠されている場合、部分的にしか見えないかもしれません。

+0

すばらしい答え、ありがとう。私はC/C++開発者であることを認めなければなりません。ポインタと参照の概念はJavaScriptではあまり明確ではありません。このコードは、私のQt/QMLプロジェクトのQML Canvasに描画するためのショートカットです。 –

+0

@MassimoCallegari、JavaScriptではすべてが値渡されますが(これは非常に重要な "but"です)、プリミティブ型を除くすべてが参照であり、それらの "スタック"バージョンを取得できません。したがって、オブジェクトまたは配列をあるメソッドに渡して引数を変更すると、値が変更されます。 http://stackoverflow.com/questions/6605640/javascript-by-reference-vs-by-valueも参照してください。これらのルールは、QWidgetをポインタとして作成するQtルールと実際に似ています – SergGr

1

あなたの問題は、あなたがそれを2回目以降の起動時に回転を2回適用されますので、ごsphere.verticesの内容は、[]配列は、あなたのstrokeSegment()呼び出しの内部で修正されていることです各点。以下に示すように

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>