2019年04月27日

wxLuaApp で OpenGL やってみた

 球体の表面を色分けした図を描いて GIF アニメーションにする、という案件が発生した。こういうやつです。

20190427-1.gif 20190427-2.gif

 OpenGL を使えばできそう、というのはすぐに思いついたのだが、どう実装するかでしばし悩んだ。最近の流行りで言えば WebGL なんだろうけど、少し調べてみてハードルが高そうと感じた。自分の OpenGL の知識が相当古いところで止まっているのが原因かもしれず、本当はそちらをアップデートするのが正攻法なのだが、今回はとにかく早く結果が欲しい。

 そこで、Molby のコードを流用することにした。さすがに C で書くのはどうかと思ったので、最近のマイブームである wxLuaApp で OpenGL に挑戦。実はこれが結構大変でありまして、WebGL を使ったほうが未来につながったかもと 15% ぐらいは思ったりしている。

sinCache = {}
sinCacheSect = 0
idle_count = 0
phase = 0
mode = 0
cflag = false
modefunc = function () return 1 end

function OnIdle(event, canvas)
  local max_count = 32
  if mode < 9 and idle_count < max_count then
    if idle_count == 0 then
      if mode == 0 then
        modefunc = function (x, y, z) return 1 end
      elseif mode == 1 then
        modefunc = function (x, y, z) return x end
      elseif mode == 2 then
        modefunc = function (x, y, z) return -y end
      elseif mode == 3 then
        modefunc = function (x, y, z) return z end
      elseif mode == 4 then
        modefunc = function (x, y, z) return 2 * z * z - x * x - y * y end
      elseif mode == 5 then
        modefunc = function (x, y, z) return 2 * z * x end
      elseif mode == 6 then
        modefunc = function (x, y, z) return -2 * y * z end
      elseif mode == 7 then
        modefunc = function (x, y, z) return x * x - y * y end
      elseif mode == 8 then
        modefunc = function (x, y, z) return -2 * x * y end
      end
    end
    CaptureCanvas(idle_count, max_count, mode, cflag)
    if cflag then
      idle_count = idle_count + 1
      if idle_count == max_count then
        idle_count = 0
        mode = mode + 1
      end
    end
    cflag = not cflag
  end
end

function ColoredVertex(c, r, x, y, z)
  local rgba
  local y2 = modefunc(x, y, z) * math.cos(phase)
  if y2 >= 0 then
    rgba = {(1 - y2) * 0.8, (1 - y2) * 0.8, 1 - (1 - y2) * 0.2, 1}
  else
    rgba = {1 - (1 + y2) * 0.2, (1 + y2) * 0.8, (1 + y2) * 0.8, 1}
  end
  gl.Material(gl.FRONT_AND_BACK, gl.DIFFUSE, rgba);
  gl.Vertex(r * x + c[1], r * y + c[2], r * z + c[3])
end

function DrawSphere(c, r, sect)
  local m = math.floor(sect / 4)
  local n = m * 4
  if sinCacheSect ~= n then
    sinCacheSect = n
    for i = 1, m * 5 + 2 do
      sinCache[i] = math.sin(math.pi * 2 / n * (i - 1))
    end
  end
  local s = sinCache
  for i = 1, n + 1 do
    gl.Begin(gl.QUAD_STRIP)
    for j = 1, n / 2 do
      gl.Normal(s[j] * s[i + m], s[j] * s[i], s[j + m])
      ColoredVertex(c, r, s[j] * s[i + m], s[j] * s[i], s[j + m])
      gl.Normal(s[j] * s[i + 1 + m], s[j] * s[i + 1], s[j + m])
      ColoredVertex(c, r, s[j] * s[i + 1 + m], s[j] * s[i + 1], s[j + m])
    end
    gl.End()
  end
  gl.Begin(gl.TRIANGLE_FAN)
  gl.Normal(0, 0, 1)
  ColoredVertex(c, r, 0, 0, 1)
  for i = n + 1, 1, -1 do
    gl.Normal(s[2] * s[i + m], s[2] * s[i], s[2 + m])
    ColoredVertex(c, r, s[2] * s[i + m], s[2] * s[i], s[2 + m])
  end
  gl.End()
  gl.Begin(gl.TRIANGLE_FAN)
  gl.Normal(0, 0, -1)
  ColoredVertex(c, r, 0, 0, -1)
  for i = 1, n + 1 do
    gl.Normal(s[2] * s[i + m], s[2] * s[i], -s[2 + m])
    ColoredVertex(c, r, s[2] * s[i + m], s[2] * s[i], -s[2 + m])
  end
  gl.End()
end

function DrawModel()
  gl.MatrixMode(gl.MODELVIEW)
  gl.LoadIdentity()
  gl.Translate(0, 0, -1)
  gl.Rotate(130, 1, 0, 0)
  gl.Rotate(-45, 0, 0, 1)
  gl.Material(gl.FRONT_AND_BACK, gl.DIFFUSE, {1, 0, 1, 1});
  gl.Enable(gl.NORMALIZE)
  DrawSphere({0, 0, 0}, 0.85, 48)
end

function CaptureCanvas(n, m, mode, cflag)
  phase = 3.1415926 * 2 * n / m
  canvas:Refresh()
  canvas:Update()
  if not cflag then return end
  canvas.context:SetCurrent(canvas)
  capture = gl.ReadPixels(0, 0, 120, 120, gl.RGBA)
  data = {}
  alpha = {}
  for i = 1, #capture do
    capture[i] = math.floor(capture[i] * 255 + 0.5)
  end
  local j = 0
  for y = 1, 120 do
    for x = 1, 120 do
      local i = ((120 - y) * 120 + x - 1) * 4 + 1
      local j = (y - 1) * 120 + x
      data[j * 3 + 1] = capture[i]
      data[j * 3 + 2] = capture[i + 1]
      data[j * 3 + 3] = capture[i + 2]
      alpha[j + 1] = capture[i + 3]
    end
  end
  image = wx.wxImage(120, 120)
  datastr = string.fromtable(data)
  alphastr = string.fromtable(alpha)
  image:SetData(datastr)
  image:SetAlpha(alphastr)
  image:SaveFile(string.format("sample%d_%02d.png", mode, n))
end

function OnPaint(event, canvas)
  local dc = wx.wxPaintDC(canvas)
  if not canvas.is_initialized then
    canvas.context = wx.wxGLContext(canvas)
    canvas.context:SetCurrent(canvas)
    sz = canvas:GetClientSize()
    gl.Viewport(0, 0, sz.x, sz.y)
    gl.Enable(gl.DEPTH_TEST)
    gl.Enable(gl.LIGHTING)
    gl.LightModel(gl.LIGHT_MODEL_AMBIENT, {0.8, 0.8, 0.8, 1.0})
    gl.Light(gl.LIGHT0, gl.DIFFUSE, {1, 1, 1, 1})
    gl.Enable(gl.LIGHT0)
    gl.Light(gl.LIGHT1, gl.AMBIENT, {0.4, 0.4, 0.4, 1})
    gl.Enable(gl.LIGHT1)
    gl.Enable(gl.BLEND)
    gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
    canvas.is_initialized = true
  end
  canvas.context:SetCurrent(canvas)
  gl.ClearColor(0, 0, 0, 0)
  gl.Clear(bit.bor(gl.COLOR_BUFFER_BIT, gl.DEPTH_BUFFER_BIT))
  DrawModel()
  gl.Flush()
  canvas:SwapBuffers()
  dc:delete()
end

function main()
  --  フレームを作成
  frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "", wx.wxPoint(50, 50), wx.wxSize(120, 200))
  frame:SetClientSize(120, 120)
  --  GLCanvas を作成
  canvas = wx.wxGLCanvas(frame, wx.wxID_ANY, {}, wx.wxDefaultPosition, wx.wxSize(120, 120), 0, "GLCanvas")
  canvas.is_initialized = false
  --  イベントを接続
  canvas:Connect(wx.wxEVT_PAINT, function (event) OnPaint(event, canvas) end)
  canvas:Connect(wx.wxEVT_IDLE, function (event) OnIdle(event, canvas) end)
  frame:Show(true)
end

main()
Posted at 2019年04月27日 00:55:20
email.png