因为只是简单地表示逻辑和方法,并没有追求性能。本文的代码过于简单,可能会让高手感到不适,尽请谅解。
借助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.作图的基本方法
放置像素
在放置像素
。
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])
对位置的像素作指定处理。
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被换成了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)]
基于邻接序列的阴影和逐面显示方法。
更多的材质。
自动签字。
提高性能。
用户界面与用户体验。
插件支持。
预览。
辅助参考线。
接入到基础绘画软件。
扁平化和提高兼容性。