一个简单的像素画制作工具的python实现

因为只是简单地表示逻辑和方法,并没有追求性能。本文的代码过于简单,可能会让高手感到不适,尽请谅解。

借助python和Pillow实现。

最终效果

在控制台输入指令,实现像素方块的堆放。


1.基本画布的构建

用0表示占位数据。

1.1.基础数据

像素

采用RGBA模式的32位颜色。

pixel = [0, 0, 0, 0]

画布

为了测试方便,将画布规格固定为49*49。

每一项都是一个RGBA模式的像素。

graph = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]

1.2.中间数据

Image类是枢纽

新建一个黑色透明的Image类,并保存为png文件。

from PIL import Image

image = Image.new("RGBA", (49, 49))
image.save("maps/test.png")

从指定地址打开png文件,转化为Image类。

from PIL import Image

image = Image.open("maps/test.png")

将一个Image类转化为画布。

from PIL import Image

image = Image.new('RGBA', (49, 49))
pixels = [[list(image.getpixel((x, y))) for x in range(image.width)] for y in range(image.height)]

for row in pixels:
    print(row)

新建画布,并转化为Image类。

from PIL import Image

pixels = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]
image = Image.frombytes('RGBA', (49, 49), bytes([channel for row in pixels for pixel in row for channel in pixel]))
image.save("maps/test.png")

借助json以便于存储

新建画布,并存储为json。

import json

pixels = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]
with open("maps/test.json", "w") as f:
    json.dump(pixels, f)

导入json文件,作为画布。

import json

with open("maps/test.json", "r") as f:
    pixels = json.load(f)

for row in pixels:
    print(row)

1.3.作图的基本方法

放置像素

(1, 2)放置像素(1,2,3,4)

pixels = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]

pixels[0][1] = [1, 2, 3, 4]
print(pixels[0])

(1,2)位置的像素作指定处理。

pixels = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]
pixels[0][1] = [1, 2, 3, 4]

def add_pixels(pixel, increment):
    new_pixel = [0, 0, 0, 0]
    new_pixel[0] = pixel[0] + increment[0]  # 红
    new_pixel[1] = pixel[1] + increment[1]  # 绿
    new_pixel[2] = pixel[2] + increment[2]  # 蓝
    new_pixel[3] = pixel[3] + increment[3]  # Alpha
    return new_pixel

increment = [1, 3, 3, 6]
pixels[0][1] = add_pixels(pixels[0][1], increment)

print(pixels[0][1])  # [2, 5, 6, 10]

从空间坐标到平面坐标

输入值是方块相对于锚的位置,返回值是方块的图形绘制起点在画布中的坐标。

关于其数学背景,可以参考

散点透视映射的构建

def pos(x, y, z):
    m = 18 - 6 * x + 6 * y
    n = 24 + 2 * x + 2 * y - 8 * z
    return (m, n)

m, n = pos(1, 2, 3)
print(m, n)

2.设计资源文件

先不急着把软件做出来,先想想怎么表示数据。

2.1.资源包

希望实现这么一种效果。提前把资源文件准备好,到时候直接告诉计算机,那个位置的方块的图形是什么。

每个方块都是13*13的RGBA的列表,一个资源包存放16个这样的方块。

资源包的数据模型如下:

chc = [[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for n in range(16)]

首先,借助绘画软件,画一些方块。

导出为png文件后,转化为便于应用的json文件。

#打开一个图片,然后转化为资源包
from PIL import Image
import json

n = 0
image = Image.open("src/laer.png")
graph = [[[(0, 0, 0, 0) for i in range(13)] for j in range(13)] for n in range(16)]
for y in range(4):
    for x in range(4):
        for j in range(13):
            n = 4 * y + x
            for i in range(13):
                graph[n][j][i] = image.getpixel((16 * y + j, 16 * x + i))

with open("chc/laer.json", "w") as f:
    json.dump(graph, f)

以下用于测试的脚本,展示了访问材质包数据的基本形式。

#检查某个方块的某个位置的像素值
import json
import re

help = """[help]
check [n] [u] [v]: 检查第n个方块的(u, v)像素值
"""

with open("chc/laer.json", "r") as f:
    chc = json.load(f)

def check(n, u, v):
    rgba = chc[n][u][v]
    print(f"r = {rgba[0]}\ng = {rgba[1]}\nb = {rgba[2]}\na = {rgba[3]}")

while True:
    ln = input("_")
    if ln == "":
        break
    elif me := re.match(r"check (\d+) (\d+) (\d+)", ln):
        check(int(me.group(1)), int(me.group(2)), int(me.group(3)))
    else:
        print(help)

输入指令进行测试:

验证结果表明,数据模型的实现与原设计是一致的。

之后,如果想画某个方块,直接访问资源包即可。

2.2.地图

地图可以是预制的。

为了测试方便,采用4*4*4的地图。用数字序号表示方块,0表示无方块。

例如,6,表示这个项对应的位置的方块的图形,在材质包的第六位。

block_id = [[[0 for x in range(4)] for y in range(4)] for z in range(4)]

初始化,创建一个空白地图。

import json

tr_blank = [[[0 for x in range(4)] for y in range(4)] for z in range(4)]

with open("tr/blank.json", "w") as f:
    json.dump(tr_blank, f)

现在想设计一个能制作地图的软件。设计一下希望有的功能。

> new (文件名):创建新的地图
> open (文件名):打开地图文件
> select (方块名):选取方块笔刷
> set (x)(y)(z):在指定位置放置方块(自动保存)
> done:退出程序
> ......

分为三个模块。Block就是地图文件管理器,Mon是绘制器,Starlit用于全局控制。

设计好之后,完全可以让AI帮忙写。kimi擅长对代码的分析和优化,deepseek擅长检查错误(大多是kimi写的错误)。

import json
import re

class Block:
    def __init__(self):
        self.block = []
        self.name = ""

    def set_name(self, name):
        self.name = "tr/" + name + ".json"

    def load(self, name):
        self.set_name(name)
        with open(self.name, "r") as f:
            self.block = json.load(f)

    def new(self, name):
        self.set_name(name)
        self.block = [[[0 for _ in range(4)] for _ in range(4)] for _ in range(4)]
        self.save()

    def save(self):
        print(self.name)
        with open(self.name, "w") as f:
            json.dump(self.block, f)

    def output(self, name):
        with open(name, "w") as f:
            json.dump(self.block, f)

    def done(self):
        print(f"地图保存至{self.name}")

class Mon:
    def __init__(self, block):
        self.draw_ = 0
        self.clean_ = 0
        self.block_ = block

    def draw(self, n):
        self.draw_ = n

    def point(self, x, y, z):
        if self.clean_:
            self.block_.block[z][y][x] = 0
        else:
            self.block_.block[z][y][x] = self.draw_
        self.block_.save()

    def clean(self):
        self.clean_ = 1 - self.clean_

class Starlit:
    def __init__(self):
        self._block = Block()
        self._mon = Mon(self._block)
        self.ln = ""
        self.help = """[help]____________________________
load [name]: 导入地图
new [name]: 新建地图
draw [n]: 设置的笔刷
point [x][y][z]: 放置/移除方块
clean: 删除模式/取消删除模式
done: 退出程序
output [name]: 导出
__________________________________
"""

    def main(self):
        while True:
            self.ln = input("_").strip()
            if not self.ln:
                continue

            if cmd := re.match(r"load (\w+)", self.ln):
                self._block.load(cmd.group(1))
            elif cmd := re.match(r"new (\w+)", self.ln):
                self._block.new(cmd.group(1))
            elif cmd := re.match(r"draw (\d+)", self.ln):
                self._mon.draw(int(cmd.group(1)))
            elif cmd := re.match(r"point (\d+) (\d+) (\d+)", self.ln):
                x, y, z = map(int, cmd.groups())
                self._mon.point(x, y, z)
            elif self.ln == "clean":
                self._mon.clean()
            elif self.ln == "done":
                self._block.done()
                break
            elif cmd := re.match(r"output (\w+)", self.ln):
                self._block.output(cmd.group(1))
            else:
                print(self.help)

_tr = Starlit()
_tr.main()

创建一个空的地图,用于测试。

按照预期,有且只有(0,0,1)(0,0,2)的0被换成了6,即放置了两个序号为6的方块。

检查一下test.json。

[[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[6, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[6, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]]

验证成立,数据模型的实现与原设计是一致的。

至于更多的功能,UI,这没那么重要,暂时不管了。

3.实现

3.1.遍历数组

信息源是tr,所谓的“地图”。本质上是让控制流在遍历tr。

tr = [[[0 for x in range(4)] for y in range(4)] for z in range(4)]

for z in range(4):
    # 对于 z = 0, 1, 2, 3, 执行以下操作
    for y in range(4):
        # 对于 y = 0, 1, 2, 3, 执行以下操作
        for x in range(4):
            # 对于 x = 0, 1, 2, 3, 执行以下操作
            if tr[z][y][x] != 0:
                print(f"to set cube at {x} {y} {z} with {tr[z][y][x]} in chc.")

比如说block_pickel的构建。

import copy

tr = [[[0 for x in range(4)] for y in range(4)] for z in range(4)]  # I
chc = [[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for n in range(16)]  # I
block_pixels = [[[[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for x in range(4)] for y in range(4)] for z in range(4)]  # O

ch = None  # 影

for z in range(4):
    for y in range(4):
        for x in range(4):
            if tr[z][y][x] != 0:
                ch = chc[tr[z][y][x]]
                block_pixels[z][y][x] = copy.deepcopy(ch) # 硬拷贝

为什么不直接构成最终的图形呢?因为以后还会添加阴影,动态变化,镜像之类的渲染,用间接的方法映射数据,清晰而便于拓展。

从block_pixels变换到graph,实现内存的扁平化。

block_pixels = [[[[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for x in range(4)] for y in range(4)] for z in range(4)]  # I
graph = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]  # O

def pos(x, y, z):
    m = 18 - 6 * x + 6 * y
    n = 24 + 2 * x + 2 * y - 8 * z
    return (m, n)

# 起笔坐标
m = None
n = None

for z in range(4):
    for y in range(4):
        for x in range(4):
            m, n = pos(x, y, z)
            for j in range(13):
                for i in range(13):
                    for k in range(4):
                        graph[n + j][m + i][k] = block_pixels[z][y][x][j][i][k]  # 避免不正确拷贝导致的连锁修改

最后一次遍历是将graph导出为png文件,之前介绍过了,不提。

3.2.像素的叠加

根据乘法原理,将不透明度看作遮光能力,理想地分析三原色的拦截率。

得到基本公式如下。

def mix(r1, g1, b1, a1, r2, g2, b2, a2):
    a = a1 + a2 * (1 - a1 / 255)
    if a == 0:
        return [0, 0, 0, 0]
    f = a1 / a
    r = int(r1 * f + r2 * (1 - f))
    g = int(g1 * f + g2 * (1 - f))
    b = int(b1 * f + b2 * (1 - f))
    return [r, g, b, int(a)]

print(mix(22, 90, 2, 2, 0, 0, 0, 2))

于是可以将含不透明度的方块图形叠加到总画布。

def mix(r1, g1, b1, a1, r2, g2, b2, a2):
    a = a1 + a2 * (1 - a1 / 255)
    if a == 0:
        return [0, 0, 0, 0]
    f = a1 / a
    r = int(r1 * f + r2 * (1 - f))
    g = int(g1 * f + g2 * (1 - f))
    b = int(b1 * f + b2 * (1 - f))
    return [r, g, b, int(a)]

block_pixels = [[[[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for x in range(4)] for y in range(4)] for z in range(4)]  # I
graph = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]  # O

def pos(x, y, z):
    m = 18 - 6 * x + 6 * y
    n = 24 + 2 * x + 2 * y - 8 * z
    return (m, n)

# 起笔坐标
m = None
n = None

for z in range(4):
    for y in range(4):
        for x in range(4):
            m, n = pos(x, y, z)
            for j in range(13):
                for i in range(13):
                    for k in range(4):
                        pixel1 = block_pixels[z][y][x][j][i]
                        pixel2 = graph[n + j][m + i]
                        mixed_pixel = mix(pixel1[0], pixel1[1], pixel1[2], pixel1[3],
                                          pixel2[0], pixel2[1], pixel2[2], pixel2[3])
                        graph[n + j][m + i] = mixed_pixel

3.3.脚本的结构

之前已经做出了全部的功能,现在完成导出方法。

help = '''[help]
chc [name] : 指定资源包
tr [name] : 指定地图
output [name] : 制作
'''

在基础设计上做出指令结构。

一共有三个功能,两个导入和一个导出。

import re

help = '''[help]
chc [name] : 指定资源包
tr [name] : 指定地图
output [name] : 制作
按下回车键以退出
'''

def chc(name):
    print(f"chc for {name}")

def tr(name):
    print(f"tr for {name}")

def output(name):
    print(f"output for {name}")


while True:
    ln = input("_")
    if ln == "":
        break
    elif cmd := re.match(r"chc (\w+)", ln):
        chc(cmd.group(1))
    elif cmd := re.match(r"tr (\w+)", ln):
        tr(cmd.group(1))
    elif cmd := re.match(r"output (\w+)", ln):
        output(cmd.group(1))
    else:
        print(help)

3.4.从输入到输出

将之前的数据模型实现出来。

准备阶段,输入是chc_pt > chc 和tr_pt > tr 

构建阶段,先制作block_pixels,再按部就班地导出。

思路清楚了。

import re

help = '''[help]
chc [name] : 指定资源包
tr [name] : 指定地图
output [name] : 制作
按下回车键以退出
'''

chc_pt = None
tr_pt = None

chc = [[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for n in range(16)]
tr = [[[0 for x in range(4)] for y in range(4)] for z in range(4)]

# 线性映射
def pos(x, y, z):
    m = 18 - 6 * x + 6 * y
    n = 24 + 2 * x + 2 * y - 8 * z
    return (m, n)

# rgba混合
def mix(r1, g1, b1, a1, r2, g2, b2, a2):
    a = a1 + a2 * (1 - a1 / 255)
    if a == 0:
        return [0, 0, 0, 0]
    f = a1 / a
    r = int(r1 * f + r2 * (1 - f))
    g = int(g1 * f + g2 * (1 - f))
    b = int(b1 * f + b2 * (1 - f))
    return [r, g, b, int(a)]


def chc(name):
    print(f"chc for {name}")
    # 导入chc

def tr(name):
    print(f"tr for {name}")
    # 导入tr

def output(name):
    print(f"output for {name}")
    # 生成block_pixels
    # 生成graph
    # 导出


while True:
    ln = input("_")
    if ln == "":
        break
    elif cmd := re.match(r"chc (\w+)", ln):
        chc(cmd.group(1))
    elif cmd := re.match(r"tr (\w+)", ln):
        tr(cmd.group(1))
    elif cmd := re.match(r"output (\w+)", ln):
        output(cmd.group(1))
    else:
        print(help)

模块化地将之前的设计放进去。

import re
import json
from PIL import Image
import copy

help = '''[help]
输入指令以执行操作。
按下回车键以执行。
chc [name] : 指定资源包
tr [name] : 指定地图
output [name] : 制作
在空行按下回车键以退出。
'''

class F:
    def __init__(self, type):
        self.name = None
        self.data = None
        self.type = type

    def load(self):
        print("load " + self.name)
        with open(self.name, "r") as f:
            self.data = json.load(f)

# 线性映射
def pos(x, y, z):
    m = 18 - 6 * x + 6 * y
    n = 24 + 2 * x + 2 * y - 8 * z
    return (m, n)

# rgba混合
def mix(r1, g1, b1, a1, r2, g2, b2, a2):
    a = a1 + a2 * (1 - a1 / 255)
    if a == 0:
        return [0, 0, 0, 0]
    f = a1 / a
    r = int(r1 * f + r2 * (1 - f))
    g = int(g1 * f + g2 * (1 - f))
    b = int(b1 * f + b2 * (1 - f))
    return [r, g, b, int(a)]

class Dvc:
    def __init__(self):
        self.chc_ = F("chc")
        self.tr_ = F("tr")

    def dvc(self):
        while True:
            ln = input("_")
            if ln == "":
                break
            elif cmd := re.match(r"chc (\w+)", ln):
                self.chc(cmd.group(1))
            elif cmd := re.match(r"tr (\w+)", ln):
                self.tr(cmd.group(1))
            elif cmd := re.match(r"output (\w+)", ln):
                self.output(cmd.group(1))
            else:
                print(help)

    def chc(self, name):
        self.chc_.name = "chc/" + name + ".json"
        self.chc_.load()

    def tr(self, name):
        self.tr_.name = "tr/" + name + ".json"
        self.tr_.load()

    def output(self, name):
        block_pixels = [[[[[[0, 0, 0, 0] for u in range(13)] for v in range(13)] for x in range(4)] for y in range(4)] for z in range(4)]
        graph = [[[0, 0, 0, 0] for i in range(49)] for j in range(49)]

        for z in range(4):
            for y in range(4):
                for x in range(4):
                    if self.tr_.data[z][y][x] != 0:
                        ch = self.chc_.data[self.tr_.data[z][y][x]]
                        block_pixels[z][y][x] = copy.deepcopy(ch)

        m = None
        n = None
        for z in range(4):
            for y in range(4):
                for x in range(4):
                    m, n = pos(x, y, z)
                    for j in range(13):
                        for i in range(13):
                            for k in range(4):
                                pixel1 = block_pixels[z][y][x][j][i]
                                pixel2 = graph[n + j][m + i]
                                mixed_pixel = mix(pixel1[0], pixel1[1], pixel1[2], pixel1[3], pixel2[0], pixel2[1], pixel2[2], pixel2[3])
                                graph[n + j][m + i] = mixed_pixel

        image = Image.frombytes('RGBA', (49, 49), bytes([channel for row in graph for pixel in row for channel in pixel]))
        image.save(name + ".png")
        print("done.")

laer = Dvc()
laer.dvc()

4.后续工作

4.1.测试

测试通过,一个思路清晰的脚本就完成了。

谨此结束本文的工作。

4.2.之后的发展方向

有些方块的图形受邻接影响。可以做一下邻接判定,实现对邻接的处理。

block_state = [[[[0, 0, 0, 0, 0, 0, 0, 0] for x in range(4)] for y in range(4)] for z in range(4)]
block_state = [[[0 for x in range(6)] for y in range(6)] for z in range(6)]

基于邻接序列的阴影和逐面显示方法。

更多的材质。

自动签字。

提高性能。

用户界面与用户体验。

插件支持。

预览。

辅助参考线。

接入到基础绘画软件。

扁平化和提高兼容性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值