【Three.js × Blender × webpack5】ビットくんで遊びたい

Takashi Yamaguchi

  • line
  • はてなブックマーク
  • x

システム開発

【Three.js × Blender × webpack5】ビットくんで遊びたい

こんにちは!
キーボード沼で散財しすぎてカード明細を見るのが怖い、エンジニアの山口です

先日、BBブログを読んでいて興味深い記事が目に止まりました
これです!

ブレンダーはじめました。その1

どこからみても青いドロップに手足がついた妖精だ

豆じゃないじゃん!と思わず吹いてしまったのですが
よくよく見ると可愛らしく見えてきます。なにか無骨ながら語りかけてくるものがある、、

「私を使って、あそんでよ」

そう言ってるような、、

ということで、今回はこの3DモデルとThree.jsを使って、3D空間いっぱいにビットくんを遊ばせたいと思います

いくぞ!

3Dモデルのデータを作る

Blenderから3Dモデルのデータを適切な形式に書き出します。

「ファイル>エクスポート>gltf2.0」を選択

「フォーマット」は「glTF Embedded(.gltf)」を選択し、あとはデフォルトの設定のままで進めます
これで3Dモデルの準備はOK!

webpack5で開発環境を整える

さくっと開発環境を紹介します
webpack5を使っていきます

新しいフォルダを作ってpackage.jsonを作成

npm init -y

必要なパッケージをインストール

npm install --save-dev @babel/preset-env autoprefixer babel-loader clean-webpack-plugin css-loader html-loader html-webpack-plugin image-webpack-loader mini-css-extract-plugin postcss postcss-loader pug-html-loader sass sass-loader style-loader three webpack webpack-cli webpack-dev-server

以下、ファイルの構成とディレクトリの構造です。

.
├── package-lock.json
├── package.json
├── src
│   ├── css
│   │   └── main.scss
│   ├── images
│   ├── js
│   │   └── index.js
│   ├── models
│   │   └── bitkun.gltf
│   └── templates
│       └── index.pug
└── webpack.config.js

webpack.config.jsを作成

const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: {
    'js/main': './src/js/index.js',
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js',
    publicPath: '/',
  },
  devServer: {
    static: {
      directory: path.resolve(__dirname, './dist'),
    },
    compress: true,
    port: 9000,
  },
  module: {
    rules: [
      {
        test: /\.js/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', { 'targets': '> 0.25%, not dead' }],
              ],
            },
          },
        ],
      },
      {
        test: /\.(css|sass|scss)/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
              importLoaders: 2,
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  ['autoprefixer', {grid: true}],
                ],
              },
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
      {
        test: /\.(glb|gltf)/,
        type: 'asset/resource',
        generator: {
          filename: 'models/[name][ext]',
        },
      },
      {
        test: /\.(png|jpe?g|gif)/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[name][ext]',
        },
        use: [
          {
            loader: 'image-webpack-loader',
          },
        ],
      },
      {
        test: /\.pug/,
        use: [
          {
            loader: 'html-loader',
          },
          {
            loader: 'pug-html-loader',
            options: {
              pretty: true,
            },
          },
        ]
      }
    ],
  },
  target: ['web', 'es5'],
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/main.css',
    }),
    new HtmlWebpackPlugin({
      template: './src/templates/index.pug',
      filename: 'index.html',
    }),
    new CleanWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin(),
  ],
}

まず、ちゃんとコンパイルされるのかテストしたいので
各ファイルに最低限必要なコードを書いていきます。

doctype html

html(lang="ja")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
    title ビットくん

  body
    canvas#canvas
import '../css/main.scss';
import bitkunModel from '../models/bitkun.gltf';

console.log('Hello, world!')
* {
  margin: 0;
  padding: 0;
}

Blenderから書き出した3Dデータはmodelsフォルダに配置します。

コンパイルの準備はできました!
試しに書き出してみます。

npx webpack

成功すると、以下のようなファイルがdistディレクトリに書き出されます。

dist
├── css
│   └── main.css
├── index.html
├── js
│   └── main.js
└── models
    └── bitkun.glb

シンプルですが必要なものが揃っています。
いい感じ!

以下はお好みですが、package.jsonにscriptsを追加しておきます。

"scripts": {
    "start": "webpack serve --mode=development",
    "prod": "webpack --mode=production",
    "dev": "webpack"
  }

開発中コマンド(ファイル変更の監視と書き出し)
ローカルサーバー(http://localhost:9000/)が起動します

npm start

開発用ビルド(非圧縮でファイル書き出し)

npm run dev

本番用ビルド(HTML・CSS・JSを圧縮してファイル書き出し)

npm run prod

準備完了です!

ちょっと長かったですが、環境は1度しっかり作っておけば理解も深まりますし、開発中は丁寧に作った恩恵を常に受けられるので時間が多少かかってでも納得のいく作りにするようにしています

Three.jsで3Dモデルを表示する

では、本題です

今回つくるものはこんな感じ

ビットくんのモデルを沢山クローンして立体空間に散りばめます

ブラウザ上で3Dグラフィックを扱うためのJSライブラリ「Three.js」を使います。Blenderを使わなくても、いろんな形がThree.js内には用意されているので、gltfデータを用意できない人はライブラリ内のジオメトリを使ってもいいかもしれません

まず、index.jsの全体のコードを書きます。続けて、ポイントを解説します

import '../css/main.scss';

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import bitkunModel from '../models/bitkun.gltf';
import helvetikerFontBold from 'three/examples/fonts/helvetiker_bold.typeface.json';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';

// Canvas
const canvasEl = document.getElementById('canvas');

let scene, camera, renderer, bitkun;
let sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
}

const bitkunArray = [];
const bitkunParams = {
  numberOfBitkun: 500,
}

// Event Listeners
window.addEventListener('load', initScene);
window.addEventListener('resize', onWindowResize);

/**
 * シーンの初期化
 * @returns {void}
 */
function initScene() {

  // Scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x00aad9);
  scene.fog = new THREE.Fog(0x00aad9, 10, 100);

  // Camera
  camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.1, 1000);
  camera.position.set(0, 0, 0).multiplyScalar(7);

  // Light
  // ambient light
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);

  // directional light
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  directionalLight.position.set(-5, 10, 10);
  scene.add(directionalLight);

  // Renderer
  renderer = new THREE.WebGLRenderer({
    alpha: true,
    antialias: true,
    canvas: canvasEl,
  });
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  // Controls
  let controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;

  // 3D Text
  textFunc('Bit', 0, 2, 0);
  textFunc('Beans', 0, -3, 5);

  // Bitkun
  createBitkunMesh(function () {
    for (let i = 0; i < bitkunParams.numberOfBitkun; i++) {
      bitkunArray.push(createBitkunClone());
    }

    // Controls
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    render();
  });

  animate();
}

/**
 * ビットくんのモデルを読み込む
 * @param {function} callback
 * @returns {void}
 */
function createBitkunMesh(callback) {
  const loader = new GLTFLoader();

  loader.load(
    bitkunModel,
    function(gltf) {
      bitkun = gltf.scene;
      bitkun.scale.set(20, 20, 20);

      if(typeof callback === 'function') {
        callback();
      }
    },
    function(xhr) {
      console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
    },
    function(error) {
      console.log('An error happened : ' + error);
    }
  )
}

/**
 * ビットくんのクローンを作成する
 * @returns {THREE.Mesh} - ビットくんのクローン
 */
function createBitkunClone() {
  if(bitkun) {
    const mesh = bitkun.clone();

    mesh.children.map((child) => {
      if(child.name === '円柱' || child.name === '円柱003') {
        child.material = new THREE.MeshToonMaterial({
          color: 0xffffff,
        });
      }else {
        child.material = new THREE.MeshToonMaterial({
          color: 0x00aad9,
        });
      }

    })

    mesh.position.x = Math.random() * 40 - 20;
    mesh.position.y = Math.random() * 40 - 20;
    mesh.position.z = Math.random() * 40 - 20;

    mesh.rotation.x = Math.random() * Math.PI;
    mesh.rotation.y = Math.random() * Math.PI;
    mesh.rotation.z = Math.random() * Math.PI;

    let scale = Math.random() * 0.15;
    mesh.scale.set(scale, scale, scale);
    scene.add(mesh);

    return mesh
  }
}

/**
 * ウィンドウのリサイズ時
 * @returns {void}
 */
function onWindowResize() {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}

/**
 * アニメーション
 * @returns {void}
 */
let deg = -60;
let fps = 100;

function animate() {
  setTimeout(() => {
    requestAnimationFrame(animate);

    deg += 0.01;

    camera.position.x = 20 * Math.sin((deg * Math.PI) / 180);
    camera.position.z = 20 * Math.cos((deg * Math.PI) / 180);

    // 原点方向を見つめる
    camera.lookAt(new THREE.Vector3(0, 0, 0));

    bitkunArray.forEach((bitkun, index) => {
      bitkun.rotation.x = performance.now() / 10000 + index;
      bitkun.rotation.y = performance.now() / 10000 + index;
      bitkun.rotation.z = performance.now() / 10000 + index;
    });

    render();
  }, 1000 / fps);
}

/**
 * レンダリング
 * @returns {void}
 */
function render() {
  renderer.render(scene, camera);
}

/**
 * テキストを作成する
 * @param {string} text - テキスト
 * @param {number} positionX - X座標
 * @param {number} positionY - Y座標
 * @param {number} positionZ - Z座標
 * @returns {void}
 */
function textFunc(text, positionX, positionY, positionZ) {

  const fontLoader = new FontLoader().parse(helvetikerFontBold);

  // TextGeometry
  const textGeometry = new TextGeometry(text, {
    font: fontLoader,
    size: 5,
    height: 1,
    curveSegments: 13,
    bevelEnabled: true,
    bevelThickness: 0.03,
    bevelSize: 0.02,
    bevelOffset: 0,
    bevelSegments: 5,
  });
  textGeometry.center();

  // Material
  const textMaterial = new THREE.MeshToonMaterial({
    color: 0xffffff,
  });

  // Mesh
  const textMesh = new THREE.Mesh(textGeometry, textMaterial);
  textMesh.position.set(positionX, positionY, positionZ);
  textMesh.rotation.y = - Math.PI * 0.25;
  scene.add(textMesh);

}

とりあえず、まるっとindex.jsにコピーしてみましょう

ブラウザに大量のビットくんが表示されたでしょうか

それではポイントを解説していきます

1. 必要なライブラリ、変数の用意

import '../css/main.scss';

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import bitkunModel from '../models/bitkun.gltf';
import helvetikerFontBold from 'three/examples/fonts/helvetiker_bold.typeface.json';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';

// Canvas
const canvasEl = document.getElementById('canvas');

let scene, camera, renderer, bitkun;
let sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
}

const bitkunArray = [];
const bitkunParams = {
  numberOfBitkun: 500,
}

// Event Listeners
window.addEventListener('load', initScene);
window.addEventListener('resize', onWindowResize);

まずは必要なライブラリをインポートしています。

  • three … Three.js本体
  • OrbitControls … Three.jsライブラリの一部です。マウス操作でカメラ視点を変更したり、スクロールでズームを調整できるので実装中は入れておいた方が便利です
  • GLTFLoader … これもThree.jsの一部ですが、3Dモデルを読み込むためのツールです

そして、HTMLのcanvas要素を取得しています。ここにThree.jsで書くシーンやカメラ、ライト、など様々な要素を追加していきます。もちろん今回使うビットくんのモデルもここに追加していきます

Blenderで作成した3Dモデルのデータをはじめ、フォントのデータもここで取得しておきましょう、まとめておくとデータを差し替えたりする時便利です

今回は読み込んだビットくんを沢山クローンして表示したいので、それらのデータを格納する配列「bitkunArray」と、ビットくんの数を調整するための連想配列「bitkunParams」を用意しています

bitkunParamsにはnumberOfBitkunしかないので変数に入れてもいいのですが、どんな動きにするのか、どんな並びにするのか、コントロールしやすいように連想配列でまとめているとパターンを作りやすくなります、先を見越して連想配列に格納しています

最後にイベントリスナーを用意して、初期化時とブラウザリサイズ時に発火するようにしています

2. シーンの初期化

/**
 * シーンの初期化
 * @returns {void}
 */
function initScene() {

  // Scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x00aad9);
  scene.fog = new THREE.Fog(0x00aad9, 10, 100);

  // Camera
  camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.1, 1000);
  camera.position.set(0, 0, 0).multiplyScalar(7);

  // Light
  // ambient light
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);

  // directional light
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  directionalLight.position.set(-5, 10, 10);
  scene.add(directionalLight);

  // Renderer
  renderer = new THREE.WebGLRenderer({
    alpha: true,
    antialias: true,
    canvas: canvasEl,
  });
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  // Controls
  let controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;

  // 3D Text
  textFunc('Bit', 0, 2, 0);
  textFunc('Beans', 0, -3, 5);

  // Bitkun
  createBitkunMesh(function () {
    for (let i = 0; i < bitkunParams.numberOfBitkun; i++) {
      bitkunArray.push(createBitkunClone());
    }

    // Controls
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    render();
  });

  animate();
}

大まかには以下のような内容です

  1. シーンを作成し、背景色を設定
  2. カメラを作成し、位置を設定
  3. ライトを作成し、シーンに追加
  4. レンダラーを作成し、キャンバスに追加
  5. OrbitControlsを作成し、カメラとレンダラーを設定
  6. テキストを作成し、シーンに追加
  7. ビットくんのモデルを読み込み、クローンを作成してシーンに追加
  8. レンダリングを開始

といった感じです。1〜5まではThree.jsを使う上では必須で使いまわせるものがほとんどです。

6以降は見せたい演出によって工夫していくところになります。

3. Blenderデータの読み込み

/**
 * ビットくんのモデルを読み込む
 * @param {function} callback
 * @returns {void}
 */
function createBitkunMesh(callback) {
  const loader = new GLTFLoader();

  loader.load(
    bitkunModel,
    function(gltf) {
      bitkun = gltf.scene;
      bitkun.scale.set(20, 20, 20);

      if(typeof callback === 'function') {
        callback();
      }
    },
    function(xhr) {
      console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
    },
    function(error) {
      console.log('An error happened : ' + error);
    }
  )
}

GLTFLoaderをつかって、あらかじめインポートしていたビットくんの3Dデータを読み込みます。3Dデータの読み込みを待って関数が実行されるようにcallback関数を使用し、bitkun変数にオブジェクトデータを格納します

ちょっと本題からずれますが、GLTFLoaderloadメソッドにはデータ読み込みのプログレスやエラーを返す関数も用意されているようです。VSCodeでマウスカーソルを合わせるだけでメソッドの引数に何を持っているのか読めるの便利ですね。

4. 3Dモデルの複製

/**
 * ビットくんのクローンを作成する
 * @returns {THREE.Mesh} - ビットくんのクローン
 */
function createBitkunClone() {
  if(bitkun) {
    const mesh = bitkun.clone();

    mesh.children.map((child) => {
      if(child.name === '円柱' || child.name === '円柱003') {
        child.material = new THREE.MeshToonMaterial({
          color: 0xffffff,
        });
      }else {
        child.material = new THREE.MeshToonMaterial({
          color: 0x00aad9,
        });
      }

    })

    mesh.position.x = Math.random() * 40 - 20;
    mesh.position.y = Math.random() * 40 - 20;
    mesh.position.z = Math.random() * 40 - 20;

    mesh.rotation.x = Math.random() * Math.PI;
    mesh.rotation.y = Math.random() * Math.PI;
    mesh.rotation.z = Math.random() * Math.PI;

    let scale = Math.random() * 0.15;
    mesh.scale.set(scale, scale, scale);
    scene.add(mesh);

    return mesh
  }
}

bitkun変数に格納されたデータを量産します。なぜかGLTFデータを読み込んだ時に目のところだけ真っ黒になってしまって不本意なのでここで調整しています

Blenderで作ったものを書き出した時にちょっと印象が違う、微調整したい、という時もデータの上書きは可能でした、なんでもできますね。ランダムな数値を使って画面全体にbitkunを配置して、大きさもある程度ランダムにしました

5. テキストを3Dで表示

青い空間内に青いビットくんがいて、大きさもランダム、配置もランダムにすると遠近感がよくわかりません。そこで中央にテキストを配置してみました。

/**
 * テキストを作成する
 * @param {string} text - テキスト
 * @param {number} positionX - X座標
 * @param {number} positionY - Y座標
 * @param {number} positionZ - Z座標
 * @returns {void}
 */
function textFunc(text, positionX, positionY, positionZ) {

  const fontLoader = new FontLoader().parse(helvetikerFontBold);

  // TextGeometry
  const textGeometry = new TextGeometry(text, {
    font: fontLoader,
    size: 5,
    height: 1,
    curveSegments: 13,
    bevelEnabled: true,
    bevelThickness: 0.03,
    bevelSize: 0.02,
    bevelOffset: 0,
    bevelSegments: 5,
  });
  textGeometry.center();

  // Material
  const textMaterial = new THREE.MeshToonMaterial({
    color: 0xffffff,
  });

  // Mesh
  const textMesh = new THREE.Mesh(textGeometry, textMaterial);
  textMesh.position.set(positionX, positionY, positionZ);
  textMesh.rotation.y = - Math.PI * 0.25;
  scene.add(textMesh);

}

テキストを3Dにしてシーンに追加する関数です、引数に表示したいテキストと表示位置を持たせてデザイン調整しやすくしています

他にも、演出面で変更を加えたい箇所は引数を持たせておくと柔軟性が生まれてくるので、変化を持たせたい部分や調整したい値は意識しておいた方が吉です

6. 動きをつける

/**
 * アニメーション
 * @returns {void}
 */
let deg = -60;
let fps = 100;

function animate() {
  setTimeout(() => {
    requestAnimationFrame(animate);

    deg += 0.01;

    camera.position.x = 20 * Math.sin((deg * Math.PI) / 180);
    camera.position.z = 20 * Math.cos((deg * Math.PI) / 180);

    // 原点方向を見つめる
    camera.lookAt(new THREE.Vector3(0, 0, 0));

    bitkunArray.forEach((bitkun, index) => {
      bitkun.rotation.x = performance.now() / 10000 + index;
      bitkun.rotation.y = performance.now() / 10000 + index;
      bitkun.rotation.z = performance.now() / 10000 + index;
    });

    render();
  }, 1000 / fps);
}

/**
 * レンダリング
 * @returns {void}
 */
function render() {
  renderer.render(scene, camera);
}

動きをつけます、一番楽しいところですね
いろいろ試してみたくなります

requestAnimationFrame関数はブラウザのフレームレートに合わせて実行される関数ですが、処理の内容や環境によってはあえてfpsをおさえたい時もあります

setTimeout関数を使ってFPSを調整できるようにし、分岐もできるように変数に格納して外に出しています

7. レスポンシブ対応

/**
 * ウィンドウのリサイズ時
 * @returns {void}
 */
function onWindowResize() {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}

3Dモデルをレスポンシブに対応させるのはなかなか骨が折れる作業です。今回は基本的なリサイズする要素を入れ込んでいます

おまけ:ビットくん、何体だせるかな?

最後にパフォーマンスについて確認してみます

Three.jsのアドオンにパフォーマンスをモニターするツール「stats.js」があるので使ってみましょう

import Stats from 'three/examples/jsm/libs/stats.module.js';

/**
 * レンダリング
 * @returns {void}
 */
function render() {
  stats.update(); // 追記
  renderer.render(scene, camera);
}

//-- ここから追記
// Stats
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
//-- ここまで追記

プログラムの先頭でstats.jsをimportして上記コードのように情報ボックスを埋め込んでみます

すると、画面左上に情報エリアが表示されました
詳しい内容についてはstats.jsのreadmeを確認して欲しいですが、FPSに関して言えば60FPSでていれば滑らかにアニメーションしていると言えるでしょう

試してみます

まずは50体

全然いけます、どんどん行きましょう
次は500体

意外といけます、思いっきり増やしてみましょう
1500体

49FPSまで落ちました
キービジュアルだと気になるレベルですが、背景など目立たないところでは使えそうです
倍の3000体にしてみます

かなりカクカクになりました
Three.jsはWebGLを比較的簡単に使用するためのライブラリなので、大量のオブジェクトを動かすためには工夫が必要です
どうすればもっと沢山のビットくんをアニメーションさせられるのか、、それはまた次回にしたいと思います

まとめ

お疲れ様でした!

ちょっと長かったですが、Three.jsは比較的手軽に3DとWebGLの内容に触れることができるライブラリです。長々と書いたコードも「必須のもの」と「表現によって工夫するもの」の2つに大きく分類できるので、要領が掴めてくるとじっくり読み込むべき箇所が浮き彫りになってきます

あと、「表現によって工夫するところ」も、狙った通りの表現・演出になった時の喜びはひとしおだと感じます

とにかく遊びつつ楽しみながら試してみることが理解への一歩になるのかなと思っています

ガジェット小僧(エンジニア)

Takashi Yamaguchi

最近パパになるも「ガジェット小僧」の異名を持つ美術系大学出身のエンジニア。ランチはなるべく近場で済ませたい。2ブロック離れるともう不安。

日々更新中! Bit Beans、
Xもやってます!
  1. TOP
  2. ブログ
  3. 【Three.js × Blender × webpack5】ビットくんで遊びたい
Bit Beansキャラクター紹介