From e656f549ff5e4af09959022426b8831af9607d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Mon, 2 Jul 2018 22:49:43 +0200 Subject: [PATCH] Initial text support implementation, pt. III. Generate TTF texture ad hoc. --- ENNTool.py | 13 ++-- ENNToolGLCanvasPanel.py | 74 ++++++--------------- ENNToolGLTTFTexture.py | 142 ++++++++++++++++++++++++++++++++++++++++ ENNToolTTFExporter.py | 78 ---------------------- README.md | 2 +- assets/texture.png | 1 - assets/textures.yaml | 1 - 7 files changed, 170 insertions(+), 141 deletions(-) create mode 100644 ENNToolGLTTFTexture.py delete mode 100755 ENNToolTTFExporter.py delete mode 120000 assets/texture.png delete mode 100644 assets/textures.yaml diff --git a/ENNTool.py b/ENNTool.py index 3ea9d42..926376e 100755 --- a/ENNTool.py +++ b/ENNTool.py @@ -8,10 +8,12 @@ # 1) -A: render frame #1, render frame #2, ... # 2) -o: support at least {GIF,MP4,WEBM} # 3) -s: effects: rotate, smash into bricks, swirl, wave, ... -# 4) XXX autodetect video width from widest mircart -# 5) XXX convert TTF to texture PNG & coords TXT, render accordingly -# 6) XXX dont stall GPU w/ glReadPixels(), switch to asynchronous model w/ FBO or PBO (http://www.songho.ca/opengl/gl_fbo.html, http://www.songho.ca/opengl/gl_pbo.html) -# 7) XXX render mircart as 3D blocks vs flat surface +# 4) Feature: include ETA @ progress bar +# 5) Feature: autodetect video width from widest mircart +# 6) Feature: render mircart as 3D blocks vs flat surface +# 7) Optimisation: dont stall GPU w/ glReadPixels(), switch to asynchronous model w/ FBO or PBO (http://www.songho.ca/opengl/gl_fbo.html, http://www.songho.ca/opengl/gl_pbo.html) +# 8) OpenGL: use VAOs + glVertexAttribFormat + glVertexAttribBinding +# 9) OpenGL: use glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) & multiply texel * bgcolour (https://learnopengl.com/Advanced-OpenGL/Blending) # from getopt import getopt, GetoptError @@ -21,6 +23,7 @@ import os, sys, time import wx from ENNToolGLCanvasPanel import ENNToolGLCanvasPanel +from ENNToolGLTTFTexture import ENNToolGLTTFTexture from ENNToolMiRCARTImporter import ENNToolMiRCARTImporter class ENNToolApp(object): @@ -91,7 +94,7 @@ class ENNToolApp(object): MiRCART += ENNToolMiRCARTImporter(inFile).outMap curY, rotateX, rotateY, translateY = 0, 0, 0, scrollRate - artTextureId, artInfo = panelGLCanvas.initTexture(texturePathName) + artTextureId, artInfo = ENNToolGLTTFTexture(MiRCART, optdict["-R"], optdict["-r"]).getParams() artVbo, artVboLen, lastY, numVertices = panelGLCanvas.renderMiRCART(artInfo, MiRCART, cubeSize=optdict["-R"]) if "-v" in optdict: print("{} vertices".format(numVertices)) diff --git a/ENNToolGLCanvasPanel.py b/ENNToolGLCanvasPanel.py index 55c0234..b7640cb 100644 --- a/ENNToolGLCanvasPanel.py +++ b/ENNToolGLCanvasPanel.py @@ -10,18 +10,14 @@ # Wed, 27 Jun 2018 16:02:12 +0200 [3] # Wed, 27 Jun 2018 16:02:13 +0200 [4] # Wed, 27 Jun 2018 16:02:14 +0200 [5] -# Thu, 28 Jun 2018 17:03:16 +0200 [6] -# Thu, 28 Jun 2018 17:04:59 +0200 [7] -# Thu, 28 Jun 2018 18:32:50 +0200 [8] +# Thu, 28 Jun 2018 18:32:50 +0200 [6] # from OpenGL.GL import * from OpenGL.GL import shaders -from PIL import Image import cv2, numpy import ctypes, os, sys, time import wx, wx.glcanvas -import yaml from ENNToolMiRCARTColours import ENNToolMiRCARTColoursFloat @@ -53,11 +49,7 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): void main() { vec4 texel = texture2D(texture, fgTexCoord); - if (texel.a > 0.0) { - gl_FragColor = vec4(texel.r, texel.g, texel.b, 1.0); - } else { - gl_FragColor = bgColour; - } + gl_FragColor = vec4(texel.r, texel.g, texel.b, 1.0); } """, GL_FRAGMENT_SHADER) vs = shaders.compileShader(""" @@ -82,37 +74,12 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): """, GL_VERTEX_SHADER) self.shader = shaders.compileProgram(vs, fs) # }}} - # {{{ initTexture(self, pathName, infoPathName=os.path.join("assets", "textures.yaml")): XXX - def initTexture(self, pathName, infoPathName=os.path.join("assets", "textures.yaml")): - with open(infoPathName, "r") as fileObject: - artInfo = yaml.load(fileObject) - - # [6], [7] - artTextureId = glGenTextures(1) - artTextureImage = Image.open(pathName).transpose(Image.FLIP_TOP_BOTTOM) - artTextureImageData = numpy.array(artTextureImage) - - glBindTexture(GL_TEXTURE_2D, artTextureId) - glPixelStorei(GL_UNPACK_ALIGNMENT, 1) - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) - - # [2] - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) - - # [4][5] - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, - artTextureImage.size[0], artTextureImage.size[1], - 0, GL_RGBA, GL_UNSIGNED_BYTE, artTextureImageData) - glBindTexture(GL_TEXTURE_2D, artTextureId) - return artTextureId, artInfo - # }}} # {{{ initVideoWriter(self): XXX def initVideoWriter(self, fourcc="XVID", fps=25): fourcc = cv2.VideoWriter_fourcc(*list(fourcc)) self.videoWriter = cv2.VideoWriter(self.videoPath, fourcc, fps, (self.width, self.height), True) # }}} + # {{{ renderFrame(self, artTextureId, artVbo, artVboLen): XXX def renderFrame(self, artTextureId, artVbo, artVboLen): glEnableClientState(GL_VERTEX_ARRAY) @@ -136,7 +103,7 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): glUniformMatrix4fv(glGetUniformLocation(self.shader, "projection"), 1, GL_FALSE, projection) glUniform1i(glGetUniformLocation(self.shader, "texture"), 0) - # [8] + # [6] glEnableVertexAttribArray(0) glVertexAttribPointer(0, 3, GL_FLOAT, False, 48, ctypes.c_void_p(0)) glEnableVertexAttribArray(1) @@ -168,43 +135,39 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): if centre and (len(artMap[numRow]) < canvasCols): curPos[0] += (((canvasCols - len(artMap[numRow])) * cubeSize[0]) / 2) for numCol in range(len(artMap[numRow])): - cubeColour = [*ENNToolMiRCARTColoursFloat[artMap[numRow][numCol][1]], 1.0] - if artMap[numRow][numCol][0] != artMap[numRow][numCol][1]: - colColour = artMap[numRow][numCol][0] - cubeChar = artMap[numRow][numCol][3] - if ord(cubeChar) >= 128: - print("dont have {}".format(cubeChar)) - cubeChar = " " - else: - colColour = artMap[numRow][numCol][1] - cubeChar = " " + cubeFg = artMap[numRow][numCol][0] + cubeBg = artMap[numRow][numCol][1] + cubeBgFloat = [*ENNToolMiRCARTColoursFloat[cubeBg], 1.0] + cubeAttrs = artMap[numRow][numCol][2] + cubeChar = artMap[numRow][numCol][3] + artCell = artInfo[cubeFg][cubeBg][cubeAttrs][cubeChar] # Top Right vertices += curPos vertices += [0.0, 0.0, 1.0] - vertices += cubeColour - vertices += [float(((ord(cubeChar) + 1) * artInfo["rowWidth"]) / artInfo["texWidth"]), ((colColour + 1) * artInfo["rowHeight"]) / float(artInfo["texHeight"])] + vertices += cubeBgFloat + vertices += artCell[0:2] numVertices += 1 # Top Left vertices += [curPos[0]-cubeSize[0], curPos[1], curPos[2]] vertices += [0.0, 0.0, 1.0] - vertices += cubeColour - vertices += [float(((ord(cubeChar) + 0) * artInfo["rowWidth"]) / artInfo["texWidth"]), ((colColour + 1) * artInfo["rowHeight"]) / float(artInfo["texHeight"])] + vertices += cubeBgFloat + vertices += artCell[2:4] numVertices += 1 # Bottom Left vertices += [curPos[0]-cubeSize[0], curPos[1]-cubeSize[1], curPos[2]] vertices += [0.0, 0.0, 1.0] - vertices += cubeColour - vertices += [float(((ord(cubeChar) + 0) * artInfo["rowWidth"]) / artInfo["texWidth"]), ((colColour) * artInfo["rowHeight"]) / float(artInfo["texHeight"])] + vertices += cubeBgFloat + vertices += artCell[4:6] numVertices += 1 # Bottom Right vertices += [curPos[0], curPos[1]-cubeSize[1], curPos[2]] vertices += [0.0, 0.0, 1.0] - vertices += cubeColour - vertices += [float(((ord(cubeChar) + 1) * artInfo["rowWidth"]) / artInfo["texWidth"]), ((colColour) * artInfo["rowHeight"]) / float(artInfo["texHeight"])] + vertices += cubeBgFloat + vertices += artCell[6:8] numVertices += 1 curPos[0] += cubeSize[0] @@ -226,6 +189,7 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): screenshot = numpy.flipud(numpy.frombuffer(screenshot, numpy.uint8).reshape((self.height, self.width, 3))) self.videoWriter.write(screenshot) # }}} + # {{{ __init__(self, parent, size, defaultPos=(24,24), videoPath=None): initialisation method def __init__(self, parent, size, defaultPos=(24,24), videoPath=None): super().__init__(parent, pos=defaultPos, size=size) diff --git a/ENNToolGLTTFTexture.py b/ENNToolGLTTFTexture.py new file mode 100644 index 0000000..d44c301 --- /dev/null +++ b/ENNToolGLTTFTexture.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) +# Copyright (c) 2018 Lucio Andrés Illanes Albornoz +# This project is licensed under the terms of the MIT license. +# +# References: +# Thu, 28 Jun 2018 17:03:16 +0200 [1] +# Thu, 28 Jun 2018 17:04:59 +0200 [2] +# Wed, 27 Jun 2018 16:02:12 +0200 [3] +# Wed, 27 Jun 2018 16:02:13 +0200 [4] +# Wed, 27 Jun 2018 16:02:14 +0200 [5] +# + +from collections import defaultdict +from OpenGL.GL import * +from PIL import Image, ImageDraw, ImageFont +import numpy +import os, string, sys + +from ENNToolMiRCARTColours import ENNToolMiRCARTColours +from ENNToolMiRCARTImporter import ENNToolMiRCARTImporter + +class ENNToolGLTTFTexture(object): + """XXX""" + + # {{{ _defaultDict(*args): XXX + @staticmethod + def _defaultDict(*args): + return defaultdict(*args) + # }}} + # {{{ _nestedDict(): XXX + @staticmethod + def _nestedDict(): + return defaultdict(ENNToolGLTTFTexture._nestedDict) + # }}} + + # {{{ _drawCharList(self, artInfo, charList, pilFontBold, pilFontNormal, pilFontSize, pilImageDraw, pilImageSize): XXX + def _drawCharList(self, artInfo, charList, pilFontBold, pilFontNormal, pilFontSize, pilImageDraw, pilImageSize): + curPos = [0, 0] + for newChar in charList: + pilFont, underLine = pilFontNormal, False + if newChar[2] & ENNToolMiRCARTImporter._CellState.CS_BOLD: + pilFont = pilFontBold + if newChar[2] & ENNToolMiRCARTImporter._CellState.CS_UNDERLINE: + underLine = True + pilImageDraw.rectangle((*curPos, curPos[0] + pilFontSize[0], curPos[1] + pilFontSize[1] - 1), + fill=(*ENNToolMiRCARTColours[newChar[1]], 255)) + pilImageDraw.text(curPos, newChar[3], (*ENNToolMiRCARTColours[newChar[0]], 255), pilFont) + if underLine: + pilImageDraw.line( + xy=(curPos[0], curPos[1] + (pilFontSize[1] - 2), + curPos[0] + pilFontSize[0], curPos[1] + pilFontSize[1]), + fill=(*ENNToolMiRCARTColours[newChar[0]], 255)) + + artInfo[newChar[0]][newChar[1]][newChar[2]][newChar[3]] = [] + # Top Right + artInfo[newChar[0]][newChar[1]][newChar[2]][newChar[3]] += [float(curPos[0] + pilFontSize[0]) / pilImageSize[0], 0.0] + # Top Left + artInfo[newChar[0]][newChar[1]][newChar[2]][newChar[3]] += [float(curPos[0]) / pilImageSize[0], 0.0] + # Bottom Left + artInfo[newChar[0]][newChar[1]][newChar[2]][newChar[3]] += [float(curPos[0]) / pilImageSize[0], float(pilFontSize[1]) / pilImageSize[1]] + # Bottom Right + artInfo[newChar[0]][newChar[1]][newChar[2]][newChar[3]] += [float(curPos[0] + pilFontSize[0]) / pilImageSize[0], float(pilFontSize[1]) / pilImageSize[1]] + + curPos[0] += pilFontSize[0] + return artInfo + # }}} + # {{{ _initArtInfoCharList(self): XXX + def _initArtInfoCharList(self, artMap): + artInfo, charList = ENNToolGLTTFTexture._nestedDict(), [] + for numRow in range(len(artMap)): + for numCol in range(len(artMap[numRow])): + artFg = artMap[numRow][numCol][0] + artBg = artMap[numRow][numCol][1] + artAttrs = artMap[numRow][numCol][2] + artChar = artMap[numRow][numCol][3] + if artInfo[artFg][artBg][artAttrs][artChar] == {}: + artInfo[artFg][artBg][artAttrs][artChar] = None + charList += [[artFg, artBg, artAttrs, artChar]] + return artInfo, charList + # }}} + # {{{ _initFonts(self): XXX + def _initFonts(self): + fontBoldPathName = os.path.join("assets", "DejaVuSansMono-Bold.ttf") + fontNormalPathName = os.path.join("assets", "DejaVuSansMono.ttf") + fontSize = int("26") + pilFontBold = ImageFont.truetype(fontBoldPathName, fontSize) + pilFontNormal = ImageFont.truetype(fontNormalPathName, fontSize) + pilFontSize = list(pilFontNormal.getsize("_")) # XXX + return pilFontBold, pilFontNormal, pilFontSize + # }}} + # {{{ _initImage(self, charList, pilFontSize): XXX + def _initImage(self, charList, pilFontSize): + pilImageSize = (pilFontSize[0] * len(charList), pilFontSize[1]) + pilImage = Image.new("RGBA", pilImageSize, (0, 0, 0, 0)) + pilImageDraw = ImageDraw.Draw(pilImage) + return pilImage, pilImageDraw, pilImageSize + # }}} + # {{{ _initTexture(self, pilImage): XXX + def _initTexture(self, pilImage): + # [1], [2] + artTextureId = glGenTextures(1) + artTextureImage = pilImage + artTextureImageData = numpy.array(artTextureImage) + + glBindTexture(GL_TEXTURE_2D, artTextureId) + glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) + + # [3] + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + + # [4][5] + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + artTextureImage.size[0], artTextureImage.size[1], + 0, GL_RGBA, GL_UNSIGNED_BYTE, artTextureImageData) + glGenerateMipmap(GL_TEXTURE_2D) + + return artTextureId + # }}} + + # {{{ getParams(self): XXX + def getParams(self): + return self.artTextureId, self.artInfo + # }}} + # {{{ __init__(self): initialisation method + def __init__(self, artMap, cubeSize, videoSize): + artInfo, charList = self._initArtInfoCharList(artMap) + pilFontBold, pilFontNormal, pilFontSize = self._initFonts() + pilImage, pilImageDraw, pilImageSize = self._initImage(charList, pilFontSize) + artInfo = self._drawCharList(artInfo, charList, + pilFontBold, pilFontNormal, pilFontSize, + pilImageDraw, pilImageSize) + artTextureId = self._initTexture(pilImage) + self.artTextureId = artTextureId + self.artInfo = artInfo + # }}} + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/ENNToolTTFExporter.py b/ENNToolTTFExporter.py deleted file mode 100755 index 4f1ae3b..0000000 --- a/ENNToolTTFExporter.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -# ENNTool -- mIRC art animation tool (for EFnet #MiRCART) (WIP) -# Copyright (c) 2018 Lucio Andrés Illanes Albornoz -# This project is licensed under the terms of the MIT license. -# - -from PIL import Image, ImageDraw, ImageFont -import os, string, yaml, sys - -from ENNToolMiRCARTColours import ENNToolMiRCARTColours -from ENNToolMiRCARTImporter import ENNToolMiRCARTImporter - -# -# Entry point -def main(*argv): - fontNormalPathName = os.path.join("assets", "DejaVuSansMono.ttf") - fontBoldPathName = os.path.join("assets", "DejaVuSansMono-Bold.ttf") - fontSize = int("26") - outInfoFileName = os.path.join("assets", "textures.yaml") - outFileName = os.path.join("assets", "DejaVuSansMono.png") - if not os.path.exists(os.path.dirname(outInfoFileName)): - os.makedirs(os.path.dirname(outInfoFileName)) - if not os.path.exists(os.path.dirname(outFileName)): - os.makedirs(os.path.dirname(outFileName)) - - pilFontNormal = ImageFont.truetype(fontNormalPathName, fontSize) - pilFontBold = ImageFont.truetype(fontBoldPathName, fontSize) - pilFontSize = list(pilFontNormal.getsize(" ")) - pilFontSize[0] += (8 - (pilFontSize[0] % 8)) - pilFontSize[1] = pilFontSize[0] * 2 - pilImageSize = (pilFontSize[0] * 128, (pilFontSize[1] * 16 * 4)) - print("font size: {}, image size: {}".format(pilFontSize, pilImageSize)) - - curPos = [0, 0] - pilImage = Image.new("RGBA", pilImageSize, (0, 0, 0, 0)) - pilImageDraw = ImageDraw.Draw(pilImage) - - pilImageTmp = Image.new("RGBA", pilFontSize, (0, 0, 0, 0)) - pilImageTmpDraw = ImageDraw.Draw(pilImageTmp) - - for fontAttr in [ENNToolMiRCARTImporter._CellState.CS_BOLD | ENNToolMiRCARTImporter._CellState.CS_UNDERLINE, - ENNToolMiRCARTImporter._CellState.CS_UNDERLINE, - ENNToolMiRCARTImporter._CellState.CS_BOLD, - ENNToolMiRCARTImporter._CellState.CS_NONE]: - for fontColour in reversed(range(16)): - for fontChar in [chr(n) for n in range(128)]: - pilFont, underLine = pilFontNormal, False - if fontAttr & ENNToolMiRCARTImporter._CellState.CS_BOLD: - pilFont = pilFontBold - if fontAttr & ENNToolMiRCARTImporter._CellState.CS_UNDERLINE: - underLine = True - if fontChar in string.printable: - pilImageTmpDraw.text((0, 0), fontChar, - (*ENNToolMiRCARTColours[fontColour], 255), pilFont) - pilImage.paste(pilImageTmp, tuple(curPos)) - pilImageTmpDraw.rectangle((0, 0, pilFontSize[0], pilFontSize[1]), - fill=(0, 0, 0, 0)) - if underLine: - pilImageDraw.line( - xy=(curPos[0], curPos[1] + (pilFontSize[1] - 2), - curPos[0] + pilFontSize[0], curPos[1] + (pilFontSize[1] - 2)), - fill=(*ENNToolMiRCARTColours[fontColour], 255)) - curPos[0] += pilFontSize[0] - curPos[0], curPos[1] = 0, curPos[1] + pilFontSize[1] - pilImage.save(outFileName) - artInfo = {} - artInfo["rowHeight"] = pilFontSize[1] - artInfo["rowWidth"] = pilFontSize[0] - artInfo["texHeight"] = pilImageSize[1] - artInfo["texWidth"] = pilImageSize[0] - with open(outInfoFileName, "w") as fileObject: - yaml.dump(artInfo, fileObject) - -if __name__ == "__main__": - main(*sys.argv) - -# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/README.md b/README.md index c8a4107..9446120 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Copyright (c) 2018 Lucio Andrés Illanes Albornoz <> This project is licensed under the terms of the MIT licence. * Prerequisites on Windows: install Python v3.5.x and script dependencies w/ the following elevated command prompt command line: - `pip install chardet numpy opencv-python Pillow PyOpenGL PyYAML wxPython` + `pip install chardet numpy opencv-python Pillow PyOpenGL wxPython` diff --git a/assets/texture.png b/assets/texture.png deleted file mode 120000 index 820158f..0000000 --- a/assets/texture.png +++ /dev/null @@ -1 +0,0 @@ -DejaVuSansMono.png \ No newline at end of file diff --git a/assets/textures.yaml b/assets/textures.yaml deleted file mode 100644 index 61d7e1c..0000000 --- a/assets/textures.yaml +++ /dev/null @@ -1 +0,0 @@ -{rowHeight: 48, rowWidth: 24, texHeight: 3072, texWidth: 3072}