- 好友
- 0
- 在线时间
- 0 小时
- 最后登录
- 2024-11-30
随仆
- UID
- 3632209
- 第纳尔
- 20
- 精华
- 0
- 互助
- 3
- 荣誉
- 0
- 贡献
- 0
- 魅力
- 20
- 注册时间
- 2024-3-10
鲜花( 4) 鸡蛋( 0)
|
本帖最后由 ChrisRitter 于 2024-4-30 02:11 编辑
简单点说就是两个功能:
1.把图片文件按照一定的规则转化成模组文件中的map.txt;
2.用类似的规则把同一张背景贴图的几个变色副本拼接起来,得到有架空历史地图感的加载页面。
工具:python3(至少有numpy和matplotlib两个库,想压缩dds文件的话还需要wand库及其依赖的ImageMagick)
可选工具:Cartographer
初始图片和最终效果如图:
1.随便找一张照片,我用的是自己在WorldBox里面抹掉所有建筑和植被之后的一张截图(其他风格的照片也可,但可能需要自己调参);
2.用python读取图片,设置规则将像素的不同颜色范围对应成游戏内的地形代码(terrain code);
3.按分辨率生成一个网状点阵,让奇偶列错开,使得三个相邻点之间构成锐角三角形,将这些三角形数据写入map.txt。
做背景图的思路:前两步一样,第三步是去网上找一张泛黄纸张图片当素材,然后调整RGB色值得到图片的几个变色副本用来对应不同地形,最后根据地形代码决定对应位置的像素来自哪种颜色的图片即可。
|
1.楼主的mod基于维京征服,因此地形代码和贴图和原版是有一点差别的,不过影响不大(p.s.:需要维京征服的Module System可以去taleworlds上的官方论坛下载,请阅读原作者的使用须知);
2.要使用这个方法需要能大概看懂python代码,至少能猜得出来一些参数代表什么意思,不会调参的话建议也用WorldBox做照片,这样RGB色值应该是一样的;
3.map.txt和MS(Module System)是相互独立的,本教程的代码只生成地形,城镇坐标需要在MS里面改,这里安利一下Cartographer,可以可视化地加载map.txt和修改module_parties.py,官方论坛上有下载链接和答疑帖;
4.如果地图尺寸和原版偏差较多,需要去module.ini里面改地图边界,我在两个论坛里面看了一晚上的帖子也没找到公认的大地图边界上限,自己试错的结果是主角可以走到的极限大概在±275左右。
|
保姆式教学环节:
首先开一个新的py文件,把用来转换的照片放在同级文件夹下(如果不会python请务必选择一张长宽相同的图片),然后开始写:
(我会在注释里把调参的具体思路写出来)
- import numpy as np
- import matplotlib.image as mpimg
- import random
- #读取图片,把"VG.jpg"改成你图片的名字和后缀
- img = mpimg.imread('VG.jpg').swapaxes(0,1).tolist()#如果生成的地图方向不对,可以试着把这行的“.swapaxes(0,1)”删掉
- rs,cs = len(img),len(img[0])
- #填写不同地形对应的代码(可以在MS的header_terrain_type.py里面看到)
- #你也可以在这里DIY,比如维京征服的沙漠贴图有问题,就可以通过写sand=3来用平原地形代替沙漠
- water = 0
- mountain= 1
- soil0 = 2
- soil1 = 3
- snow = 4
- sand = 5
- #不同地形代码对应的基础高度,0:0.8代表water地形的高度是0.8
- #确保上面出现过的数字都有对应的高度
- height = {0:0.8 ,1:3.6, 2:2, 3:1.4, 4:2, 5:1}
- #将像素的RGB值转换成对应的地形代码
- #原理:img[r][c]是单个像素,在后面加上[0]、[1]、[2]表示R、G、B的取值(范围是0-255)
- for r in range(rs):
- for c in range(cs):
- if img[r][c][0]<120 and img[r][c][2]>100:#比如这里,意思是红色少于120且蓝色大于100的像素一律识别为海洋
- img[r][c] = water
- elif img[r][c][0]<100 and img[r][c][2]<100:
- img[r][c] = mountain
- elif img[r][c][0]>200 and img[r][c][1]>200 and img[r][c][2]<200:
- img[r][c] = sand
- elif c<cs/5 and img[r][c][1]>150 and img[r][c][2]>150:
- img[r][c] = snow
- elif img[r][c][0]>200 and img[r][c][1]>130:
- img[r][c] = soil1
- else: #if img[r][c][0]>150 and img[r][c][2]<100:
- img[r][c] = soil0
- #如果你原图的长宽不相等或需要截取,可以在下一行去掉#并指定范围
- img = np.array(img)#[20:1685,10:1675]
- #旋转地图以对齐北方
- img = img.T[:,::-1]#如果生成的地图方向不对,可以试着把这行删掉
复制代码 这段代码能得到一个二维列表img,其中每个元素都是原像素对应的地形代码
|
在上一段代码后面接着写:- #池化
- #这一步的目的是控制大地图中顶点的总数:比如图像边长为1665个像素,rate=5,则顶点总数为(1665/5)^2,即11万左右,这个例子已经是很大的数字了,战团原版的顶点数为20791,维京征服56923
- #因为后续需要,请保证图像的边长在除以rate之后的整数部分是奇数!!!
- rate = 5
- e,sl = rate//2,len(img)//rate#边长,现在需要是奇数
- dots = np.array([img[c*rate+e,r*rate+e] for c in range(sl) for r in range(sl)]).reshape((sl,sl))#现在感觉这行写复杂了,可以试着改成dots = img[::rate,::rate]
- ele = dots.flatten()
- p_count = sl*sl
- #生成坐标网,并且让奇偶列错位形成三角形
- coor = np.array([(j,i) for i in range(sl) for j in range(sl)])
- a = np.array([(0,0,0,0.5) for i in range(sl) for j in range(sl//2+1)]).reshape((sl,sl+1,2))[:,:-1].reshape((p_count,2))
- coor = coor-a
- #这一步用来调节地图尺寸,amp越大尺寸越大
- amp = 1.7
- coor = (coor-sl/2)*amp*np.array((-1,-1))#如果生成的地图方向不对,可以试着把*np.array((-1,-1))删掉
- #原本用于在计算出三角形地形代码后的调整,现在被弃用
- def t7(i):
- if i==0:
- return i
- else:
- return i
- #生成三角形地形数据
- #原理是取三角形三个顶点所对应的地形代码的最大值作为整个三角形对应的地形代码,因为我的地图里面没有河流(代码8),而海洋(0)是地形代码里最小的,所以这样可以让陆地看上去更像一个凸集
- #中位数和最小值或者取坐标最靠左的顶点我都试过,最后感觉还是max生成的地形看着舒服一点
- #(不要交换顶点在行内的写入顺序,详见后文碎碎念)
- oc = np.arange(p_count).reshape((sl,sl)).T[1::2]
- print(len(ele),oc.shape,oc)
- tl = []
- for c in oc:
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[c[0]-1],ele[c[0]],ele[c[0]+sl])),c[0]-1,c[0],c[0]+sl))
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[c[0]+1],ele[c[0]],ele[c[0]+sl])),c[0],c[0]+1,c[0]+sl))
- for i in c[1:-1]:
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[i-1],ele[i],ele[i+sl])),i-1,i,i+sl))#i-1
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[i+1],ele[i],ele[i+sl])),i,i+1,i+sl))#i+1
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[i-1],ele[i],ele[i-sl-1])),i-sl-1,i,i-1))
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[i+1],ele[i],ele[i-sl+1])),i-sl+1,i+1,i))
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[c[-1]-1],ele[c[-1]],ele[c[-1]-sl-1])),c[-1]-sl-1,c[-1],c[-1]-1))
- tl.append('{} 0 3 {} {} {}\n'.format(t7(max(ele[c[-1]+1],ele[c[-1]],ele[c[-1]-sl+1])),c[-1]-sl+1,c[-1]+1,c[-1]))
- #最后写入12个边界三角形,使地图四周是山地而不是虚空,如果你希望边界以外是海洋,就把1 0 3改成0 0 3
- tl.append('1 0 3 {} {} {}\n'.format(0,p_count+1,p_count))
- tl.append('1 0 3 {} {} {}\n'.format(p_count+1,p_count-sl+1,p_count+2))
- tl.append('1 0 3 {} {} {}\n'.format(p_count-sl+1,p_count+1,0))
- tl.append('1 0 3 {} {} {}\n'.format(p_count+3,0,p_count))
- tl.append('1 0 3 {} {} {}\n'.format(p_count+2,p_count-sl+1,p_count+4))
- tl.append('1 0 3 {} {} {}\n'.format(sl-1,0,p_count+3))
- tl.append('1 0 3 {} {} {}\n'.format(p_count+4,p_count-sl+1,p_count-2))
- tl.append('1 0 3 {} {} {}\n'.format(sl-1,p_count+3,p_count+5))
- tl.append('1 0 3 {} {} {}\n'.format(sl-1,p_count+5,p_count+6))
- tl.append('1 0 3 {} {} {}\n'.format(sl-1,p_count+6,p_count-2))
- tl.append('1 0 3 {} {} {}\n'.format(p_count-2,p_count+6,p_count+7))
- tl.append('1 0 3 {} {} {}\n'.format(p_count-2,p_count+7,p_count+4))
- #写入文件
- if 1:
- with open('map.txt','w') as f:
- f.write(str(p_count+8)+'\n')
- for i in range(len(coor)):
- #翻转等操作可以在这里实现
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(coor[i][0],coor[i][1],height[ele[i]]*(1+random.uniform(0,0.5)**2*(ele[i]==0))))
- #写入8个边界坐标点
- #b_co决定边界的宽度,h决定边界的海拔,这两个参数可以随便改
- b_co,h = 0.6*amp,10
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(b_co*sl,b_co*sl,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(b_co*sl,0,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(b_co*sl,-b_co*sl,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(0,b_co*sl,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(0,-b_co*sl,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(-b_co*sl,b_co*sl,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(-b_co*sl,0,h))
- f.write('{:.6f} {:.6f} {:.6f}\n'.format(-b_co*sl,-b_co*sl,h))
- f.write(str(len(tl))+'\n')
- f.writelines(tl)
复制代码
运行程序后会在py同级文件夹下生成一个map.txt文件,用它替换mod文件夹下对应的map.txt就可以了
|
这个在官方论坛里有外国网友发过,仅凭回忆说一下:
第一行是数字顶点数目m,之后m行是顶点的三维坐标,用空格分隔,即'x y z',如'129.731003 242.634003 1.000000',我记得自己之前试过,也不是非要保留6位小数,但6位小数肯定不会出问题;
之后的一行给出上述顶点组成的需要上色的三角形的数量n,再之后的n行是用空格分割的六位数据't 0 3 d1 d2 d3',每行对应一个三角形,第一位指的是地形代码,第二、三位似乎被弃用了(官方论坛网友说的,我感觉可能是对的,因为全都是0 3,没有例外),四五六位是三个顶点的索引,索引从0开始,顺序和之前那m行的写入顺序相同,相当于第i个点的坐标写在第i+1行。
也就是整个文件的长度是m+n+2行。
三角形顶点之间没有距离的限制,而且三角形可以交叉,也就是说你在写入顶点时改变行顺序会看到一些非常神奇的景象(笑)。游戏中大地图上的party都是坐落在这些三角形上的,而且目测寻路也是基于这些三角形的,如果两片地图之间没有被指定三角形,那么它们之间便无法通行,如果你在相隔很远的地方设置了一个三角形,那么玩家和ai可能就会沿着那个三角形的面在两地之间往返,而这个面往往和地面是不重合的,很有一种走地下通道的感觉。
据此我们可以推测:骑砍的大地图系统可能是兼容天桥或者地下通道,甚至空岛国家和地下城这样的结构的,期待有朝一日有大佬在自己的地图里面加入一个z轴意义上的天国之类的设定的。
关于map.txt还有一个我到现在都还没想清楚的问题:三角形数据中三个顶点是有某种顺序要求的,而且既不是从小到大也不是从大到小,顺序不对会导致这个三角形在Cartographer和游戏引擎里都无法渲染,运气不好就会进一步导致有party的坐标在虚空上,进游戏就报错。上文用到的代码是各种改三个顶点在行内的顺序之后试错出来的,但即便如此我也没从试错结果里面看出什么规律。
顺序错误的结果和这位朋友的情况一致:
萌新关于大地图制作的疑惑 https://bbs.mountblade.com.cn/thread-2101134-1-1.html
所以"展开uv"指的是什么呢,在数据格式层面上有什么具体规范吗?还望有高手解惑
|
然后是制作加载背景图片:
首先,你需要准备一张像这样的图片
这种图网上有一大把,mod不用于商业目的应该也不存在版权问题
然后你需要或截取或压缩,得到一张1024*1024的图片
python可以用
- <div>import matplotlib.image as mpimg
- </div><div>img=mpimg.imread('blabla.jpg')[:1024,:1024]</div><div>mpimg.imsave('blabla1.dds',img)</div>
复制代码 这样的简单方法来实现
这里保存为dds格式是为了之后合并处理更方便,因为dds默认是RGBA,而一些图片格式只是RGB,通道数不一样
接下来瞎调一波颜色,直到你认为这个色块符合你想要的某种地形了为止,然后保存为副本
代码例如:
- import numpy as np
- import matplotlib.image as mpimg
- def normalize(arr):
- arr[arr>255] = 255
- arr[arr<0] = 0
- img = mpimg.imread('VGb.dds').copy().astype('float64')
- a = np.array((1,1,1,10))*100
- img += a
- a = np.array((0.88,1,1.2,10))*0.85
- img *= a
- normalize(img)
-
- img = img.astype('uint8')
- mpimg.imsave("VGs.dds",img)
复制代码 改有"a = np.array(...)"那两行的参数即可,注意保证A通道的数值始终是255,即上例中10*100或10*0.85都比1大
一番调参后,得到这样的结果:
接下来就是合并图像了,新建一个py文件,把最开始那张地形图片也放在同级文件夹复制过来,然后调整参数并运行:
(wand库可能需要你下载ImageMagick,去报错中给的那个地址下一个就行;也可以不用wand库,后果见”关于python处理dds文件的一些经验“)- import numpy as np
- import matplotlib.image as mpimg
- from wand import image#依赖于应用ImageMagick
- def b(x):
- return x.astype(bool)#,1)+np.zeros((1024,1024,4))
-
- def img_save(filename,arr,compression='dxt1',resize=1):
- mpimg.imsave(filename,arr)
- with image.Image(filename=filename) as img:
- if resize:
- img.resize(1024,1024)
- img.compression = compression#1/3/5
- img.save(filename=filename)
- if 1:
- img = mpimg.imread('VG1.dds').copy()#main map
- imgb = mpimg.imread('VGb.dds').copy()#background
- imgs = mpimg.imread('VGs.dds').copy()#snow
- imgw = mpimg.imread('VGw.dds').copy()#water
- imgl = mpimg.imread('VGl.dds').copy()#land
- imgg = mpimg.imread('VGg.dds').copy()#green land
- white = np.array((255,255,255,255))
- mountain = np.array((57,52,41,255))
- sand = np.array((57,52,41,255))
-
- img[b(img[:,:,0]<100)*b(img[:,:,2]<100)] = mountain
- img[b(img[:,:,0]>200)*b(img[:,:,1]>200)*b(img[:,:,2]<200)] = sand
- is_water = b(img[:,:,0]<120)*b(img[:,:,2]>100)
- img[is_water] = imgw[is_water]#water
- is_snow = b(img[:210,:,0]<200)*b(img[:210,:,1]>170)*b(img[:210,:,2]>150)
- img[:210,:,:][is_snow] = imgs[:210,:,:][is_snow]#snow
- is_soil1 = b(img[:,:,0]>200)*b(img[:,:,1]>130)*b(img[:,:,2]<95)
- img[is_soil1] = imgl[is_soil1]#soil1
- is_soil0 = b(img[:,:,0]>150)*b(img[:,:,2]<95)
- img[is_soil0] = imgg[is_soil0]#soil0
-
- img_save("loadscreen_map.dds")
-
复制代码
然后理论上你就会得到一个像这样的文件:
|
原则上这一步方法已经很多了,艺术字去网上随便生成一个就行,然后把艺术字图片改成镂空的,叠在刚刚的dds文件上面。这一步甚至用ppt+微信截图就可以做到(不能读dds的话可以先保存为jpg,叠加完再改尺寸、保存为dds即可)
这里也给一个python代码方案,稍微改一改代码的最后一行还可以用来生成同款模组图标main.bmp,两者原理上是差不多的:
(建议调整好艺术字的尺寸之后再用代码插入图像;另外因为loadscreen.dds是1024*1024的,而游戏载入时会铺满全屏,所以建议把艺术字的x轴调短一点,比如我的分辨率是1920*1080,我就反过来先给横轴尺寸乘上1.08,纵轴尺寸乘上1.92,在此基础上再调整文本的整体尺寸,乘上0.5什么的)
- import numpy as np
- import matplotlib.image as mpimg
- import matplotlib.pyplot as plt
- from wand import image#依赖于应用ImageMagick
-
- def img_save(filename,arr,compression='dxt1',resize=1):
- mpimg.imsave(filename,arr)
- with image.Image(filename=filename) as img:
- if resize:
- img.resize(1024,1024)
- img.compression = compression#1/3/5
- img.save(filename=filename)
- def insert_img(img0,img1,coor,size=1):
- x,y,_ = img1.shape
- to_cover = img1!=img1[0,0]#用左上角的像素来识别背景色
- x0,y0 = coor
- if y0 == 'mid':
- y0 = int((img0.shape[1]-img1.shape[1])/2)
- print(img0.shape,img1.shape)
- img0[x0:x0+x,y0:y0+y][to_cover] = img1[to_cover]
- if 1:
- img0 = mpimg.imread('loadscreen_map.dds').copy()
- img1 = mpimg.imread('title_vg.dds').copy()
- img2 = mpimg.imread('title_x.dds').copy()
- insert_img(img0,img1,(250,'mid'))
- insert_img(img0,img2,(600,'mid'))
-
- img_save("loadscreen.dds",img0,resize=0)
复制代码
|
关于python处理dds文件的一些经验:
dds文件有三种压缩格式,dxt1、dxt3和dxt5,而用matplotlib来写入dds文件时无法进行任何压缩,这就导致1024*1024的dds文件有刚刚好4M的大小,而这个大小用openbrf和文件资源管理器都没法预览,只能在python里面plt.show()。
好消息是游戏引擎事实上可以加载这个大小的dds文件,因此如果懒得下wand库和相关依赖也没有关系,把上文的代码改一改,塞一个4M的文件进texture文件夹就完事了。
坏消息是你会发现texture文件夹里面其他的文件都有预览,就这一个dds文件只显示一个图标,强迫症看了很不舒服,而且估计多多少少会影响性能,如果你想用python来改其他的dds文件呢?
我不确定matplotlib的图像处理是不是直接依赖Pillow的,但反正百度一番后我看了Pillow的说明文档,上面大概意思是Pillow支持dds文件三种压缩格式的解码,但是不支持带压缩格式的写入,我觉得这和我当时调试中遇到的情况完全符合。
之后我就在找dds文件的数据格式,最后偶然发现wand库有img.compression = 'dxt1',试了一下,确实可以把4M的dds文件压缩回和骑砍自带的dds文件差不多的大小(我估计骑砍texture里面的dds文件用的是dxt1压缩),所有把这个结果分享给有相同需求的modder。
- filename = 'example.dds'
- with image.Image(filename=filename) as img:
- img.compression = 'dxt1'#1/3/5
- img.save(filename=filename)
复制代码
|
|
评分
-
查看全部评分
鲜花鸡蛋ggfgfgf 在2024-5-2 14:42 送朵鲜花 并说:我非常同意你的观点,送朵鲜花鼓励一下 杰喵喵 在2024-4-30 10:17 送朵鲜花 并说:我非常同意你的观点,送朵鲜花鼓励一下
|