diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt index e069d817..d2998533 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/ColorPalette.kt @@ -8,7 +8,7 @@ import kotlin.math.abs /** * Color palette used by the game. * - * Every colors from every resources will be aligned to use this color palette. + * Every color from every resource will be aligned to use this color palette. * It means that if a color from a resource is not part of this color palette, * this color will be replaced will the closed color from the palette. * diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt index d2861df9..ec8dd144 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/FrameBuffer.kt @@ -30,7 +30,7 @@ class Blender(private val gamePalette: ColorPalette) { switch[gamePalette.check(source)] = gamePalette.check(target) } - fun mix(colors: Array, x: Pixel, y: Pixel, transparency: Array?): Array? { + fun mix(colors: ByteArray, x: Pixel, y: Pixel, transparency: Array?): ByteArray? { fun dither(pattern: Int): Boolean { val a = x % 4 val b = (y % 4) * 4 @@ -38,10 +38,10 @@ class Blender(private val gamePalette: ColorPalette) { return (pattern shr (15 - (a + b))) and 0x01 == 0x01 } - val color = gamePalette.check(colors[0]) - colors[0] = switch[gamePalette.check(color)] + val color = gamePalette.check(colors[0].toInt()) + colors[0] = switch[color].toByte() // Return null if transparent - if (transparency == null && gamePalette.isTransparent(colors[0])) return null + if (transparency == null && gamePalette.isTransparent(colors[0].toInt())) return null return if (!dither(dithering)) { null } else { @@ -88,7 +88,7 @@ class FrameBuffer( internal val camera = Camera() - private var tmp = Array(1) { 0 } + private var tmp = ByteArray(1) { 0 } private val transparency = arrayOf(0) @@ -103,9 +103,9 @@ class FrameBuffer( val cy = camera.cy(y) if (!clipper.isIn(cx, cy)) return - tmp[0] = gamePalette.check(colorIndex) + tmp[0] = gamePalette.check(colorIndex).toByte() val index = blender.mix(tmp, cx, cy, transparency) ?: return - colorIndexBuffer.set(cx, cy, index[0]) + colorIndexBuffer.set(cx, cy, index[0].toInt()) } fun fill(startX: Pixel, endX: Pixel, y: Pixel, colorIndex: ColorIndex) { @@ -129,7 +129,7 @@ class FrameBuffer( pixel(x, y, colorIndex) } } else { - tmp[0] = color + tmp[0] = color.toByte() val targetColor = blender.mix(tmp, 0, 0, transparency) ?: return colorIndexBuffer.fill(left, right, cy, targetColor[0]) } @@ -180,7 +180,7 @@ class FrameBuffer( /** * Blend function */ - blender: (Array, Pixel, Pixel) -> Array = { colors, _, _ -> colors }, + blender: (ByteArray, Pixel, Pixel) -> ByteArray = { colors, _, _ -> colors }, ) { val cx = camera.cx(dstX) val cy = camera.cy(dstY) @@ -209,23 +209,6 @@ class FrameBuffer( * Create a buffer using the colorIndexBuffer as reference. */ fun generateBuffer(): ByteArray { - // Reset the old buffer - gifBuffer = IntArray(height * width) - - var pos = 0 - for (x in 0 until width) { - for (y in 0 until height) { - val index = colorIndexBuffer.getOne(x, y) - val color = gamePalette.getRGBA(index) - - buffer[pos++] = color[0] - buffer[pos++] = color[1] - buffer[pos++] = color[2] - buffer[pos++] = color[3] - - gifBuffer[x + y * width] = gamePalette.getRGAasInt(index) - } - } - return buffer + return this.colorIndexBuffer.pixels } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt index 8e6271b8..eee435ae 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/graphic/PixelArray.kt @@ -7,11 +7,11 @@ import kotlin.math.min class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = PixelFormat.INDEX) { - internal var pixels = Array(width * height * pixelFormat) { 0 } + internal var pixels = ByteArray(width * height * pixelFormat) { 0 } val size = width * height * pixelFormat - private val tmp = Array(pixelFormat) { 0 } + private val tmp = ByteArray(pixelFormat) { 0 } fun copyFrom(array: PixelArray) { pixels = array.pixels.copyOf() @@ -24,13 +24,13 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix val ctop = 0 - top val cbottom = bottom + 2 * ctop - pixels = Array(width * height * pixelFormat) { index -> + pixels = ByteArray(width * height * pixelFormat) { index -> val p = index / pixelFormat val x = p % width val y = p / width if (x in cleft..cright && y in ctop..cbottom) { - pixel + pixel.toByte() } else { 0 } @@ -47,13 +47,13 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix val position = (correctedX + correctedY * width) * pixelFormat pixel.forEachIndexed { index, value -> - pixels[position + index] = value + pixels[position + index] = value.toByte() } } - fun get(x: Pixel, y: Pixel): Array { + fun get(x: Pixel, y: Pixel): ByteArray { assert(x >= 0 && x < width) { "x ($x) has to be between 0 and $width (excluded)" } - assert(y >= 0 && x < height) { "y ($y) has to be between 0 and $height (excluded)" } + assert(y >= 0 && y < height) { "y ($y) has to be between 0 and $height (excluded)" } val position = (x + y * width) * pixelFormat when (pixelFormat) { PixelFormat.RGBA -> { @@ -81,7 +81,7 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix * The pixel format should be equals to 1 otherwise * it will returns only the first component of the color. */ - fun getOne(x: Pixel, y: Pixel): Int = get(x, y)[0] + fun getOne(x: Pixel, y: Pixel): Int = get(x, y)[0].toInt() fun copyFrom( source: PixelArray, @@ -93,7 +93,7 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix height: Pixel = this.height, reverseX: Boolean = false, reverseY: Boolean = false, - blender: (Array, Pixel, Pixel) -> Array?, + blender: (ByteArray, Pixel, Pixel) -> ByteArray?, ) { assert(source.pixelFormat == pixelFormat) { "Can't copy PixelArray because the pixel format is different between the two PixelArray" @@ -103,14 +103,14 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix val minHeight = min(height, min(height - (dstY + height - this.height), height - (sourceY + height - source.height))) - (0 until minHeight).forEach { h -> + for (h in 0 until minHeight) { val offsetY = if (reverseY) { minHeight - h - 1 } else { h } - (0 until minWidth).forEach { w -> + for (w in 0 until minWidth) { val dstPosition = (w + dstX + (h + dstY) * this.width) * pixelFormat val offsetX = if (reverseX) { @@ -136,9 +136,13 @@ class PixelArray(val width: Pixel, val height: Pixel, val pixelFormat: Int = Pix } } - operator fun iterator(): Iterator = pixels.iterator() + // operator fun iterator(): Iterator = pixels.iterator() fun fill(startX: Int, endX: Int, y: Int, value: Int) { + fill(startX, endX, y, value.toByte()) + } + + fun fill(startX: Int, endX: Int, y: Int, value: Byte) { val yy = (y * width * pixelFormat) pixels.fill(value, yy + startX * pixelFormat, yy + endX * pixelFormat) } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt index a847b982..71e78846 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/SprLib.kt @@ -49,7 +49,7 @@ class SprLib(val gameOptions: GameOptions, val resourceAccess: GameResourceAcces if (isInPixelArray(pixelArray, x, y)) { val index = pixelArray.get(x, y) val colorIndex = index.get(0) - return valueOf(colorIndex) + return valueOf(colorIndex.toInt()) } else { return NIL } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt index 7fe19168..1ca05882 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/lua/StdLib.kt @@ -246,11 +246,11 @@ class StdLib( indexY * 4, 4, 4, - ) { pixel: Array, _, _ -> - if (pixel[0] == 0) { + ) { pixel: ByteArray, _, _ -> + if (pixel[0].toInt() == 0) { pixel } else { - pixel[0] = color + pixel[0] = color.toByte() pixel } } diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt index c09cfe92..9358338f 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRender.kt @@ -11,9 +11,13 @@ import com.danielgergely.kgl.GL_FLOAT import com.danielgergely.kgl.GL_FRAGMENT_SHADER import com.danielgergely.kgl.GL_LINK_STATUS import com.danielgergely.kgl.GL_NEAREST +import com.danielgergely.kgl.GL_R8 +import com.danielgergely.kgl.GL_RED import com.danielgergely.kgl.GL_REPEAT import com.danielgergely.kgl.GL_RGBA import com.danielgergely.kgl.GL_STATIC_DRAW +import com.danielgergely.kgl.GL_TEXTURE0 +import com.danielgergely.kgl.GL_TEXTURE1 import com.danielgergely.kgl.GL_TEXTURE_2D import com.danielgergely.kgl.GL_TEXTURE_MAG_FILTER import com.danielgergely.kgl.GL_TEXTURE_MIN_FILTER @@ -26,6 +30,7 @@ import com.danielgergely.kgl.Kgl import com.danielgergely.kgl.Shader import com.github.minigdx.tiny.Pixel import com.github.minigdx.tiny.engine.GameOptions +import com.github.minigdx.tiny.graphic.PixelFormat import com.github.minigdx.tiny.log.Logger import com.github.minigdx.tiny.platform.RenderContext import com.github.minigdx.tiny.platform.WindowManager @@ -36,14 +41,16 @@ class GLRender( private val gameOptions: GameOptions, ) : Render { + private var buffer = ByteArray(0) + private val uvsData = FloatBuffer( floatArrayOf( 2f, - 0f, - 0f, 2f, 0f, 0f, + 0f, + 2f, ), ) @@ -73,9 +80,13 @@ class GLRender( varying vec2 texture; uniform sampler2D image; + uniform sampler2D colors; void main() { - gl_FragColor = texture2D(image, texture); + vec4 point = texture2D(image, texture); + vec4 color = texture2D(colors, vec2(point.r, 1.0)); + + gl_FragColor = color; } """.trimIndent() @@ -110,12 +121,12 @@ class GLRender( gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) val vertexData = floatArrayOf( + 3f, + -1f, -1f, - -3f, 3f, - 1f, -1f, - 1f, + -1f, ) val positionBuffer = gl.createBuffer() @@ -154,10 +165,47 @@ class GLRender( ) gl.enableVertexAttribArray(uvs) + val colors = gameOptions.colors() + // texture of one pixel height and 256 pixel width. + // one pixel of the texture = one index. + buffer = ByteArray(256 * 256 * PixelFormat.RGBA) + var pos = 0 + for (y in 0 until 256) { + for (index in 0 until 256) { + val color = colors.getRGBA(index) + + buffer[pos++] = color[0] + buffer[pos++] = color[1] + buffer[pos++] = color[2] + buffer[pos++] = color[3] + } + } + val index = gl.createTexture() + gl.bindTexture(GL_TEXTURE_2D, index) + + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) + + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + gl.texImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA, + 256, + 256, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + ByteBuffer(buffer), + ) + gl.uniform1i(gl.getUniformLocation(shaderProgram, "colors")!!, 1) + return GLRenderContext( windowManager = windowManager, program = shaderProgram, texture = gameTexture, + colors = index, ) } @@ -188,24 +236,26 @@ class GLRender( gameOptions.height * gameOptions.zoom * context.windowManager.ratioHeight, ) + // -- game screen -- // + gl.activeTexture(GL_TEXTURE0) gl.bindTexture(GL_TEXTURE_2D, context.texture) gl.texImage2D( GL_TEXTURE_2D, 0, - GL_RGBA, - // I think that the texture format is not in the format OpenGL expect it (column first or line first) - // So I swap the height and the width so it's working even with non square game resolution. - height, + GL_R8, width, + height, 0, - GL_RGBA, + GL_RED, GL_UNSIGNED_BYTE, ByteBuffer(image), ) - gl.uniform1i(gl.getUniformLocation(context.program, "image")!!, 0) + gl.activeTexture(GL_TEXTURE1) + gl.bindTexture(GL_TEXTURE_2D, context.colors) + gl.clear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) gl.clearColor(0f, 0f, 0f, 1.0f) diff --git a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt index 7395d3b2..b0bc4626 100644 --- a/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt +++ b/tiny-engine/src/commonMain/kotlin/com/github/minigdx/tiny/render/GLRenderContext.kt @@ -8,5 +8,6 @@ import com.github.minigdx.tiny.platform.WindowManager class GLRenderContext( val program: Program, val texture: Texture, + val colors: Texture, val windowManager: WindowManager, ) : RenderContext diff --git a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt index 578ea1b2..80bae2d8 100644 --- a/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt +++ b/tiny-engine/src/jvmMain/kotlin/com/github/minigdx/tiny/platform/glfw/GlfwPlatform.kt @@ -56,7 +56,8 @@ class GlfwPlatform( private var lastFrame: Long = getTime() // Keep 30 seconds at 60 frames per seconds - private val gifBufferCache: MutableFixedSizeList = MutableFixedSizeList(gameOptions.record.toInt() * FPS) + private val gifFrameCache: MutableFixedSizeList = MutableFixedSizeList(gameOptions.record.toInt() * FPS) + private var lastBuffer: FrameBuffer? = null private val lwjglInputHandler = LwjglInput(gameOptions) @@ -187,10 +188,22 @@ class GlfwPlatform( GLFW.glfwTerminate() } + private fun convert(data: ByteArray): IntArray { + val result = IntArray(data.size) + val colorPalette = gameOptions.colors() + data.forEachIndexed { index, byte -> + result[index] = colorPalette.getRGAasInt(byte.toInt()) + } + return result + } + override fun draw(context: RenderContext, frameBuffer: FrameBuffer) { val image = frameBuffer.generateBuffer() render.draw(context, image, frameBuffer.width, frameBuffer.height) - gifBufferCache.add(frameBuffer.gifBuffer) + val imageCopy = image.copyOf() + recordScope.launch { + gifFrameCache.add(convert(imageCopy)) + } lastBuffer = frameBuffer } @@ -201,7 +214,7 @@ class GlfwPlatform( logger.info("GLWF") { "Starting to generate GIF in '${origin.absolutePath}' (Wait for it...)" } val buffer = mutableListOf().apply { - addAll(gifBufferCache) + addAll(gifFrameCache) } recordScope.launch {