🐶

WebGLでフレーミーを描こう!

2022/09/23に公開

はじめに

WebGLの勉強がてらフレーミーを描いてみました
フレーミーとはNHK教育のピタゴラスイッチに登場する犬のキャラクターです
骨が好物で掃除が苦手
https://dic.pixiv.net/a/フレーミー
※ピクシブ百科事典参考

開発環境

Nuxt3 + WebGL API

完成物

ソースコード

pagesファイル

pages/framy.vue
<script setup lang="ts">
import { Ref } from 'vue'
const canvas: Ref<HTMLCanvasElement> = ref()
useFramy(canvas)
</script>

<template>
  <div>
    <canvas ref="canvas"></canvas>
  </div>
</template>

composablesファイル

composables/framy.ts
import { Ref } from 'vue'
import { createProgramFromCode } from './webgl'
import { VSHADER_CODE } from './shader/framy-v-shader'
import { FSHADER_CODE } from './shader/framy-f-shader'

export const useFramy = (canvas: Ref<HTMLCanvasElement>) => {
  onMounted(() => {
    if (!(canvas.value instanceof HTMLCanvasElement)) {
      throw new Error('canvas要素がありません')
    }
    canvas.value.width = 512
    canvas.value.height = 512
    const gl = canvas.value.getContext('webgl2')
    if (!(gl instanceof WebGL2RenderingContext)) {
      throw new Error('WebGLの初期化に失敗しました')
    }
    // GLSLプログラムをGPUにアップロード
    const program = createProgramFromCode(gl, VSHADER_CODE, FSHADER_CODE)
    // 作成したプログラムを設定する
    gl.useProgram(program)
    // 頂点バッファ
    const positionBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
    const index = gl.getAttribLocation(program, 'a_position')
    const size = 2
    const type = gl.FLOAT
    const normalized = false
    const stride = 0
    const offset = 0
    gl.vertexAttribPointer(index, size, type, normalized, stride, offset)
    gl.enableVertexAttribArray(index)
    gl.clearColor(0, 0, .0, .0)
    gl.clear(gl.COLOR_BUFFER_BIT)

    const oneEight = 0.125

    // 四角を描画
    const drawRectangle = (pos) => {
      pos = pos.map(index => index * oneEight)
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos), gl.STATIC_DRAW)
      gl.drawArrays(gl.LINE_LOOP, 0, pos.length / 2)
    }
    // 塗りつぶしの四角を描画
    const fillRectangle = (pos) => {
      pos = pos.map(index => index * oneEight)
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos), gl.STATIC_DRAW)
      gl.drawArrays(gl.TRIANGLES, 0, pos.length / 2)
    }
    // 点を描画
    const drawPoints = (pos) => {
      pos = pos.map(index => index * oneEight)
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos), gl.STATIC_DRAW)
      gl.drawArrays(gl.POINTS, 0, 2)
    }

    // 頭
    const headPosition = [
      -5 , 5 ,
      -1, 5,
      -1, 1,
      -5, 1
    ]
    drawRectangle(headPosition)
    // 体
    const bodyPosition = [
      -1.5, 2,
      6.5, 2,
      6.5, -2,
      -1.5, -2
    ]
    drawRectangle(bodyPosition)
    // 口
    const mouthPosition = [
      -7, 2.5,
      -4, 2.5,
      -4, 1,
      -7, 1
    ]
    drawRectangle(mouthPosition)
    // 右前足
    const rightFrontLegPosition = [
      -1, -2,
      0, -2,
      0, -5,
      -1, -5
    ]
    drawRectangle(rightFrontLegPosition)
    // 左前足
    const leftFrontLegPosition = [
      0.5, -2,
      1.5, -2,
      1.5, -5,
      0.5, -5
    ]
    drawRectangle(leftFrontLegPosition)
    // 右後ろ足
    const rightBackLegPosition = [
      3.5, -2,
      4.5, -2,
      4.5, -5,
      3.5, -5
    ]
    drawRectangle(rightBackLegPosition)
    // 左後ろ足
    const leftBackLegPosition = [
      5, -2,
      6, -2,
      6, -5,
      5, -5
    ]
    drawRectangle(leftBackLegPosition)
    // 右耳
    const rightEarPosition = [
      -6, 6.5,
      -4, 6.5,
      -4, 4,
      -6, 4
    ]
    drawRectangle(rightEarPosition)
    // 左耳
    const leftEarPosition = [
      -2, 6.5,
      .5, 6.5,
      .5, 4,
      -2, 4
    ]
    drawRectangle(leftEarPosition)
    // 尻尾
    const tailPosition = [
      5.25, 1.75,
      7.25, 2.75,
      7.75, 2,
      5.75, 1
    ]
    drawRectangle(tailPosition)
    // 鼻
    const nousePosition = [
      -7, 2.5,
      -6, 2.5,
      -7, 2,
      -7, 2,
      -6, 2.5,
      -6, 2,
    ]
    fillRectangle(nousePosition)
    // 目
    const eyesPosition = [
      -4, 3,
      -2.5, 3,
    ]
    drawPoints(eyesPosition)
  })
}

頂点シェーダー

composables/shader/framy-v-shader.ts
export const VSHADER_CODE = `
  attribute vec4 a_position;

  void main() {
    gl_Position = a_position;
    gl_PointSize = 7.0;
  }
`

フラグメントシェーダー

composables/shader/framy-f-shader.ts
export const FSHADER_CODE = `
  void main() {
    gl_FragColor = vec4(.0, .0, .0, 1.);
  }
`

WebGL 共通処理

composables/webgl.ts
/**
 * createShader
 * 
 * @param gl WebGL コンテキスト
 * @param type gl.VERTEX_SAHDER あるいは gl.FRAGMENT_SHADER
 * @param source シェーダーのソースコード
 */
const createShader = (gl: WebGL2RenderingContext, type: number, source: string) => {
  const shader = gl.createShader(type)
  if (shader === null) {
    console.error('Faild to create a shader')
    return null
  }
  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  // check compile result
  const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
  if (!compiled) {
    const log = gl.getShaderInfoLog(shader)
    console.error('Faild to compile a shader\n' + log)
    gl.deleteShader(shader)
    return null
  }
  return shader
}

/**
 * createProgram
 * 
 * @param gl WebGL コンテキスト
 * @param vshader 頂点シェーダー
 * @param fshader フラグメントシェーダー
 */
const createProgram = (gl: WebGL2RenderingContext, vshader: WebGLShader, fshader: WebGLShader) => {
  const program = gl.createProgram()
  if (!program) {
    return null
  }
  gl.attachShader(program, vshader)
  gl.deleteShader(vshader)
  gl.attachShader(program, fshader)
  gl.deleteShader(fshader)
  gl.linkProgram(program)

  // check link error
  const linked = gl.getProgramParameter(program, gl.LINK_STATUS)
  if (!linked) {
    const log = gl.getProgramInfoLog(program)
    console.error('Faild to link a program\n' + log)
    gl.deleteProgram(program)
    return null
  }
  return program
}

/**
 * createProgramFromCode
 * 
 * @param gl WebGL コンテキスト
 * @param vshaderCode 頂点シェーダーソース
 * @param fshaderCode フラグメントシェーダーソース
 */
export const createProgramFromCode = (gl: WebGL2RenderingContext, vshaderCode: string, fshaderCode: string) => {
  const vshader = createShader(gl, gl.VERTEX_SHADER, vshaderCode)
  if (!vshader) {
    console.log('hoge')
    return null
  }
  const fshader = createShader(gl, gl.FRAGMENT_SHADER, fshaderCode)
  if (!fshader) {
    console.log('hoge2')
    gl.deleteShader(vshader)
    return null
  }
  return createProgram(gl, vshader, fshader)
}

おわりに

フレーミーを描くことによってWebGLの2D描画について少し詳しくなった。
ありがとうフレーミー

参考文献

https://webglfundamentals.org/webgl/lessons/ja/
https://wgld.org/
この2つのサイトの記事を読み漁りました

Discussion