Sphere 구성을 위한 확인사항
Canvas 를 활용하여 원을 그리는 방법을 이전 포스트 글에서 구성해 보았습니다. sin, cos 함수를 사용하면, 그리 어렵지 않게
원을 구성할 수 있었습니다.
구는 입체이기 때문에 x,y 좌표외에 z 좌표가 필요합니다. 반지름이 1인 구를 기준으로 생각해 보면 x,y 가 0,0 일때 z 좌표는 1 인 경우
WebGL 에서는 앞으로 튀어나오는 방향이고, -1 인 경우 뒤에 있는 구 지점으로 판단할 수 있습니다.
불 투명일 경우 뒤에 있는 영역은 그려지지 않을 겁니다. 이렇듯 x, y, z 좌표로 Sphere 를 구성하는 좌표만 정의 할 수 있으면, 2차원
화면에 표현할 수 있으니, 그 방법을 정리해 보려고 합니다.
WebGL 에서는 오른손 좌표계를 사용하니, 오른손 좌표계에 맞게 좌표의 순서를 반시계 방향으로 등록할 수 있도록 구성할 예정 입니다.
예제 사이트는 이곳을 클릭하여 확인해 보실 수 있습니다.
참조 사이트
구에 대해서 많은 분들이 자세한 설명, 구현방법을 구성해 놓았습니다. 여러 사이트가 있지만, 아무래도 대표적인 설명은 wiki를 참조하는게 좋을 듯 해서
두개의 위키 사이트를 참조하여 구성해 보았습니다.
하나는 Sphere 자체에 대한 부분이고, 다른 하나는 좌표구성 ( 경우에 따라 지도의 위도, 경도에 따른 좌표 등 )을 중심으로 설명하고 있는 사이트 입니다.
https://en.wikipedia.org/wiki/Sphere
https://en.wikipedia.org/wiki/Spherical_coordinate_system
Sphere 구현을 위한 수식
\begin{align} center ( x_9, y_0, z_0 ), points(x, y, z), r 은 반지름 &&\\ ( x-x_0)^2 + (y-y_0)^2 + (z-z_0)^2 = r^2 &&\\ x = x_0 + r\times sin\theta \times cos\phi &&\\ y = y_0 + r\times sin\theta \times sin\phi &&\\ z = z_0 + r\times cos\theta &&\\ x^2+y^2+z^2 = r^2 => center ( 0, 0, 0 )\quad 일 때 &&\\ r \geq 0, &&\\ 0° ≤ \theta ≤ 180° (π\quad rad) &&\\ 0° ≤ \phi < 360° (2π\quad rad) && \end{align}
프로그램 구현과정에서의 중복 사항
수식으로 정리는 하고 있으나, 크게 보면 theta 가 0 ~ 180 도 까지 변화 하는데 이는 위도와 매칭된다고 생각해도 좋을 것 같습니다.
phi 는 0 ~ 360 도 까지 인데 경도에 해당합니다. 위키의 설명에는 360 도를 포함하지 않습니다. 360도는 사실상 첫 시작점과 같기 때문이지요…
하지만 프로그램 구성에서는 360 도를 포함 시켰습니다. 이유는 GL에서 사용하는 texture 좌표에서 마지막을 처음으로 연결시키지 않고 독립적으로 구성하기 위해서 입니다.
texture 에서 0 ~ 1 사이인데 360도가 1이 되도록 구성하기 위해서 입니다. ( 물론 조금 낭비가 되겠네요 … )
동일한 의미에서 지도에서 극점인 북극과 남극은 단 하나의 포인트만 있어야 하지만, texture 좌표를 구성하기 위해서 동일 좌표지만, 중복된 좌표가 등록되도록
구성해 보았습니다.
Canvas 로 구성한 첫번째 Sphere
위의 수식을 활용하여, x, y, z 좌표를 구하고 있습니다. z 좌표가 음수 이면 현재 상태에서는 그릴 필요가 없는 부분이라 건너 뛰도록 구성해 놓았습니다.
포인트를 출력하는 방법으로 구성하였기 때문에 구라는 느낌이 조금 적을 수도 있습니다.
Canvas 로 구성된 내용에서 위의 수식을 단순하게 적용해 놓았다는 것을 확인하는 정도면 족할 것 같습니다.
이전에도 언급하였듯 canvas 좌표는 왼쪽 상단이 0, 0 이기 때문에 중앙지점으로 translate 하고 있습니다. y 좌표도 아래로 증가하는 방향이라, 뒤집어서
표현하고 있습니다.
한번이라도 계산을 덜 하게 하려면, Math.sin(theta), Math.sin(phi), Math.cos(theta), Math.cos(phi) 를 한번만 하고, 사용만 하게 하는게 조금이라도 계산을 줄이겠지만,
이해를 위해 위의 수식과 동일하게 구성해 보았습니다.
const canvasWidth = 500;
const canvasHeight = 500;
function makeBasicCanvasSphere(xSize,ySize) {
const roundValue = 10000;
const radious = 200;
const thetaDelta = Math.floor(180/ySize * roundValue)/roundValue;
const phiDelta = Math.floor(360/xSize * roundValue)/roundValue;
const canvas = CanvasUtils.makeCanvasObject("basicCanvas", undefined, canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
ctx.save();
ctx.translate(canvasWidth/2, canvasHeight/2);
for ( let r = 0; r <= ySize; r++ ) {
let thetaDeg = r*thetaDelta;
if ( r == ySize ) {
thetaDeg = Math.round(thetaDeg);
}
let theta = Math.PI*thetaDeg/180;
ctx.beginPath();
ctx.strokeStyle = "#000080";
for ( let c = 0; c <= xSize; c++ ) {
let phiDeg = c*phiDelta;
if ( c == xSize )
phiDeg = Math.round(phiDeg);
let phi = Math.PI*phiDeg/180;
let xPos = Math.sin(theta)*Math.cos(phi);
let yPos = Math.sin(theta)*Math.sin(phi);
let zPos = Math.cos(theta);
let trX = Math.round(xPos*radious);
let trY = -Math.round(yPos*radious);
let trZ = Math.round(zPos*radious);
if ( zPos < 0 ) {
//continue;
}
ctx.moveTo(trX, trY);
ctx.arc(trX,trY , 2, 0, Math.PI*2 );
}
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
makeBasicCanvasSphere(20,20);
그 결과물 입니다.
Canvas 로 Sphere 좌표를 연결해 보았습니다.
순차적으로 좌표를 계산하여 저장하고, 저장된 Index 를 기준으로 반시계 방향으로 순서가 이어지도록 구성해 보았습니다.
WebGL 에서는 오른손 법칙이 적용되어 좌표가 반시계 방향 순서로 구성되어 있어야 정상 동작이 가능합니다.
Normal 등의 다른 항목도 있지만, 이 글에서는 좌표를 구성하고, 반시계 방향으로 인덱스를 구성하였다 정도만 정리하면 좋을 것 같습니다.
포지션과 인덱스를 구성하는 함수 입니다.
export const makeSphereValues = ( xSize, ySize, defColors ) => {
const roundValue = 10000;
const thetaDelta = Math.floor(180/ySize * roundValue)/roundValue;
const phiDelta = Math.floor(360/xSize * roundValue)/roundValue;
const positions = [];
const colors = [];
const vertexNormals = [];
const textureCoordinates = [];
const indices = [];
let defaultColors = undefined;
if ( defColors ) {
defaultColors = defColors;
} else {
defaultColors = [0.5, 0.5, 1.0, 1.0];
}
for ( let r = 0; r <= ySize; r++ ) {
let thetaDeg = r*thetaDelta;
if ( r == ySize ) {
thetaDeg = Math.round(thetaDeg);
}
let uvy = r/ySize;
let theta = Math.PI*thetaDeg/180;
for ( let c = 0; c <= xSize; c++ ) {
let phiDeg = c*phiDelta;
let uvx = c/xSize;
if ( c == xSize )
phiDeg = Math.round(phiDeg);
let phi = Math.PI*phiDeg/180;
let xPos = Math.sin(theta)*Math.cos(phi);
let yPos = Math.sin(theta)*Math.sin(phi);
let zPos = Math.cos(theta);
positions.push(xPos);
positions.push(yPos);
positions.push(zPos);
vertexNormals.push(xPos);
vertexNormals.push(yPos);
vertexNormals.push(zPos);
textureCoordinates.push(uvx);
textureCoordinates.push(uvy);
for ( let t = 0; t < 4; t++ ) {
colors.push(defaultColors[t]);
}
/*
idx01, idx02,
idx03, idx04
*/
if ( r > 0 && c > 0 ) {
let idx01 = (r-1)*(xSize+1) + (c-1);
let idx02 = idx01+1;
let idx03 = idx01+xSize+1;
let idx04 = idx03+1; // r, c 의 현재 위치
// GL 에서는 삼각형이 반시계 방향
indices.push(idx01);
indices.push(idx03);
indices.push(idx04);
indices.push(idx01);
indices.push(idx04);
indices.push(idx02);
}
}
}
return {
positions : positions,
colors : colors,
normals : vertexNormals,
textures : textureCoordinates,
indices : indices,
}
}
해당 함수를 활용하여 그리는 소스입니다.
function makeLineCanvasSphere(xSize,ySize) {
const roundValue = 10000;
const radius = 200;
const pInfos = GLDataUtils.makeSphereValues(xSize, ySize);
const canvas = CanvasUtils.makeCanvasObject("lineCanvas", undefined, canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
ctx.save();
ctx.translate(canvasWidth/2, canvasHeight/2);
let indexSize = pInfos.indices.length;
for ( let i = 0; i < indexSize; i += 6 ) {
ctx.beginPath();
ctx.strokeStyle = "blue";
let idx = pInfos.indices[i];
let xPos = Math.round(pInfos.positions[idx*3]*radius);
let yPos = -Math.round(pInfos.positions[idx*3+1]*radius);
let zPos = Math.round(pInfos.positions[idx*3+2]*radius);
if ( zPos < 0 )
continue;
ctx.moveTo(xPos,yPos);
idx = pInfos.indices[i+1];
xPos = Math.round(pInfos.positions[idx*3]*radius);
yPos = -Math.round(pInfos.positions[idx*3+1]*radius);
zPos = Math.round(pInfos.positions[idx*3+2]*radius);
if ( zPos < 0 )
continue;
ctx.lineTo(xPos,yPos);
idx = pInfos.indices[i+2];
xPos = Math.round(pInfos.positions[idx*3]*radius);
yPos = -Math.round(pInfos.positions[idx*3+1]*radius);
zPos = Math.round(pInfos.positions[idx*3+2]*radius);
if ( zPos < 0 )
continue;
ctx.lineTo(xPos,yPos);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.strokeStyle = "red";
idx = pInfos.indices[i+3];
xPos = Math.round(pInfos.positions[idx*3]*radius);
yPos = -Math.round(pInfos.positions[idx*3+1]*radius);
zPos = Math.round(pInfos.positions[idx*3+2]*radius);
if ( zPos < 0 )
continue;
ctx.moveTo(xPos,yPos);
idx = pInfos.indices[i+4];
xPos = Math.round(pInfos.positions[idx*3]*radius);
yPos = -Math.round(pInfos.positions[idx*3+1]*radius);
zPos = Math.round(pInfos.positions[idx*3+2]*radius);
if ( zPos < 0 )
continue;
ctx.lineTo(xPos,yPos);
idx = pInfos.indices[i+5];
xPos = Math.round(pInfos.positions[idx*3]*radius);
yPos = -Math.round(pInfos.positions[idx*3+1]*radius);
zPos = Math.round(pInfos.positions[idx*3+2]*radius);
if ( zPos < 0 )
continue;
ctx.lineTo(xPos,yPos);
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
makeLineCanvasSphere(20,20);
중복되는 부분이 많지만, 가독성이 더 좋을 것 같아서 나열하듯이 구성해 보았습니다.
2개의 삼각형이 거의 한쌍 처럼 반복 되기 때문에 6개의 index를 기준으로 출력하도록 구성해 보았습니다.
그 결과물 입니다.
그림을 로딩하여 Sphere 에 표현해 보았습니다.
위의 형태를 조금 변형해서 지구 같은 지도를 구에 어떻게 표현할 수 있는지 간단히 정리해 보았습니다.
좌표값을 설정하는 부분에서 낭비가 많지만, 대략 어떤식으로 매핑할 수 있는지를 중심으로 구현해 보았습니다.
function makeImageCanvasSphere(orgImageData) {
const roundValue = 10000;
const radious = 200;
const xSize = orgImageData.width;
const ySize = orgImageData.height;
const thetaDelta = Math.floor(180/ySize * roundValue)/roundValue;
const phiDelta = Math.floor(360/xSize * roundValue)/roundValue;
const canvas = CanvasUtils.makeCanvasObject("imageCanvas", undefined, canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(canvasWidth, canvasHeight);
ctx.save();
const cx = canvasWidth/2;
const cy = canvasHeight/2;
const currentSet = new Set();
let samePositions = 0;
for ( let r = 0; r <= ySize; r++ ) {
let thetaDeg = r*thetaDelta;
if ( r == ySize ) {
thetaDeg = Math.round(thetaDeg);
}
let uvy = r/ySize;
// 만약 pixel 로 uv y 를 가져올려면 webgl 에서는 1-uvy
uvy = 1-uvy;
let theta = Math.PI*thetaDeg/180;
for ( let c = 0; c <= xSize; c++ ) {
let phiDeg = c*phiDelta;
let uvx = c/xSize;
if ( c == xSize )
phiDeg = Math.round(phiDeg);
let phi = Math.PI*phiDeg/180;
let xPos = Math.sin(theta)*Math.cos(phi);
let yPos = Math.sin(theta)*Math.sin(phi);
let zPos = Math.cos(theta);
if ( zPos < 0 ) {
continue;
}
let trX = Math.round(xPos*radious);
let trY = -Math.round(yPos*radious);
let trZ = Math.round(zPos*radious);
let keys = trX+"#"+trY;
if ( currentSet.has(keys) ) {
samePositions++;
continue;
}
currentSet.add(keys);
let imgIdx = (trY+cy)*500*4+(trX+cx)*4;
let pixels = CanvasUtils.getPixels(c,r,1,xSize,ySize,orgImageData);
for ( let t = 0; t < 4; t++ ) {
imageData.data[imgIdx+t] = pixels[t];
}
}
}
ctx.putImageData(imageData,0,0);
ctx.restore();
//console.log ( currentSet.size + " : " + samePositions);
}
const url = "/imgs/8k_earth_daymap.jpg";
CanvasUtils.makeCanvasImageData(url).then( imgData => {
makeImageCanvasSphere(imgData);
});