From eff72be6ef849aa89783b9e4859098824e369afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucio=20Andr=C3=A9s=20Illanes=20Albornoz?= Date: Tue, 3 Jul 2018 18:17:23 +0200 Subject: [PATCH] Initial FFmpeg & {GIF,MP4,WEBM,...} support for -o. ENNToolGLCanvasPanel.py: minor cleanup regarding deprecated calls. --- ENNTool.py | 48 +++++++-------- ENNToolGLCanvasPanel.py | 125 +++++++++++--------------------------- ENNToolGLTTFTexture.py | 4 +- ENNToolGLVideoWriter.py | 44 ++++++++++++++ ENNToolMiRCARTImporter.py | 1 - README.md | 3 +- 6 files changed, 104 insertions(+), 121 deletions(-) create mode 100644 ENNToolGLVideoWriter.py diff --git a/ENNTool.py b/ENNTool.py index 926376e..4628ae0 100755 --- a/ENNTool.py +++ b/ENNTool.py @@ -6,14 +6,12 @@ # # TODO: # 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) 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) +# 2) -s: effects: rotate, smash into bricks, swirl, wave, ... +# 3) Feature: include ETA @ progress bar +# 4) Feature: autodetect video width from widest mircart +# 5) Feature: render mircart as 3D blocks vs flat surface +# 6) 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) +# 7) OpenGL: use VAOs + glVertexAttribFormat + glVertexAttribBinding # from getopt import getopt, GetoptError @@ -24,6 +22,7 @@ import wx from ENNToolGLCanvasPanel import ENNToolGLCanvasPanel from ENNToolGLTTFTexture import ENNToolGLTTFTexture +from ENNToolGLVideoWriter import ENNToolGLVideoWriter from ENNToolMiRCARTImporter import ENNToolMiRCARTImporter class ENNToolApp(object): @@ -33,31 +32,30 @@ class ENNToolApp(object): def parseArgv(self, argv): def usage(argv0): print("usage: {}".format(os.path.basename(argv0)), file=sys.stderr) - print(" [-A] [-f fps] [-h] [-o pname]".format(os.path.basename(argv0)), file=sys.stderr) - print(" [-p] [-r WxH] [-R WxH] [-s pname]", file=sys.stderr) - print(" [-S] [-t pname] [-v] [--] pname..", file=sys.stderr) + print(" [-A] [-f fps] [-h] [-o fname]".format(os.path.basename(argv0)), file=sys.stderr) + print(" [-p] [-r WxH] [-R WxH] [-s fname]", file=sys.stderr) + print(" [-S] [-v] [--] fname..", file=sys.stderr) print("", file=sys.stderr) print(" -a........: select animation mode", file=sys.stderr) print(" -f fps....: set video FPS; defaults to 25", file=sys.stderr) print(" -h........: show this screen", file=sys.stderr) - print(" -o pname..: output video pathname", file=sys.stderr) + print(" -o fname..: output video filename; extension determines video type", file=sys.stderr) print(" -p........: play video after rendering", file=sys.stderr) print(" -r WxH....: set video resolution; defaults to 1152x864", file=sys.stderr) print(" -R WxH....: set MiRCART cube resolution; defaults to 0.1x0.2", file=sys.stderr) - print(" -s pname..: input script pathname", file=sys.stderr) + print(" -s fname..: input script filename", file=sys.stderr) print(" -S........: select scrolling mode", file=sys.stderr) - print(" -t pname..: set MiRCART texture pathname; defaults to {}".format(os.path.join("assets", "texture.png")), file=sys.stderr) print(" -v........: be verbose", file=sys.stderr) try: - optlist, argv = getopt(argv[1:], "Af:ho:pr:R:s:St:v") + optlist, argv = getopt(argv[1:], "Af:ho:pr:R:s:Sv") optdict = dict(optlist) if "-h" in optdict: usage(sys.argv[0]); exit(0); elif not "-o" in optdict: - raise GetoptError("-o pname must be specified") + raise GetoptError("-o fname must be specified") elif not len(argv): - raise GetoptError("at least one MiRCART input pname must be specified") + raise GetoptError("at least one MiRCART input fname must be specified") if not "-f" in optdict: optdict["-f"] = "25" @@ -65,8 +63,6 @@ class ENNToolApp(object): optdict["-r"] = "1152x864" if not "-R" in optdict: optdict["-R"] = "0.1x0.2" - if not "-t" in optdict: - optdict["-t"] = os.path.join("assets", "texture.png") if "-r" in optdict: optdict["-r"] = [int(r) for r in optdict["-r"].split("x")][0:2] @@ -86,8 +82,8 @@ class ENNToolApp(object): print("\r[{:<50}] {}%".format( ("=" * int(progressDiv * 50)), int(progressDiv * 100)), end=endChar) # }}} - # {{{ modeScroll(self, argv, optdict, panelGLCanvas, texturePathName, fps=25, scrollRate=0.25): XXX - def modeScroll(self, argv, optdict, panelGLCanvas, texturePathName, fps=25, scrollRate=0.25): + # {{{ modeScroll(self, argv, optdict, GLVideoWriter, panelGLCanvas, fps=25, scrollRate=0.25): XXX + def modeScroll(self, argv, optdict, GLVideoWriter, panelGLCanvas, fps=25, scrollRate=0.25): MiRCART = [] for inFileArg in argv: for inFile in sorted(glob(inFileArg)): @@ -110,9 +106,11 @@ class ENNToolApp(object): glRotatef(rotateX * (180.0/w), 0.0, 1.0, 0.0) if rotateY: glRotatef(rotateY * (180.0/h), 1.0, 0.0, 0.0) - panelGLCanvas.saveFrame() + GLVideoWriter.saveFrame() if curY >= lastY: self.printProgress(curY, lastY); break; + + GLVideoWriter.saveVideo() # }}} # {{{ __init__(self, argv): XXX def __init__(self, argv): @@ -123,14 +121,14 @@ class ENNToolApp(object): appPanelSkin = wx.Panel(appFrame, wx.ID_ANY) videoFps, videoPath = int(optdict["-f"]), optdict["-o"] - panelGLCanvas = ENNToolGLCanvasPanel(appPanelSkin, size=appFrameSize, videoPath=videoPath) + panelGLCanvas = ENNToolGLCanvasPanel(appPanelSkin, size=appFrameSize) panelGLCanvas.initOpenGL() panelGLCanvas.initShaders() - panelGLCanvas.initVideoWriter(fps=videoFps) + GLVideoWriter = ENNToolGLVideoWriter(videoPath, panelGLCanvas.GetClientSize(), videoFps=videoFps) if "-v" in optdict: time0 = time.time() - self.modeScroll(argv, optdict, panelGLCanvas, fps=videoFps, texturePathName=optdict["-t"]) + self.modeScroll(argv, optdict, GLVideoWriter, panelGLCanvas, fps=videoFps) if "-v" in optdict: print("delta {}s".format(time.time() - time0)) if "-p" in optdict: diff --git a/ENNToolGLCanvasPanel.py b/ENNToolGLCanvasPanel.py index b7640cb..d26f0b6 100644 --- a/ENNToolGLCanvasPanel.py +++ b/ENNToolGLCanvasPanel.py @@ -11,15 +11,12 @@ # Wed, 27 Jun 2018 16:02:13 +0200 [4] # Wed, 27 Jun 2018 16:02:14 +0200 [5] # Thu, 28 Jun 2018 18:32:50 +0200 [6] +# Tue, 03 Jul 2018 14:34:57 +0200 [7] # from OpenGL.GL import * from OpenGL.GL import shaders -import cv2, numpy -import ctypes, os, sys, time -import wx, wx.glcanvas - -from ENNToolMiRCARTColours import ENNToolMiRCARTColoursFloat +import ctypes, wx, wx.glcanvas class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): """XXX""" @@ -40,10 +37,10 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): # }}} # {{{ initShaders(self): XXX def initShaders(self): + # Fragment shader fs = shaders.compileShader(""" #version 330 core - in vec4 bgColour; in vec2 fgTexCoord; uniform sampler2D texture; @@ -52,125 +49,83 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): gl_FragColor = vec4(texel.r, texel.g, texel.b, 1.0); } """, GL_FRAGMENT_SHADER) + + # Vertex shader vs = shaders.compileShader(""" #version 330 core layout(location = 0) in vec4 vertex; - layout(location = 1) in vec3 normal; - layout(location = 2) in vec4 colour; - layout(location = 3) in vec2 texcoord; + layout(location = 1) in vec2 texcoord; - out vec4 bgColour; out vec2 fgTexCoord; - uniform mat4 model; + uniform mat4 modelview; uniform mat4 projection; void main() { - gl_Position = projection * model * vertex; - bgColour = colour; + gl_Position = projection * modelview * vertex; fgTexCoord = texcoord; } """, GL_VERTEX_SHADER) self.shader = shaders.compileProgram(vs, fs) # }}} - # {{{ 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) - glEnableClientState(GL_NORMAL_ARRAY) - glEnableClientState(GL_COLOR_ARRAY) - glEnableClientState(GL_TEXTURE_COORD_ARRAY) - glEnable(GL_TEXTURE_2D) - - glEnable(GL_BLEND) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - - glBindTexture(GL_TEXTURE_2D, artTextureId) + # Bind VBO and named texture & install shader program object glBindBuffer(GL_ARRAY_BUFFER, artVbo) - + glBindTexture(GL_TEXTURE_2D, artTextureId) glUseProgram(self.shader) - model = (GLfloat * 16)() - glGetFloatv(GL_MODELVIEW_MATRIX, model) - projection = (GLfloat * 16)() + + # Specify modelview and projection matrix & texture unit uniforms for shader programs + modelview, projection = (GLfloat * 16)(), (GLfloat * 16)() + glGetFloatv(GL_MODELVIEW_MATRIX, modelview) glGetFloatv(GL_PROJECTION_MATRIX, projection) - glUniformMatrix4fv(glGetUniformLocation(self.shader, "model"), 1, GL_FALSE, model) + glUniformMatrix4fv(glGetUniformLocation(self.shader, "modelview"), 1, GL_FALSE, modelview) glUniformMatrix4fv(glGetUniformLocation(self.shader, "projection"), 1, GL_FALSE, projection) glUniform1i(glGetUniformLocation(self.shader, "texture"), 0) - # [6] + # VBO vertices location glEnableVertexAttribArray(0) - glVertexAttribPointer(0, 3, GL_FLOAT, False, 48, ctypes.c_void_p(0)) + glVertexAttribPointer(0, 3, GL_FLOAT, False, 20, ctypes.c_void_p(0)) + glVertexPointer(3, GL_FLOAT, 20, ctypes.c_void_p(0)) + + # VBO texture coordinates glEnableVertexAttribArray(1) - glVertexAttribPointer(1, 3, GL_FLOAT, False, 48, ctypes.c_void_p(12)) - glEnableVertexAttribArray(2) - glVertexAttribPointer(2, 4, GL_FLOAT, False, 48, ctypes.c_void_p(24)) - glEnableVertexAttribArray(3) - glVertexAttribPointer(3, 2, GL_FLOAT, False, 48, ctypes.c_void_p(40)) + glVertexAttribPointer(1, 2, GL_FLOAT, False, 20, ctypes.c_void_p(12)) + glTexCoordPointer(2, GL_FLOAT, 20, ctypes.c_void_p(12)) - glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) - glVertexPointer(3, GL_FLOAT, 48, ctypes.c_void_p(0)) - glNormalPointer(GL_FLOAT, 48, ctypes.c_void_p(12)) - glColorPointer(4, GL_FLOAT, 48, ctypes.c_void_p(24)) - glTexCoordPointer(2, GL_FLOAT, 48, ctypes.c_void_p(40)) + # Clear colour and depth buffer, draw quads from VBO & clear state + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glDrawArrays(GL_QUADS, 0, artVboLen) - glDisableVertexAttribArray(0) - glDisable(GL_BLEND) - glDisable(GL_TEXTURE_2D) - glDisableClientState(GL_TEXTURE_COORD_ARRAY) - glDisableClientState(GL_COLOR_ARRAY) - glDisableClientState(GL_NORMAL_ARRAY) - glDisableClientState(GL_VERTEX_ARRAY) + glBindTexture(GL_TEXTURE_2D, 0) # }}} # {{{ renderMiRCART(self, artInfo, artMap, centre=True, canvasCols=100, cubeSize=(0.1, 0.2)): XXX def renderMiRCART(self, artInfo, artMap, centre=True, canvasCols=100, cubeSize=(0.1, 0.2)): - curPos = [0, 0, 0]; vertices = []; numVertices = 0; + curPos, vertices, numVertices = [0, 0, 0], [], 0 for numRow in range(len(artMap)): if centre and (len(artMap[numRow]) < canvasCols): curPos[0] += (((canvasCols - len(artMap[numRow])) * cubeSize[0]) / 2) for numCol in range(len(artMap[numRow])): 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 + # Top Right, Top Left vertices += curPos - vertices += [0.0, 0.0, 1.0] - 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 += cubeBgFloat + vertices += [curPos[0] - cubeSize[0], curPos[1], curPos[2]] 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 += cubeBgFloat + # Bottom Left, Bottom Right + vertices += [curPos[0] - cubeSize[0], curPos[1] - cubeSize[1], curPos[2]] 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 += cubeBgFloat + vertices += [curPos[0], curPos[1] - cubeSize[1], curPos[2]] vertices += artCell[6:8] - numVertices += 1 - curPos[0] += cubeSize[0] + curPos[0], numVertices = curPos[0] + cubeSize[0], numVertices + 4 curPos[0], curPos[1] = 0, curPos[1] - cubeSize[1] artVbo = glGenBuffers(1) @@ -180,22 +135,10 @@ class ENNToolGLCanvasPanel(wx.glcanvas.GLCanvas, wx.Panel): GL_STATIC_DRAW) return artVbo, len(vertices), -curPos[1], numVertices # }}} - # {{{ saveFrame(self): XXX - def saveFrame(self): - if sys.byteorder == "little": - screenshot = glReadPixels(0, 0, self.width, self.height, GL_BGR, GL_UNSIGNED_BYTE) - else: - screenshot = glReadPixels(0, 0, self.width, self.height, GL_RGB, GL_UNSIGNED_BYTE) - 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): + # {{{ __init__(self, parent, size, defaultPos=(24,24)): initialisation method + def __init__(self, parent, size, defaultPos=(24,24)): super().__init__(parent, pos=defaultPos, size=size) self.curPos = list(defaultPos); self.curSize = list(size); - self.width, self.height = self.GetClientSize() - self.videoPath = videoPath # }}} # vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/ENNToolGLTTFTexture.py b/ENNToolGLTTFTexture.py index d44c301..c2ef112 100644 --- a/ENNToolGLTTFTexture.py +++ b/ENNToolGLTTFTexture.py @@ -17,7 +17,6 @@ from OpenGL.GL import * from PIL import Image, ImageDraw, ImageFont import numpy import os, string, sys - from ENNToolMiRCARTColours import ENNToolMiRCARTColours from ENNToolMiRCARTImporter import ENNToolMiRCARTImporter @@ -34,7 +33,6 @@ class ENNToolGLTTFTexture(object): 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] @@ -119,9 +117,9 @@ class ENNToolGLTTFTexture(object): 0, GL_RGBA, GL_UNSIGNED_BYTE, artTextureImageData) glGenerateMipmap(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, 0) return artTextureId # }}} - # {{{ getParams(self): XXX def getParams(self): return self.artTextureId, self.artInfo diff --git a/ENNToolGLVideoWriter.py b/ENNToolGLVideoWriter.py new file mode 100644 index 0000000..8d5da00 --- /dev/null +++ b/ENNToolGLVideoWriter.py @@ -0,0 +1,44 @@ +#!/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 OpenGL.GL import * +import numpy, subprocess + +class ENNToolGLVideoWriter(object): + """XXX""" + + # {{{ saveFrame(self): XXX + def saveFrame(self): + frameBuffer = glReadPixels(0, 0, self.videoSize[0], self.videoSize[1], GL_RGB, GL_UNSIGNED_BYTE) + frameBuffer = numpy.frombuffer(frameBuffer, numpy.uint8) + frameBuffer = frameBuffer.reshape((self.videoSize[1], self.videoSize[0], 3)) + frameBuffer = numpy.flipud(frameBuffer) + self.videoFrames += [frameBuffer] + # }}} + # {{{ saveVideo(self): XXX + def saveVideo(self): + with subprocess.Popen([ + "FFmpeg.exe", + "-pix_fmt", "rgb24", + "-r", str(self.videoFps), + "-s", "x".join([str(r) for r in self.videoSize]), + "-vcodec", "rawvideo", + "-f", "rawvideo", + "-i", "-", + "-an", + "-y", + self.videoPath], stdin=subprocess.PIPE) as procObject: + for videoFrame in self.videoFrames: + procObject.stdin.write(videoFrame.tobytes()) + # }}} + # {{{ __init__(self, videoPath, videoSize, videoFps=25): XXX + def __init__(self, videoPath, videoSize, videoFps=25): + self.videoFps, self.videoPath, self.videoSize = videoFps, videoPath, videoSize + self.videoFrames = [] + # }}} + +# vim:expandtab foldmethod=marker sw=4 ts=4 tw=120 diff --git a/ENNToolMiRCARTImporter.py b/ENNToolMiRCARTImporter.py index 8bd1893..be86641 100644 --- a/ENNToolMiRCARTImporter.py +++ b/ENNToolMiRCARTImporter.py @@ -26,7 +26,6 @@ class ENNToolMiRCARTImporter(object): PS_COLOUR_DIGIT0 = 2 PS_COLOUR_DIGIT1 = 3 # }}} - # {{{ _flipCellStateBit(self, cellState, bit): XXX def _flipCellStateBit(self, cellState, bit): if cellState & bit: diff --git a/README.md b/README.md index 9446120..67c6256 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,5 @@ 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 wxPython` + `pip install chardet numpy Pillow PyOpenGL wxPython` +* Additionally, FFmpeg.exe must be present in the current working directory and/or %PATH%.