diff --git a/leenkx/Sources/iron/format/bmp/Data.hx b/leenkx/Sources/iron/format/bmp/Data.hx new file mode 100644 index 00000000..db35cdd4 --- /dev/null +++ b/leenkx/Sources/iron/format/bmp/Data.hx @@ -0,0 +1,50 @@ +/* + * format - Haxe File Formats + * + * BMP File Format + * Copyright (C) 2007-2009 Trevor McCauley, Baluta Cristian (hx port) & Robert Sköld (format conversion) + * + * Copyright (c) 2009, The Haxe Project Contributors + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + */ +package iron.format.bmp; + +typedef Data = { + var header : iron.format.bmp.Header; + var pixels : haxe.io.Bytes; +#if (haxe_ver < 4) + var colorTable : Null; +#else + var ?colorTable : haxe.io.Bytes; +#end +} + +typedef Header = { + var width : Int; // real width (in pixels) + var height : Int; // real height (in pixels) + var paddedStride : Int; // number of bytes in a stride (including padding) + var topToBottom : Bool; // whether the bitmap is stored top to bottom + var bpp : Int; // bits per pixel + var dataLength : Int; // equal to `paddedStride` * `height` + var compression : Int; // which compression is being used, 0 for no compression +} diff --git a/leenkx/Sources/iron/format/bmp/Reader.hx b/leenkx/Sources/iron/format/bmp/Reader.hx new file mode 100644 index 00000000..d46895c7 --- /dev/null +++ b/leenkx/Sources/iron/format/bmp/Reader.hx @@ -0,0 +1,122 @@ +/* + * format - Haxe File Formats + * + * BMP File Format + * Copyright (C) 2007-2009 Robert Sköld + * + * Copyright (c) 2009, The Haxe Project Contributors + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + */ + +package iron.format.bmp; + +import iron.format.bmp.Data; + + +class Reader { + + var input : haxe.io.Input; + + public function new( i ) { + input = i; + } + + /** + * Only supports uncompressed 24bpp bitmaps (the most common format). + * + * The returned bytes in `Data.pixels` will be in BGR order, and with padding (if present). + * + * @see https://msdn.microsoft.com/en-us/library/windows/desktop/dd318229(v=vs.85).aspx + * @see https://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header + */ + public function read() : format.bmp.Data { + // Read Header + for (b in ["B".code, "M".code]) { + if (input.readByte() != b) throw "Invalid header"; + } + + var fileSize = input.readInt32(); + input.readInt32(); // Reserved + var offset = input.readInt32(); + + // Read InfoHeader + var infoHeaderSize = input.readInt32(); // InfoHeader size + if (infoHeaderSize != 40) { + throw 'Info headers with size $infoHeaderSize not supported.'; + } + var width = input.readInt32(); // Image width (actual, not padded) + var height = input.readInt32(); // Image height + var numPlanes = input.readInt16(); // Number of planes + var bits = input.readInt16(); // Bits per pixel + var compression = input.readInt32(); // Compression type + var dataLength = input.readInt32(); // Image data size (includes padding!) + input.readInt32(); // Horizontal resolution + input.readInt32(); // Vertical resolution + var colorsUsed = input.readInt32(); // Colors used (0 when uncompressed) + input.readInt32(); // Important colors (0 when uncompressed) + + // If there's no compression, the dataLength may be 0 + if ( compression == 0 && dataLength == 0 ) dataLength = fileSize - offset; + + var bytesRead = 54; // total read above + + var colorTable : haxe.io.Bytes = null; + if ( bits <= 8 ) { + if ( colorsUsed == 0 ) { + colorsUsed = Tools.getNumColorsForBitDepth(bits); + } + var colorTableLength = 4 * colorsUsed; + colorTable = haxe.io.Bytes.alloc( colorTableLength ); + input.readFullBytes( colorTable, 0, colorTableLength ); + bytesRead += colorTableLength; + } + + input.read( offset - bytesRead ); + + var p = haxe.io.Bytes.alloc( dataLength ); + + // Read Raster Data + var paddedStride = Tools.computePaddedStride(width, bits); + var topToBottom = false; + if ( height < 0 ) { // if bitmap is stored top to bottom + topToBottom = true; + height = -height; + } + + input.readFullBytes(p, 0, dataLength); + + return { + header: { + width: width, + height: height, + paddedStride: paddedStride, + topToBottom: topToBottom, + bpp: bits, + dataLength: dataLength, + compression: compression + }, + pixels: p, + colorTable: colorTable + } + } +} \ No newline at end of file diff --git a/leenkx/Sources/iron/format/bmp/Tools.hx b/leenkx/Sources/iron/format/bmp/Tools.hx new file mode 100644 index 00000000..04051e6b --- /dev/null +++ b/leenkx/Sources/iron/format/bmp/Tools.hx @@ -0,0 +1,256 @@ +/* + * format - Haxe File Formats + * + * BMP File Format + * Copyright (C) 2007-2009 Trevor McCauley, Baluta Cristian (hx port) & Robert Sköld (format conversion) + * + * Copyright (c) 2009, The Haxe Project Contributors + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + */ +package iron.format.bmp; + + +class Tools { + + // a r g b + static var ARGB_MAP(default, never):Array = [0, 1, 2, 3]; + static var BGRA_MAP(default, never):Array = [3, 2, 1, 0]; + + static var COLOR_SIZE(default, never):Int = 4; + + /** + Extract BMP pixel data (24bpp in BGR format) and expands it to BGRA, removing any padding in the process. + **/ + inline static public function extractBGRA( bmp : iron.format.bmp.Data ) : haxe.io.Bytes { + return _extract32(bmp, BGRA_MAP, 0xFF); + } + + /** + Extract BMP pixel data (24bpp in BGR format) and converts it to ARGB. + **/ + inline static public function extractARGB( bmp : iron.format.bmp.Data ) : haxe.io.Bytes { + return _extract32(bmp, ARGB_MAP, 0xFF); + } + + /** + Creates BMP data from bytes in BGRA format for each pixel. + **/ + inline static public function buildFromBGRA( width : Int, height : Int, srcBytes : haxe.io.Bytes, topToBottom : Bool = false ) : Data { + return _buildFrom32(width, height, srcBytes, BGRA_MAP, topToBottom); + } + + /** + Creates BMP data from bytes in ARGB format for each pixel. + **/ + inline static public function buildFromARGB( width : Int, height : Int, srcBytes : haxe.io.Bytes, topToBottom : Bool = false ) : Data { + return _buildFrom32(width, height, srcBytes, ARGB_MAP, topToBottom); + } + + inline static public function computePaddedStride(width:Int, bpp:Int):Int { + return ((((width * bpp) + 31) & ~31) >> 3); + } + + /** + * Gets number of colors for indexed palettes + */ + inline static public function getNumColorsForBitDepth(bpp:Int):Int { + return switch (bpp) { + case 1: 2; + case 4: 16; + case 8: 256; + case 16: 65536; + default: throw 'Unsupported bpp $bpp'; + } + } + + + // `channelMap` contains indices to map into ARGB (f.e. the mapping for ARGB is [0,1,2,3], while for BGRA is [3,2,1,0]) + static function _extract32( bmp : iron.format.bmp.Data, channelMap : Array, alpha : Int = 0xFF) : haxe.io.Bytes { + var srcBytes = bmp.pixels; + var dstLen = bmp.header.width * bmp.header.height * 4; + var dstBytes = haxe.io.Bytes.alloc( dstLen ); + var srcPaddedStride = bmp.header.paddedStride; + + var yDir = -1; + var dstPos = 0; + var srcPos = srcPaddedStride * (bmp.header.height - 1); + + if ( bmp.header.topToBottom ) { + yDir = 1; + srcPos = 0; + } + + if ( bmp.header.bpp < 8 || bmp.header.bpp == 16 ) { + throw 'bpp ${bmp.header.bpp} not supported'; + } + + var colorTable:haxe.io.Bytes = null; + if ( bmp.header.bpp <= 8 ) { + var colorTableLength = getNumColorsForBitDepth(bmp.header.bpp); + colorTable = haxe.io.Bytes.alloc(colorTableLength * COLOR_SIZE); + var definedColorTableLength = Std.int( bmp.colorTable.length / COLOR_SIZE ); + for( i in 0...definedColorTableLength ) { + var b = bmp.colorTable.get( i * COLOR_SIZE); + var g = bmp.colorTable.get( i * COLOR_SIZE + 1); + var r = bmp.colorTable.get( i * COLOR_SIZE + 2); + + colorTable.set(i * COLOR_SIZE + channelMap[0], alpha); + colorTable.set(i * COLOR_SIZE + channelMap[1], r); + colorTable.set(i * COLOR_SIZE + channelMap[2], g); + colorTable.set(i * COLOR_SIZE + channelMap[3], b); + } + // We want to have the table the full length in case indices outside the range are present + colorTable.fill(definedColorTableLength, colorTableLength - definedColorTableLength, 0); + for( i in definedColorTableLength...colorTableLength ) { + colorTable.set(i * COLOR_SIZE + channelMap[0], alpha); + } + } + + switch bmp.header.compression { + case 0: + while( dstPos < dstLen ) { + for( i in 0...bmp.header.width ) { + if (bmp.header.bpp == 8) { + + var currentSrcPos = srcPos + i; + var index = srcBytes.get(currentSrcPos); + dstBytes.blit( dstPos, colorTable, index * COLOR_SIZE, COLOR_SIZE ); + + } else if (bmp.header.bpp == 24) { + + var currentSrcPos = srcPos + i * 3; + var b = srcBytes.get(currentSrcPos); + var g = srcBytes.get(currentSrcPos + 1); + var r = srcBytes.get(currentSrcPos + 2); + + dstBytes.set(dstPos + channelMap[0], alpha); + dstBytes.set(dstPos + channelMap[1], r); + dstBytes.set(dstPos + channelMap[2], g); + dstBytes.set(dstPos + channelMap[3], b); + + } else if (bmp.header.bpp == 32) { + + var currentSrcPos = srcPos + i * 4; + var b = srcBytes.get(currentSrcPos); + var g = srcBytes.get(currentSrcPos + 1); + var r = srcBytes.get(currentSrcPos + 2); + + dstBytes.set(dstPos + channelMap[0], alpha); + dstBytes.set(dstPos + channelMap[1], r); + dstBytes.set(dstPos + channelMap[2], g); + dstBytes.set(dstPos + channelMap[3], b); + + } + dstPos += 4; + } + srcPos += yDir * srcPaddedStride; + } + case 1: + srcPos = 0; + var x = 0; + var y = bmp.header.topToBottom ? 0 : bmp.header.height - 1; + while( srcPos < bmp.header.dataLength ) { + var count = srcBytes.get(srcPos++); + var index = srcBytes.get(srcPos++); + if ( count == 0 ) { + if ( index == 0 ) { + x = 0; + y += yDir; + } else if ( index == 1 ) { + break; + } else if ( index == 2 ) { + x += srcBytes.get(srcPos++); + y += srcBytes.get(srcPos++); + } else { + count = index; + for( i in 0...count ) { + index = srcBytes.get(srcPos++); + dstBytes.blit( COLOR_SIZE * ((x+i) + y * bmp.header.width), colorTable, index * COLOR_SIZE, COLOR_SIZE ); + } + if (srcPos % 2 != 0) srcPos++; + x += count; + } + } else { + for( i in 0...count ) { + dstBytes.blit( COLOR_SIZE * ((x+i) + y * bmp.header.width), colorTable, index * COLOR_SIZE, COLOR_SIZE ); + } + x += count; + } + } + default: + throw 'compression ${bmp.header.compression} not supported'; + } + + return dstBytes; + } + + // `channelMap` contains indices to map into ARGB (f.e. the mapping for ARGB is [0,1,2,3], while for BGRA is [3,2,1,0]) + static function _buildFrom32( width : Int, height : Int, srcBytes : haxe.io.Bytes, channelMap : Array, topToBottom : Bool = false ) : Data { + var bpp = 24; + var paddedStride = computePaddedStride(width, bpp); + var bytesBGR = haxe.io.Bytes.alloc(paddedStride * height); + var topToBottom = topToBottom; + var dataLength = bytesBGR.length; + + var dstStride = width * 3; + var srcLen = width * height * 4; + var yDir = -1; + var dstPos = dataLength - paddedStride; + var srcPos = 0; + + if ( topToBottom ) { + yDir = 1; + dstPos = 0; + } + + while( srcPos < srcLen ) { + var i = dstPos; + while( i < dstPos + dstStride ) { + var r = srcBytes.get(srcPos + channelMap[1]); + var g = srcBytes.get(srcPos + channelMap[2]); + var b = srcBytes.get(srcPos + channelMap[3]); + + bytesBGR.set(i++, b); + bytesBGR.set(i++, g); + bytesBGR.set(i++, r); + + srcPos += 4; + } + dstPos += yDir * paddedStride; + } + + return { + header: { + width: width, + height: height, + paddedStride: paddedStride, + topToBottom: topToBottom, + bpp: bpp, + dataLength: dataLength, + compression: 0 + }, + pixels: bytesBGR, + colorTable: null + } + } +} \ No newline at end of file diff --git a/leenkx/Sources/iron/format/bmp/Writer.hx b/leenkx/Sources/iron/format/bmp/Writer.hx new file mode 100644 index 00000000..2f56132b --- /dev/null +++ b/leenkx/Sources/iron/format/bmp/Writer.hx @@ -0,0 +1,74 @@ +/* + * format - Haxe File Formats + * + * BMP File Format + * Copyright (C) 2007-2009 Robert Sköld + * + * Copyright (c) 2009, The Haxe Project Contributors + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + */ + +package iron.format.bmp; + +import iron.format.bmp.Data; + + +class Writer { + + static var DATA_OFFSET : Int = 0x36; + + var output : haxe.io.Output; + + public function new(o) { + output = o; + } + + /** + * Specs: http://s223767089.online.de/en/file-format-bmp + */ + public function write( bmp : Data ) { + // Write Header (14 bytes) + output.writeString( "BM" ); // Signature + output.writeInt32(bmp.pixels.length + DATA_OFFSET ); // FileSize + output.writeInt32( 0 ); // Reserved + output.writeInt32( DATA_OFFSET ); // Offset + + // Write InfoHeader (40 bytes) + output.writeInt32( 40 ); // InfoHeader size + output.writeInt32( bmp.header.width ); // Image width + var height = bmp.header.height; + if (bmp.header.topToBottom) height = -height; + output.writeInt32( height ); // Image height + output.writeInt16( 1 ); // Number of planes + output.writeInt16( 24 ); // Bits per pixel (24bit RGB) + output.writeInt32( 0 ); // Compression type (no compression) + output.writeInt32( bmp.header.dataLength ); // Image data size (0 when uncompressed) + output.writeInt32( 0x2e30 ); // Horizontal resolution + output.writeInt32( 0x2e30 ); // Vertical resolution + output.writeInt32( 0 ); // Colors used (0 when uncompressed) + output.writeInt32( 0 ); // Important colors (0 when uncompressed) + + // Write Raster Data + output.write(bmp.pixels); + } +} \ No newline at end of file diff --git a/leenkx/Sources/leenkx/logicnode/DrawCameraTextureNode.hx b/leenkx/Sources/leenkx/logicnode/DrawCameraTextureNode.hx index e367709e..58eff7c2 100644 --- a/leenkx/Sources/leenkx/logicnode/DrawCameraTextureNode.hx +++ b/leenkx/Sources/leenkx/logicnode/DrawCameraTextureNode.hx @@ -2,6 +2,9 @@ package leenkx.logicnode; import iron.Scene; import iron.object.CameraObject; +import iron.math.Vec4; +import iron.math.Quat; +import leenkx.math.Helper; import leenkx.renderpath.RenderPathCreator; @@ -27,11 +30,19 @@ class DrawCameraTextureNode extends LogicNode { final c = inputs[2].get(); assert(Error, Std.isOfType(c, CameraObject), "Camera must be a camera object!"); cam = cast(c, CameraObject); - rt = kha.Image.createRenderTarget(iron.App.w(), iron.App.h()); + rt = kha.Image.createRenderTarget(iron.App.w(), iron.App.h(), + kha.graphics4.TextureFormat.RGBA32, + kha.graphics4.DepthStencilFormat.NoDepthAndStencil); assert(Error, mo.materials[matSlot].contexts[0].textures != null, 'Object "${mo.name}" has no diffuse texture to render to'); - mo.materials[matSlot].contexts[0].textures[0] = rt; // Override diffuse texture + final n = inputs[5].get(); + for (i => node in mo.materials[matSlot].contexts[0].raw.bind_textures){ + if (node.name == n){ + mo.materials[matSlot].contexts[0].textures[i] = rt; // Override diffuse texture + break; + } + } tree.notifyOnRender(render); runOutput(0); @@ -48,8 +59,20 @@ class DrawCameraTextureNode extends LogicNode { iron.Scene.active.camera = cam; cam.renderTarget = rt; + #if kha_html5 + var q: Quat = new Quat(); + q.fromAxisAngle(new Vec4(0, 0, 1, 1), Helper.degToRad(180)); + cam.transform.rot.mult(q); + cam.transform.buildMatrix(); + #end + cam.renderFrame(g); + #if kha_html5 + cam.transform.rot.mult(q); + cam.transform.buildMatrix(); + #end + cam.renderTarget = oldRT; iron.Scene.active.camera = sceneCam; } diff --git a/leenkx/Sources/leenkx/logicnode/DrawImageSequenceNode.hx b/leenkx/Sources/leenkx/logicnode/DrawImageSequenceNode.hx index dd1d4946..230b6dbb 100644 --- a/leenkx/Sources/leenkx/logicnode/DrawImageSequenceNode.hx +++ b/leenkx/Sources/leenkx/logicnode/DrawImageSequenceNode.hx @@ -99,8 +99,6 @@ class DrawImageSequenceNode extends LogicNode { final colorVec = inputs[4].get(); g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); - trace(currentImgIdx); - g.drawScaledImage(images[currentImgIdx], inputs[5].get(), inputs[6].get(), inputs[7].get(), inputs[8].get()); } } diff --git a/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx b/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx new file mode 100644 index 00000000..4318c1c7 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/DrawSubImageNode.hx @@ -0,0 +1,59 @@ +package leenkx.logicnode; + +import iron.math.Vec4; +import kha.Image; +import kha.Color; +import leenkx.renderpath.RenderToTexture; + +class DrawSubImageNode extends LogicNode { + var img: Image; + var lastImgName = ""; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + RenderToTexture.ensure2DContext("DrawImageNode"); + + final imgName: String = inputs[1].get(); + final colorVec: Vec4 = inputs[2].get(); + final anchorH: Int = inputs[3].get(); + final anchorV: Int = inputs[4].get(); + final x: Float = inputs[5].get(); + final y: Float = inputs[6].get(); + final width: Float = inputs[7].get(); + final height: Float = inputs[8].get(); + final sx: Float = inputs[9].get(); + final sy: Float = inputs[10].get(); + final swidth: Float = inputs[11].get(); + final sheight: Float = inputs[12].get(); + final angle: Float = inputs[13].get(); + + final drawx = x - 0.5 * width * anchorH; + final drawy = y - 0.5 * height * anchorV; + final sdrawx = sx - 0.5 * swidth * anchorH; + final sdrawy = sy - 0.5 * sheight * anchorV; + + RenderToTexture.g.rotate(angle, x, y); + + if (imgName != lastImgName) { + // Load new image + lastImgName = imgName; + iron.data.Data.getImage(imgName, (image: Image) -> { + img = image; + }); + } + + if (img == null) { + runOutput(0); + return; + } + + RenderToTexture.g.color = Color.fromFloats(colorVec.x, colorVec.y, colorVec.z, colorVec.w); + RenderToTexture.g.drawScaledSubImage(img, sdrawx, sdrawy, swidth, sheight, drawx, drawy, width, height); + RenderToTexture.g.rotate(-angle, x, y); + + runOutput(0); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/ProbabilisticOutputNode.hx b/leenkx/Sources/leenkx/logicnode/ProbabilisticOutputNode.hx index a53ae7df..5c3f2b6b 100644 --- a/leenkx/Sources/leenkx/logicnode/ProbabilisticOutputNode.hx +++ b/leenkx/Sources/leenkx/logicnode/ProbabilisticOutputNode.hx @@ -18,7 +18,6 @@ class ProbabilisticOutputNode extends LogicNode { } if (sum > 1){ - trace(sum); for (p in 0...probs.length) probs[p] /= sum; } diff --git a/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx b/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx new file mode 100644 index 00000000..8fc0a8e7 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/WriteImageNode.hx @@ -0,0 +1,105 @@ +package leenkx.logicnode; + +import iron.object.CameraObject; + +class WriteImageNode extends LogicNode { + + var file: String; + var camera: CameraObject; + var renderTarget: kha.Image; + + public function new(tree: LogicTree) { + super(tree); + } + + override function run(from: Int) { + // Relative or absolute path to file + file = inputs[1].get(); + + assert(Error, iron.App.w() % inputs[3].get() == 0 && iron.App.h() % inputs[4].get() == 0, "Aspect ratio must match display resolution ratio"); + + camera = inputs[2].get(); + renderTarget = kha.Image.createRenderTarget(inputs[3].get(), inputs[4].get(), + kha.graphics4.TextureFormat.RGBA32, + kha.graphics4.DepthStencilFormat.NoDepthAndStencil); + + tree.notifyOnRender(render); + + } + + function render(g: kha.graphics4.Graphics) { + + var ready = false; + final sceneCam = iron.Scene.active.camera; + final oldRT = camera.renderTarget; + + iron.Scene.active.camera = camera; + camera.renderTarget = renderTarget; + + camera.renderFrame(g); + + var tex = camera.renderTarget; + + camera.renderTarget = oldRT; + iron.Scene.active.camera = sceneCam; + + var pixels = tex.getPixels(); + + for (i in 0...pixels.length){ + if (pixels.get(i) != 0){ ready = true; break; } + } + + //wait for getPixels ready + if (ready) { + + var tx = inputs[5].get(); + var ty = inputs[6].get(); + var tw = inputs[7].get(); + var th = inputs[8].get(); + + var bo = new haxe.io.BytesOutput(); + var rgb = haxe.io.Bytes.alloc(tw * th * 4); + for (j in ty...ty + th) { + for (i in tx...tx + tw) { + var k = j * tex.width + i; + var m = (j - ty) * tw + i - tx; + + #if kha_krom + var l = k; + #elseif kha_html5 + var l = (tex.height - j) * tex.width + i; + #end + + //ARGB 0xff + rgb.set(m * 4 + 0, pixels.get(l * 4 + 3)); + rgb.set(m * 4 + 1, pixels.get(l * 4 + 0)); + rgb.set(m * 4 + 2, pixels.get(l * 4 + 1)); + rgb.set(m * 4 + 3, pixels.get(l * 4 + 2)); + } + } + + var imgwriter = new iron.format.bmp.Writer(bo); + imgwriter.write(iron.format.bmp.Tools.buildFromARGB(tw, th, rgb)); + + #if kha_krom + Krom.fileSaveBytes(Krom.getFilesLocation() + "/" + file, bo.getBytes().getData()); + + #elseif kha_html5 + var blob = new js.html.Blob([bo.getBytes().getData()], {type: "application"}); + var url = js.html.URL.createObjectURL(blob); + var a = cast(js.Browser.document.createElement("a"), js.html.AnchorElement); + a.href = url; + a.download = file; + a.click(); + js.html.URL.revokeObjectURL(url); + #end + + runOutput(0); + + tree.removeRender(render); + + } + + } + +} diff --git a/leenkx/blender/lnx/logicnode/draw/LN_draw_camera_texture.py b/leenkx/blender/lnx/logicnode/draw/LN_draw_camera_texture.py index c2c505d1..47fe3ed6 100644 --- a/leenkx/blender/lnx/logicnode/draw/LN_draw_camera_texture.py +++ b/leenkx/blender/lnx/logicnode/draw/LN_draw_camera_texture.py @@ -11,14 +11,15 @@ class DrawCameraTextureNode(LnxLogicTreeNode): @input Object: Object of which to choose the material in the `Material Slot` input. @input Material Slot: Index of the material slot of which the diffuse texture is replaced with the camera's render target. - + @input Node: Node name of the Image Texture Node. + @output On Start: Activated after the `Start` input has been activated. @output On Stop: Activated after the `Stop` input has been activated. """ bl_idname = 'LNDrawCameraTextureNode' bl_label = 'Draw Camera to Texture' lnx_section = 'draw' - lnx_version = 1 + lnx_version = 2 def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'Start') @@ -26,6 +27,13 @@ class DrawCameraTextureNode(LnxLogicTreeNode): self.add_input('LnxNodeSocketObject', 'Camera') self.add_input('LnxNodeSocketObject', 'Object') self.add_input('LnxIntSocket', 'Material Slot') - + self.add_input('LnxStringSocket', 'Node') + self.add_output('LnxNodeSocketAction', 'On Start') self.add_output('LnxNodeSocketAction', 'On Stop') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.lnx_version not in (0, 1): + raise LookupError() + + return NodeReplacement.Identity(self) \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py b/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py new file mode 100644 index 00000000..7559e61f --- /dev/null +++ b/leenkx/blender/lnx/logicnode/draw/LN_draw_sub_image.py @@ -0,0 +1,44 @@ +from lnx.logicnode.lnx_nodes import * + + +class DrawSubImageNode(LnxLogicTreeNode): + """Draws an image. + @input Draw: Activate to draw the image on this frame. The input must + be (indirectly) called from an `On Render2D` node. + @input Image: The filename of the image. + @input Color: The color that the image's pixels are multiplied with. + @input Left/Center/Right: Horizontal anchor point of the image. + 0 = Left, 1 = Center, 2 = Right + @input Top/Middle/Bottom: Vertical anchor point of the image. + 0 = Top, 1 = Middle, 2 = Bottom + @input X/Y: Position of the anchor point in pixels. + @input Width/Height: Size of the sub image in pixels. + @input sX/Y: Position of the sub anchor point in pixels. + @input sWidth/Height: Size of the image in pixels. + @input Angle: Rotation angle in radians. Image will be rotated cloclwiswe + at the anchor point. + @output Out: Activated after the image has been drawn. + @see [`kha.graphics2.Graphics.drawImage()`](http://kha.tech/api/kha/graphics2/Graphics.html#drawImage). + """ + bl_idname = 'LNDrawSubImageNode' + bl_label = 'Draw Sub Image' + lnx_section = 'draw' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'Draw') + self.add_input('LnxStringSocket', 'Image File') + self.add_input('LnxColorSocket', 'Color', default_value=[1.0, 1.0, 1.0, 1.0]) + self.add_input('LnxIntSocket', '0/1/2 = Left/Center/Right', default_value=0) + self.add_input('LnxIntSocket', '0/1/2 = Top/Middle/Bottom', default_value=0) + self.add_input('LnxFloatSocket', 'X') + self.add_input('LnxFloatSocket', 'Y') + self.add_input('LnxFloatSocket', 'Width') + self.add_input('LnxFloatSocket', 'Height') + self.add_input('LnxFloatSocket', 'sX') + self.add_input('LnxFloatSocket', 'sY') + self.add_input('LnxFloatSocket', 'sWidth') + self.add_input('LnxFloatSocket', 'sHeight') + self.add_input('LnxFloatSocket', 'Angle') + + self.add_output('LnxNodeSocketAction', 'Out') \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/native/LN_write_file.py b/leenkx/blender/lnx/logicnode/native/LN_write_file.py index c275ee3e..7ea1ac62 100644 --- a/leenkx/blender/lnx/logicnode/native/LN_write_file.py +++ b/leenkx/blender/lnx/logicnode/native/LN_write_file.py @@ -5,8 +5,6 @@ class WriteFileNode(LnxLogicTreeNode): """Writes the given string content to the given file. If the file already exists, the existing content of the file is overwritten. - > **This node is currently only implemented on Krom** - @input File: the name of the file, relative to `Krom.getFilesLocation()` @input Content: the content to write to the file. diff --git a/leenkx/blender/lnx/logicnode/native/LN_write_image.py b/leenkx/blender/lnx/logicnode/native/LN_write_image.py new file mode 100644 index 00000000..87cab808 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/native/LN_write_image.py @@ -0,0 +1,34 @@ +from lnx.logicnode.lnx_nodes import * + + +class WriteImageNode(LnxLogicTreeNode): + """Writes the given image to the given file. If the image + already exists, the existing content of the image is overwritten. + Aspect ratio must match display resolution ratio. + @input Image File: the name of the image, relative to `Krom.getFilesLocation()` + @input Camera: the render target image of the camera to write to the image file. + @input Width: width of the image file. + @input Height: heigth of the image file. + @input sX: sub position of first x pixel of the sub image (0 for start). + @input sY: sub position of first y pixel of the sub image (0 for start). + @input sWidth: width of the sub image. + @input sHeight: height of the sub image. + @seeNode Read File + """ + bl_idname = 'LNWriteImageNode' + bl_label = 'Write Image' + lnx_section = 'file' + lnx_version = 1 + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxStringSocket', 'Image File') + self.add_input('LnxNodeSocketObject', 'Camera') + self.add_input('LnxIntSocket', 'Width') + self.add_input('LnxIntSocket', 'Height') + self.add_input('LnxIntSocket', 'sX') + self.add_input('LnxIntSocket', 'sY') + self.add_input('LnxIntSocket', 'sWidth') + self.add_input('LnxIntSocket', 'sHeight') + + self.add_output('LnxNodeSocketAction', 'Out') \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/native/LN_write_json.py b/leenkx/blender/lnx/logicnode/native/LN_write_json.py index 779262eb..b7a7264b 100644 --- a/leenkx/blender/lnx/logicnode/native/LN_write_json.py +++ b/leenkx/blender/lnx/logicnode/native/LN_write_json.py @@ -5,8 +5,6 @@ class WriteJsonNode(LnxLogicTreeNode): """Writes the given content to the given JSON file. If the file already exists, the existing content of the file is overwritten. - > **This node is currently only implemented on Krom** - @input File: the name of the file, relative to `Krom.getFilesLocation()`, including the file extension. @input Dynamic: the content to write to the file. Can be any type that can diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index cff51f1b..0a7927e0 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -233,7 +233,7 @@ def build(): wrd.compo_defs += '_CGrain' if rpdat.lnx_sharpen: wrd.compo_defs += '_CSharpen' - if bpy.data.scenes[0].view_settings.exposure != 0.0: + if bpy.utils.get_active_scene().view_settings.exposure != 0.0: wrd.compo_defs += '_CExposure' if rpdat.lnx_fog: wrd.compo_defs += '_CFog' diff --git a/leenkx/blender/lnx/material/cycles_functions.py b/leenkx/blender/lnx/material/cycles_functions.py index f0a84196..1ba23211 100644 --- a/leenkx/blender/lnx/material/cycles_functions.py +++ b/leenkx/blender/lnx/material/cycles_functions.py @@ -298,6 +298,125 @@ float tex_brick_f(vec3 p) { } """ + + +#https://github.com/blender/blender/blob/main/source/blender/gpu/shaders/material/gpu_shader_material_tex_brick.glsl +str_tex_brick_blender = """ +float integer_noise(int n) +{ + /* Integer bit-shifts for these calculations can cause precision problems on macOS. + * Using uint resolves these issues. */ + uint nn; + nn = (uint(n) + 1013u) & 0x7fffffffu; + nn = (nn >> 13u) ^ nn; + nn = (uint(nn * (nn * nn * 60493u + 19990303u)) + 1376312589u) & 0x7fffffffu; + return 0.5f * (float(nn) / 1073741824.0f); +} +vec2 calc_brick_texture(vec3 p, + float mortar_size, + float mortar_smooth, + float bias, + float brick_width, + float row_height, + float offset_amount, + int offset_frequency, + float squash_amount, + int squash_frequency) +{ + int bricknum, rownum; + float offset = 0.0f; + float x, y; + rownum = int(floor(p.y / row_height)); + if (offset_frequency != 0 && squash_frequency != 0) { + brick_width *= (rownum % squash_frequency != 0) ? 1.0f : squash_amount; /* squash */ + offset = (rownum % offset_frequency != 0) ? 0.0f : (brick_width * offset_amount); /* offset */ + } + bricknum = int(floor((p.x + offset) / brick_width)); + x = (p.x + offset) - brick_width * bricknum; + y = p.y - row_height * rownum; + float tint = clamp((integer_noise((rownum << 16) + (bricknum & 0xFFFF)) + bias), 0.0f, 1.0f); + float min_dist = min(min(x, y), min(brick_width - x, row_height - y)); + if (min_dist >= mortar_size) { + return vec2(tint, 0.0f); + } + else if (mortar_smooth == 0.0f) { + return vec2(tint, 1.0f); + } + else { + min_dist = 1.0f - min_dist / mortar_size; + return vec2(tint, smoothstep(0.0f, mortar_smooth, min_dist)); + } +} +vec3 tex_brick_blender(vec3 co, + vec3 color1, + vec3 color2, + vec3 mortar, + float scale, + float mortar_size, + float mortar_smooth, + float bias, + float brick_width, + float row_height, + float offset_amount, + float offset_frequency, + float squash_amount, + float squash_frequency) +{ + vec2 f2 = calc_brick_texture(co * scale, + mortar_size, + mortar_smooth, + bias, + brick_width, + row_height, + offset_amount, + int(offset_frequency), + squash_amount, + int(squash_frequency)); + float tint = f2.x; + float f = f2.y; + if (f != 1.0f) { + float facm = 1.0f - tint; + color1 = facm * color1 + tint * color2; + } + return mix(color1, mortar, f); +} +float tex_brick_blender_f(vec3 co, + vec3 color1, + vec3 color2, + vec3 mortar, + float scale, + float mortar_size, + float mortar_smooth, + float bias, + float brick_width, + float row_height, + float offset_amount, + float offset_frequency, + float squash_amount, + float squash_frequency) +{ + vec2 f2 = calc_brick_texture(co * scale, + mortar_size, + mortar_smooth, + bias, + brick_width, + row_height, + offset_amount, + int(offset_frequency), + squash_amount, + int(squash_frequency)); + float tint = f2.x; + float f = f2.y; + if (f != 1.0f) { + float facm = 1.0f - tint; + color1 = facm * color1 + tint * color2; + } + return f; +} +""" + + + str_tex_wave = """ float tex_wave_f(const vec3 p, const int type, const int profile, const float dist, const float detail, const float detail_scale) { float n; diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py index e0615c1b..0cee7349 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py @@ -29,24 +29,35 @@ else: def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: - state.curshader.add_function(c_functions.str_tex_brick) + state.curshader.add_function(c_functions.str_tex_brick_blender) if node.inputs[0].is_linked: co = c.parse_vector_input(node.inputs[0]) else: co = 'bposition' + offset_amount = node.offset + offset_frequency = node.offset_frequency + squash_amount = node.squash + squash_frequency = node.squash_frequency + + col1 = c.parse_vector_input(node.inputs[1]) + col2 = c.parse_vector_input(node.inputs[2]) + col3 = c.parse_vector_input(node.inputs[3]) + scale = c.parse_value_input(node.inputs[4]) + mortar_size = c.parse_value_input(node.inputs[5]) + mortar_smooth = c.parse_value_input(node.inputs[6]) + bias = c.parse_value_input(node.inputs[7]) + brick_width = c.parse_value_input(node.inputs[8]) + row_height = c.parse_value_input(node.inputs[9]) + #res = f'tex_brick({co} * {scale}, {col1}, {col2}, {col3})' + # Color if out_socket == node.outputs[0]: - col1 = c.parse_vector_input(node.inputs[1]) - col2 = c.parse_vector_input(node.inputs[2]) - col3 = c.parse_vector_input(node.inputs[3]) - scale = c.parse_value_input(node.inputs[4]) - res = f'tex_brick({co} * {scale}, {col1}, {col2}, {col3})' + res = f'tex_brick_blender({co}, {col1}, {col2}, {col3}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})' # Fac else: - scale = c.parse_value_input(node.inputs[4]) - res = 'tex_brick_f({0} * {1})'.format(co, scale) + res = f'tex_brick_blender_f({co}, {col1}, {col2}, {col3}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})' return res diff --git a/leenkx/blender/lnx/nodes_logic.py b/leenkx/blender/lnx/nodes_logic.py index 1babb945..a5917029 100644 --- a/leenkx/blender/lnx/nodes_logic.py +++ b/leenkx/blender/lnx/nodes_logic.py @@ -77,7 +77,7 @@ class LNX_MT_NodeAddOverride(bpy.types.Menu): layout.separator() layout.menu(f'LNX_MT_{INTERNAL_GROUPS_MENU_ID}_menu', text=internal_groups_menu_class.bl_label, icon='OUTLINER_OB_GROUP_INSTANCE') - elif context.space_data.tree_type == 'ShaderNodeTree': + elif context.space_data.tree_type == 'ShaderNodeTree' and bpy.app.version > (4, 0, 0): # TO DO - Recursively gather nodes and draw them to menu LNX_MT_NodeAddOverride.overridden_draw(self, context) diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 415b331f..73683a5e 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -781,9 +781,9 @@ const vec3 compoLetterboxColor = vec3(""" + str(round(rpdat.lnx_letterbox_color[ """const float compoSharpenStrength = """ + str(round(rpdat.lnx_sharpen_strength * 100) / 100) + """; """) - if bpy.data.scenes[0].view_settings.exposure != 0.0: + if bpy.utils.get_active_scene().view_settings.exposure != 0.0: f.write( -"""const float compoExposureStrength = """ + str(round(bpy.data.scenes[0].view_settings.exposure * 100) / 100) + """; +"""const float compoExposureStrength = """ + str(round(bpy.utils.get_active_scene().view_settings.exposure * 100) / 100) + """; """) if rpdat.lnx_fog: