1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【three.js】地球などの3D球体に円を描画する【GLSL】

Last updated at Posted at 2023-09-25

three.jsにおいて、地球などの3D球体の表面に円を描画する方法をいくつか考えたのでまとめ。
球体は地球などを想定し、円の中心座標を緯度経度で与える想定とする。

完成形はこんな感じ。

この円は半径とかを時間とともに動かして、衝撃波のエフェクトだったり、波のエフェクトみたいなのを作る基礎になるのをイメージして。。

環境

はじめに

球体はSphereGeometryを使って作成することとし、円を上に描画するためのシェーダーを書きたいので、ShaderMaterialと一緒に利用する。
構造は以下のようになる。

  • radiusは球の半径。
  • circleCenterは円の中心座標で、これはuniformsを通じて渡す。
<mesh>
  <sphereGeometry args={[radius, 128, 64]} />
  <shaderMaterial
    vertexShader={vertexShader}
    fragmentShader={fragmentShader}
    uniforms={{
      radius: { value: radius },
      circleCenter: { value: circleCenter },
    }}
  />
</mesh>

circleCenter

緯度経度は角度情報なので、一度ラジアンに直して、それをthree.jsのsetFromSphericalCoordsメソッドでVector3に変換することができる。
これは、球体の中心を0,0,0とするローカルスペース上での座標になる。

const circleCenter = new Vector3().setFromSphericalCoords(
  radius,
  MathUtils.degToRad(90), // longitude
  MathUtils.degToRad(90) // latitude
);

パターン1. Vertex Shaderで実装する

Vertex Shaderは頂点ごとに実行されるシェーダーで、頂点は3D空間上での位置を保持しているので(position変数)、円の中心からの距離を簡単に計算することができる。
しかし、最終的な円の見た目がポリゴンの粒度に左右されるのがよくない。

Vertex Shader

単純に処理中の頂点の位置と描画したい円の中心の位置の距離を測って、distという変数でFragment Shaderに受け渡す。

vertex shader
uniform vec3 circleCenter;
varying vec2 vUv;
varying float dist;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  dist = distance(position, circleCenter);
}

Fragment Shader

ピクセル単位にdistの値によって色をわける。

fragment shader
varying vec2 vUv;
varying float dist;
void main() {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  if(dist < 0.2) {
    gl_FragColor = vec4(1.0, 0., 0., 1.0);
  }
}

パターン2. Fragment Shaderで実装する

Fragment Shaderはピクセルごとに実行されるシェーダーで、Vertex Shaderに比べると少し高価になる。
ピクセルの座標はUV座標やテクスチャ座標など2次元での座標になるので、3次元での座標である円の中心座標との距離計算に難があるが、それを乗り切れば以下のように綺麗な円を作れる。

ちなみにSphereGeometryは正筒投影(Cylindrical Projection)のようなので、テクスチャも世界地図みたいなものをイメージしてOK。

Vertex Shader

特に何もしない。

vertex shader
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

Fragment Shader

setFromSphericalCoordsというメソッドを実装する。
これはcircleCenterを作るときに使ったthree.jsのメソッドと同じ処理をGLSLで書き直したもの。

three.jsのリポジトリにVector3クラスのコードが全部あるので、この中からsetFromSphericalCoordsを探して、ChatGPTにコピペ、「GLSLで書き直して」と頼むだけ。こういうときオープンソースは便利だなあ。
https://github.com/mrdoob/three.js/blob/master/src/math/Vector3.js

uvCoordsToSphericalCoordsはUV座標系(-1から+1の範囲になる)を球表面の座標に変換するためのメソッドで、ピクセルごとに距離を計測していくため必要。
UnityとかだとUV座標が0〜1なのに、three.jsは-1から1っぽいのでハマってしまった。

ちなみに緯度は-90から90の範囲で、マイナスが北方向、経度は-180から180の範囲らしい。(間違ってたらすいません。)

fragment shader
uniform float radius;
uniform vec3 circleCenter;
varying vec2 vUv;

vec3 setFromSphericalCoords(float radius, float phi, float theta) {
  float sinPhiRadius = sin(phi) * radius;
  vec3 result;
  result.x = sinPhiRadius * sin(theta);
  result.y = cos(phi) * radius;
  result.z = sinPhiRadius * cos(theta);
  return result;
}

vec3 uvCoordsToSphericalCoords(vec2 uv) {
  // UV is -1 to 1 in three.js
  float phi = radians(180. * -uv.y);
  float theta = radians(360. * -uv.x);
  return setFromSphericalCoords(radius, phi, theta);
}

void main() {
  vec3 pixelOnSphereCoords = uvCoordsToSphericalCoords(vUv);
  float dist = distance(pixelOnSphereCoords, circleCenter);
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // white
  if(dist < 0.5) {
    gl_FragColor = vec4(1.0, 0., 0., 1.0); // red
  }
}

この方法はピクセルごとにサインだったりコサインだったり計算するので結構高価かも。

パターン3. Vertex Shaderで実装するけど形状を工夫してうまく見せる

いろいろ調べてて感心した方法。
それは、VertexShaderを使うけどポリゴンをこんな感じの点で描画して行う方法。等間隔に点を敷き詰められればメッシュのガクガクした感じが出ずに、いい感じになりそう。

この点1つ1つがGeometryになっていて、Vertex Shaderで距離計算とかができる。参考コードはここ。
https://codepen.io/prisoner849/pen/MWbeoGj

上記だとUVSphereがベースになってそうだけど、ICOSphereを使ったらもっと綺麗になりそう。
参考、UV球とICO球の違い)https://www.makeuseof.com/uv-sphere-icosphere-blender/

その場合Blenderとかで作っておいてもいいかも。(このコード例だとJavaScript側で球体メッシュの生成までやってる。)
正筒投影(Cylindrical Projection)に対応させるためにUV展開するのはまた面倒かもしれないけど。

追記:と思ったらHexSphereの綺麗なUVのプロジェクトがあった。
https://github.com/arscan/hexasphere.js

さらに追記:HexasphereはUVを頂点ごとに作ってくれるプロジェクトではなさそうなので、three.jsなどで自分でUVを計算する必要がある。左右の端においてはどうしても歪みが出てしまうが、36分割くらいすれば見た目に問題はなさそう?
頂点ごとだと、

const getUV = (vertex: Vector3) => {
  const { phi, theta } = cartesianCoordsToSphericalCoords(vertex);
  const V = phi / Math.PI;
  const U = theta / Math.PI;
  return { U, V };
};

まとめ

サイバーな見た目というのは、理由があってサイバーなんだなと感心したのでよかった。
点で作った球体でいろいろなエフェクトを乗せて遊ぶのも楽しそうなので今度やってみる。
今回はVertex ShaderとFragment Shaderの違いや特長も学習できたのでよかった。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?