戻る

Texture map for a sphere

2023/12/27


概要

Three.jsライブリーを使った3Dグラフィックスを始めた頃から、球体をガラスまたは鏡のように見せる手法があるのは知っていた。

今回、作成したので整理を兼ねて記録しておく



テキスチャーの作成

球体が背景画像を反射しているように見せる6枚の画像を貼り付け、疑似的に表現。

texture画像の切り取り

  • アスペクト比4:3の背景画像の6か所を切り取る
  • widthとheightが同じにしないとエラーになる
  • topとbottomの画像が出来るだけ水平位置にある他の画像と類似になるようにする

参考). 画像を切り取るpythonプロクラム


from PIL import Image
import os

# Open the uploaded image
img = Image.open("data/P8178284_01.jpg")

# Assuming the panorama is a horizontal strip, we will divide it into four equal parts for front, back, left, right
# and create two additional parts for top and bottom with a solid color.

# Calculate the width of each part for the horizontal strip
part_width = img.width // 4
part_height = img.width // 4

# Define the parts for the cube map
parts = {
    'front': img.crop((part_width, part_height, 2 * part_width, 2 * part_height)),
    'back': img.crop((3 * part_width, part_height, 4 * part_height, 2 * part_height)),
    'left': img.crop((0, part_height, part_width, 2 * part_height)),
    'right': img.crop((2 * part_width, part_height, 3 * part_width, 2 * part_height)),
    'top': img.crop((part_width, 0 , 2 * part_width, part_height)),
    'bottom' : img.crop((part_width, 2 *  part_height, 2 * part_width, 3 * part_height))
}

# Save the parts to the disk
saved_paths = {}
output_dir = 'data/cubemap/'
if not os.path.exists(output_dir):
    os.makedirs(output_dir)

for part_name, part_img in parts.items():
    part_path = f"{output_dir}{part_name}.jpg"
    part_img.save(part_path)
    saved_paths[part_name] = part_path

saved_paths



html


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="keywords" content="3D glass shere" />
<meta name="description" content="Three js glass shere" />
<titleɯD glass shere</title>
<link rel="shortcut icon" href="https://redrb.heteml.net/parts/akaicon.ico">
<link rel="stylesheet" href="css/kl.css" />
<style type="text/css">
<!--
h1 {
     color:lime;
     text-align:center;
}
#canvas_container {
     width:80%;
     margin:50px auto;
}
#canvas3dglass {
      width:100%;
      margin:0px auto;
}
-->
</style>
<script src="https://unpkg.com/stats.js@0.17.0/build/stats.min.js"></script>
  <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.152.2/build/three.module.js",
          "three/addons/": "https://unpkg.com/three@0.152.2/examples/jsm/"
          }
      }
  </script>
</head>
<body>
<h1>町田市薬師池公園</h1>
<div id="canvas_container">
<canvas id="canvas3dglass"></canvas>
 <script type="module" src ="js/glass.js"> </script> 
</div>




javascript コード(glass.js)


//-----------------   2023.12.27
import * as THREE from "three";
import { FontLoader } from "./FontLoader.js";
import { TextGeometry } from "./TextGeometry.js";
import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js";

// canvas size
const canvasElement = document.querySelector("#canvas3dglass");
const width = canvasElement.clientWidth;
const height = canvasElement.clientHeight;
// renderer
const renderer = new THREE.WebGLRenderer({
  canvas: canvasElement, alpha: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
// renderer.setClearColor(0x000040);

// scene
const scene = new THREE.Scene();

// camera
const camera = new THREE.PerspectiveCamera(45, width / height);
//  camera.position.set(0, 0, 1000);
var radiuscamera = width ;  // camera radius 
var anglecamera = 0;              // camera angle 

// light
const ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
// 1辺あたりに配置するオブジェクトの個数
const CELL_NUM = 30;
// 結合用のジオメトリを格納する配列
const boxes = [];
for (let i = 0; i < CELL_NUM; i++) {
  for (let j = 0; j < CELL_NUM; j++) {
    for (let k = 0; k < CELL_NUM; k++) {
      // 立方体個別の要素を作成
      const geometrySphere = new THREE.SphereGeometry(2, 2, 2);
      // 座標調整
      const geometryTranslated = geometrySphere.translate(
        100 * (i - CELL_NUM / 2),
        100 * (j - CELL_NUM / 2),
        100 * (k - CELL_NUM / 2)
      );
      // ジオメトリを保存
      boxes.push(geometryTranslated);
    }
  }
}
// ジオメトリを生成
const geometry = BufferGeometryUtils.mergeGeometries(boxes);

// マテリアルを作成
const material = new THREE.MeshStandardMaterial({ color: 0xffffff, wireframe: true });

// メッシュを作成
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// ---- sphere
const path = "glassimg";
var bgimg = "P8178284.jpg";

document.getElementById('canvas_container').style.backgroundImage = "url(" + path + "/" + bgimg +")";
document.getElementById('canvas_container').style.backgroundRepeat = "no-repeat";
document.getElementById('canvas_container').style.backgroundPosition = "50% 50%";  // x y
document.getElementById('canvas_container').style.backgroundSize = 100 + "%";

const urls = [
  path + '/' + 'right.jpg',
  path + '/' + 'left.jpg',
  path + '/' + 'top.jpg',
  path + '/' + 'bottom.jpg',
  path + '/' + 'front.jpg',
  path + '/' + 'back.jpg'
];

var cubeTextureLoader = new THREE.CubeTextureLoader();
var textureMirror = cubeTextureLoader.load(urls); 

// Mirror -----------
var mirrorGeometry = new THREE.SphereGeometry(200, 200, 30);
var mirrorMaterial = new THREE.MeshPhongMaterial({
   envMap: textureMirror,
   reflectivity: 1.2
});

var mesh1 = new THREE.Mesh(mirrorGeometry, mirrorMaterial);
scene.add(mesh1);
// text define
const fsize = 50;
const text = 'Yakushiike Park';
const loadert = new FontLoader()
loadert.load('fonts/optimer_regular.typeface.json', function (font) {
  const textGeometry = new TextGeometry(text, {
    font: font,
    size: fsize,
    height: 1,
  });
  const textMaterial = new THREE.MeshPhongMaterial({ color: 0xffd700 });
  var textMesh = new THREE.Mesh(textGeometry, textMaterial);
  scene.add(textMesh);
  textGeometry.center();         // text on center position set
  textMesh.position.set(0, -300, 0);
 });

tick();

// 毎フレーム時に実行されるループイベントです  
function tick() {

  // camera move 
  camera.position.x = radiuscamera * Math.cos(Math.PI / 180 * anglecamera);
  camera.position.y = radiuscamera * Math.sin(Math.PI / 180 * anglecamera);
  camera.position.z = 1000 + radiuscamera * Math.sin(Math.PI / 180 * anglecamera);
  camera.lookAt(new THREE.Vector3(0, 0, 0));  // 中央に向ける
  anglecamera += 0.2;
  
  // レンダリング
  renderer.render(scene, camera);
  requestAnimationFrame(tick);

}; // tick end
// resize
window.addEventListener('resize', onResize);
function onResize() {
  camera.aspect = canvasElement.clientWidth / canvasElement.clientHeight;
  camera.updateProjectionMatrix();
// --- end 


感想

鏡やガラスでなくても、球体に画像を貼り付けるときは出来上がりを意識して原画像を注意して選ぶ。 例えば建物であれば前後左右上下に意識して撮影するとよいと思うが、現実には難しい。疑似的に使えるような写真を意識して撮るのも必要であろう。

戻る