⭐✔️
本帖最后由 贰逼 于 2019-5-23 00:35 编辑
前言
    进入数据包时代后,Sethbling继续领衔玩转逆天操作,从“BlingEdit”(一个基于原版制作的类似于worldedit的数据包)到Minecraft内制作“ATARI2600模拟器”(甚至用它破了speedrun世界纪录),这个Shaded_Camera数据包是几个月前的Camera数据包的续作,现在增加了材质渲染功能。没错,材质渲染
    至于这个数据包的后续,我不做继续跟踪了。目前Seth已经让这个数据包能录制gif动画了,更强的操作请关注Sethbling个人频道。


先看看效果:







    要继续吹这个数据包,可谓是大体上实现了计算3D物体的2D成像的过程,只不过是简化到了立方体。从效率的角度,这个数据包处理完成一张128*128的图像仅需几秒;从性能的角度,这几秒执行的上千万条命令,没有卡顿。

    Seth的数据包风格非常明显,那就是用递归替换掉大量枚举,将函数内部命令按照逻辑关系分为多文件。这样做能让代码效率提高不少,同时也间接地混淆了文件。但当文件名用命令行数表示时,看其中的原理就很麻烦了。花了几个小时,顺藤摸瓜,我最终将89个函数的关系理清了。

    不得不说,该数据包涉及的内容还是挺多的,因此在讲整个流程之前需要有一些预备知识,否则一些过程可能无法理解。至于各个过程的具体实现,我认为不是重点内容了,重要的是知道有什么方法,从中有什么启发。

    上方的目录,我将其分为三个板块,一个是所用到的预备知识,主要提供链接;另一个是具体流程,主要基于该数据包内的函数;最后我会总结这个数据包中的一些值得借鉴的方法


2021.12 数据,可能有更多内容
前言
    进入数据包时代后,Sethbling继续领衔玩转逆天操作,从“BlingEdit”(一个基于原版制作的类似于worldedit的数据包)到Minecraft内制作“ATARI2600模拟器”(甚至用它破了speedrun世界纪录),这个Shaded_Camera数据包是几个月前的Camera数据包的续作,现在增加了材质渲染功能。没错,材质渲染
    至于这个数据包的后续,我不做继续跟踪了。目前Seth已经让这个数据包能录制gif动画了,更强的操作请关注Sethbling个人频道。


先看看效果:







    要继续吹这个数据包,可谓是大体上实现了计算3D物体的2D成像的过程,只不过是简化到了立方体。从效率的角度,这个数据包处理完成一张128*128的图像仅需几秒;从性能的角度,这几秒执行的上千万条命令,没有卡顿。


    Seth的数据包风格非常明显,那就是用递归替换掉大量枚举,将函数内部命令按照逻辑关系分为多文件。这样做能让代码效率提高不少,同时也间接地混淆了文件。但当文件名用命令行数表示时,看其中的原理就很麻烦了。花了几个小时,顺藤摸瓜,我最终将89个函数的关系理清了。


    不得不说,该数据包涉及的内容还是挺多的,因此在讲整个流程之前需要有一些预备知识,否则一些过程可能无法理解。至于各个过程的具体实现,我认为不是重点内容了,重要的是知道有什么方法,从中有什么启发。


    上方的目录,我将其分为三个板块,一个是所用到的预备知识,主要提供链接;另一个是具体流程,主要基于该数据包内的函数;最后我会总结这个数据包中的一些值得借鉴的方法


以下内容为预备知识,你最好能有所了解;即便不了解,你也可以直接去"技术流程"结合本数据包进行学习,但可能会遇到一些阻碍

下面提供的内容大多为专业信息,包含算法。如果你不具备相关的数学知识,只需要知道概念即可,我们在"技术流程"中展开讨论。

坐标系相关
  • 世界坐标系 & 摄像机坐标系
https://en.wikipedia.org/wiki/Gr ... d_Coordinate_System
https://baike.baidu.com/item/世界坐标系
https://baike.baidu.com/item/摄像机坐标系
结合Minecraft,玩家只需要有以下基本认识
https://mc-command.oschina.io/co ... concepts/space.html

成像相关
  • 光线投射 & 光线追踪
https://en.wikipedia.org/wiki/Ray_casting
https://zh.wikipedia.org/wiki/光线追踪
https://en.wikipedia.org/wiki/Ray_tracing_(graphics)
https://baike.baidu.com/item/光线追踪
其中只需要了解RayCasting(光线投射)
在本数据包中,Seth将函数命名为RayTracing(光线追踪),但Seth的函数本质上是光线投射,因为光线追踪还要结合了反射、折射、阴影,而光线投射只是光线追踪的第一个过程。

贴图
  • UV贴图
https://en.wikipedia.org/wiki/UV_mapping
对于Minecraft而言:
http://www.mcbbs.net/thread-491597-1-1.html
阅读其中的"方块模型"-"uv"

Function
最后就是关于数据包Function的内容了,起码要知道函数的调用函数的递归


技术流程
整个数据包的处理过程如下

计分板 & 实体标记
  • 计分板
    • Constant
      用于储存假名常量。如c16=16,minus=-1,c100=100。
    • col & row
      在平面扫描中用于表示行与列的索引值。(0~127)
    • step & final_step
      记录光线投射的步数。
    • handled
      布尔量,表示递归是否可以结束。
    • .x., .y., .z.
      例如"bx1","by1","bz1",这些计分板用于三维计算。(因为Minecraft不支持数学表达式运算,所以过程变量挺多的)
    • face_x, face_y, face_z, bottom_face
      布尔量,表示投影点落在方块的哪个面上。其中bottom_face表示的是投影点是否落在底面。
    • ray2_scratch0~2
      浮点量,表示UV所选择的像素的世界坐标。
  • Marker
    • Scan
      平面扫描的标记。
    • Position
      计算重叠面的标记。
    • Texture
      UV的标记。

初始化

  • Reset 重置

    代码:

    1. #初始化计分板
    2. scoreboard objectives add col dummy
    3. scoreboard objectives add z dummy
    4. scoreboard objectives add x dummy
    5. scoreboard objectives add y dummy
    6. ...
    7. #常数设置
    8. scoreboard players set c16 Constant 16
    9. scoreboard players set minus Constant -1
    10. scoreboard players set c100 Constant 100
    11. #重置杂项
    12. gamerule maxCommandChainLength 1000000000
    13. gamerule randomTickSpeed 0
    14. bossbar add progress "Progress"
    15. bossbar set progress max 128
    16. #重置marker
    17. execute unless entity @e[type=minecraft:area_effect_cloud,tag=Scan,limit=1] run summon minecraft:area_effect_cloud ~ ~ ~ {Tags:["Scan"], Duration:-1,Age:-2147483648,WaitTime:-2147483648}
    18. execute unless entity @e[type=minecraft:area_effect_cloud,tag=Position,limit=1] run summon minecraft:area_effect_cloud ~ ~ ~ {Tags:["Position"], Duration:-1,Age:-2147483648,WaitTime:-2147483648}
    19. execute unless entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] run summon minecraft:area_effect_cloud ~ ~ ~ {Tags:["Texture"], Duration:-1,Age:-2147483648,WaitTime:-2147483648}
    20. #输出tellraw
    21. tellraw @a ["",{"text":"[Take Picture]","clickEvent":{"action":"run_command","value":"/function shaded_camera:take_picture"},"color":"yellow"},{"text":" ","color":"yellow"},{"text":"[Setup]","clickEvent":{"action":"run_command","value":"/function shaded_camera:setup"},"color":"yellow"}]
    这个函数主要功能包括初始化计分板、设置常数、重置杂项、重置Marker以及提示信息。

  • Setup 安装

    代码:

    1. function shaded_camera:fill_ceiling
    2. bossbar set progress players @s
    3. bossbar set progress value 0
    4. fill -64 0 -64 -57 254 -57 air
    5. fill -64 0 -56 -57 254 -49 air
    6. ...
    7. bossbar set progress value 8
    8. fill -56 0 -64 -49 254 -57 air
    9. fill -56 0 -56 -49 254 -49 air
    10. fill -56 0 -48 -49 254 -41 air
    11. ...
    12. ...
    13. bossbar set progress value 120
    14. fill 56 0 -64 63 254 -57 air
    15. fill 56 0 -56 63 254 -49 air
    16. fill 56 0 -48 63 254 -41 air
    17. ...
    18. bossbar set progress players
    这个函数用于清空区块。

  • Develop 洗相片

    代码:

    1. tellraw @a ["",{"text":"Filling ceiling..."}]
    2. fill -80 255 -80 80 255 80 redstone_lamp
    3. tellraw @a ["",{"text":"Ceiling done."}]
    这个函数的功能是生成一个由红石灯组成的遮罩层。具体目的看【技术流程-杂项】。


平面扫描
平面扫描是一个循环过程,扫描的对象是摄像机平面。
其可视化过程如下

用流程表示则如下

循环遍历平面坐标的x,y很简单,但如何做到将遍历从“摄像机坐标系”转移至“世界坐标系”呢?  可以通过"Scan"这个Marker,利用局部坐标(^)进行传送位移,达到“摄像机->世界”的目的。
具体的可视化过程如下:


("Scan"所遍历的平面与摄像机平面平行)
源码解析关于execute命令下的at,首先你需要知道
    1. 相对性

代码:

  1. execute at @m positioned ~ / ^
相对坐标或局部坐标都是相对于@m的,表示的是在基于@m建立的坐标系下的位置。
    2. 传递性

代码:

  1. execute at @m run tp @n ~ ~ ~ ~ ~
使用tp命令,能让实体@n继承实体@m的位置和视角。
利用这两个特性,我们能避免进行复杂的运算而直接让Scan实体在摄像机所捕捉的局部区域平行于摄像机平面移动。

接下来我们复习一下什么叫相对坐标,什么叫局部坐标

两者都是表示基于实体建立的坐标系下的位置,且~ / ^后面的数值都表示距离。但两者区别在于相对坐标沿用了世界坐标轴,而局部坐标使用的是实体坐标轴;世界坐标轴的三轴与实体旋转无关,而局部坐标的三轴与实体旋转有关。


  • 继承玩家摄像机

    代码:

    1. (from take_picture.mcfunction)
    2. execute at @s run tp @e[type=minecraft:area_effect_cloud,tag=Scan,limit=1] ~ ~1.8 ~ ~ ~
    (~, ~1.8, ~)即玩家头部所在位置。此处利用了execute下at的传递性。

  • 关于扫描平面
    扫描平面并非摄像机平面,而是将摄像机平面向前投影,并得到一个更大的区域。
    在本数据包中,扫描平面位于摄像机正前方十格。作为Marker的"Scan"在扫描平面中的扫描范围为20*20(格)。


  • 初始化
    让"Scan"来到扫描起始位点

    代码:

    1. (from take_picture.mcfunction)
    2. execute as @e[type=minecraft:area_effect_cloud,tag=Scan,limit=1] at @s run tp @s ^10 ^10 ^10
    3-7.png

  • 扫描位点移动
    将20*20(格)的区域平分得到128*128个扫描位点,相邻位点间距为0.15625(格)。

    代码:

    1. (from execute003/005/007.mcfunction)
    2. scoreboard players add Global row 1
    3. execute as @e[type=minecraft:area_effect_cloud,tag=Scan,limit=1] at @s run tp @s ^ ^-0.15625 ^
    向"Scan"的局部坐标y轴负方向移动"Scan",表示行数加一

    代码:

    1. (from while001.mcfunction)
    2. scoreboard players set Global row 0
    3. scoreboard players add Global col 1
    4. execute as @e[type=minecraft:area_effect_cloud,tag=Scan,limit=1] at @s run tp @s ^-0.15625 ^20 ^
    向"Scan"的局部坐标x轴负方向移动"Scan",表示列数加一。同时重置其行数,让它回到初始的局部y坐标处。

  • 递归逻辑
    这个递归逻辑支撑了平面扫描的遍历过程。
    • 起点
      take_picture.mcfunction

      初始化。
    • 列递归
      take_picture.mcfunction -> execute002.mcfunction -> while001.mcfunction -> take_picture_col.mcfunction -> while001.mcfunction

      在列的方向上递归,主要依靠while001.mcfunction进行递归,递归次数为128次。
    • 行递归
      take_picture_col.mcfunction -> execute004/006/008 -> execute003/005/007 -> take_picture_col.mcfunction

      在行的方向上递归,主要依靠take_picture_col.mcfunction进行递归,递归次数为128次。


光线追踪
光线投射"光线追踪"是从每一个像素射出一条射线,然后找到最接近的物体挡住射线的路径(来自wiki)。在本数据包中,光线投射则是在每一个扫描位点射出一条射线,最终找到投射位置。
再次强调,此处的"光线追踪"仅实现了"光线投射"。为避免歧义,下文都使用"光线投射"。
其可视化过程如下(射线起点为玩家头部中心(~ ~1.8 ~))

用流程表示则如下

那么光线投射的目的是什么呢?就是为了找到摄像机扫描位点所对应的在世界坐标系下的绝对位置。
光线投射和平面扫描有何关系?平面扫描借助扫描位点,定义了光线投射的方向。
光线投射结合平面扫描,效果如下

源码解析
  • 初始化

    代码:

    1. execute at @s positioned ~ ~1.8 ~ facing entity @e[type=minecraft:area_effect_cloud,tag=Scan,limit=1] feet run function shaded_camera:ray_trace_step
    利用facing来获取投射的方向(由玩家头部指向扫描位点),并传递给函数ray_trace_step.mcfunction。
  • 投射
    全过程可视化

    • 递归逻辑
      ray_trace_step.mcfunction -> execute009.mcfunction -> ray_trace_step.mcfunction -> ...

      两个函数嵌套递归,其中ray_trace_step为检测函数,execute009是位移函数
      为什么不调用自身呢?在递归结束已经进行了结束操作后,我们不希望返回时上一级函数再次进行递归结束的操作。所以必须要有一个返回值(在本数据包中用计分板"handled"表示)来代表是否已执行结束操作。具体目的请看下方的[检测碰撞]。
    • 射线位移

      代码:

      1. (from execute009.mcfunction)
      2. #计分板"step"用于表示步数。分数每加一,代表前进0.2格。
      3. scoreboard players add Global step 1

      4. #位移
      5. #  向前移动0.2格。
      6. execute positioned ^ ^ ^0.2 run function shaded_camera:ray_trace_step

      7. #递归返回。设置"handled"为1,表示已处理。
      8. scoreboard players set Global handled 1
      "step"这个计分板储存了光线投射的步数(图中step=350表示射线移动了350步碰撞到了方块)。

      (^,^,^0.2)就是射线向前运动0.2格。结合递归逻辑以及execute的传递性,位移函数能将其执行坐标不断向前位移,且没有借助任何实体

    • 碰撞检测 + 步数限制

      代码:

      1. (from ray_trace_step.mcfunction)
      2. scoreboard players set Global handled 0

      3. #检测
      4. #  如果未投射到方块,且步数小于799(最大步数799,长度即800*0.2=160),调用位移函数
      5. #    (标签为#shaded_camera:none的方块表示能被穿透)
      6. execute if block ~ ~ ~ #shaded_camera:none if score Global step matches ..799 run function shaded_camera:execute009_ln136

      7. #递归结束
      8. #  若检测失败,即投射到方块或超过最大步数,且尚未处理(handled = 0表示未处理),则回退0.2格,进入下一步(调用execute010.mcfunction进而调用ray_trace_fine_step.mcfunction,以及后续等函数)
      9. execute if score Global handled matches ..0 positioned ^ ^ ^-0.2 run function shaded_camera:execute010_ln143

      • 碰撞检测的过程是这样的:首先检测函数检测当前执行位置的方块是否可穿透,然后再检查步数是否在范围内,满足两者则调用位移函数;位移函数将执行位置前移并再次调用检测函数
      • 若检测失败,则进行递归结束的操作。递归结束操作完成后,递归返回:先返回到上一级位移函数,返回时将handled设为1,表示已处理;再返回到上一级检测函数,返回位置在递归结束操作处,但由于已处理,所以直接跳过了递归结束操作;依此循环,直到返回到最高级。
  • 投射修正

    • 递归逻辑
      ray_trace_fine_step.mcfunction -> execute011.mcfunction -> ray_trace_fine_step.mcfunction -> ...

      是一个与投射相同的过程,区别在于最大步数只有10,一步仅0.02格(长度即10*0.02=2)。
    • 目的
      修正投射结果,更精确(坐标精确到小数点后两位)地找到碰撞位置。
    • 下一步

      代码:

      1. execute if score Global handled matches ..0 run function shaded_camera:execute082_ln174
      调用获取方块材质及面信息的函数。


像素获取
  在光线投射完成后,我们得到了投射坐标与投射方向。接下来,我们要想办法将投射的结果呈现在地图上
  我们都知道地图(仅讨论scale=0,也就是大小为128*128的地图)的原理是将方块以像素的形式呈现。因此要将结果呈现在地图上,只需获取投射坐标处的方块。你可能立刻会想到直接使用clone,但事实是许多方块共用一个颜色;而且很多方块无法被正确地显示出来。于是Seth想到了将方块按照特征颜色分类,通过以下过程获取像素。

源码解析
  • 分类
    利用blocktag把下列方块分为如下多类,每类的方块共享一个特征颜色:

  • 枚举

    代码:

    1. (from execute082)
    2. execute if block ~ ~ ~ #shaded_camera:grass run function shaded_camera:execute012_ln755
    3. execute if score Global handled matches ..0 if block ~ ~ ~ #shaded_camera:sand run function shaded_camera:execute013_ln759
    4. execute if score Global handled matches ..0 if block ~ ~ ~ #shaded_camera:cloth run function shaded_camera:execute014_ln763
    5. execute if score Global handled matches ..0 if block ~ ~ ~ #shaded_camera:tnt run function shaded_camera:execute015_ln767
    6. execute if score Global handled matches ..0 if block ~ ~ ~ #shaded_camera:ice run function shaded_camera:execute016_ln771
    7. execute if score Global handled matches ..0 if block ~ ~ ~ #shaded_camera:iron run function shaded_camera:execute017_ln775
    8. ...
    对当前坐标(光线投射到的位置)的方块进行判断,然后进入下一步为像素映射做准备。
  • 像素映射的准备
    函数中execute012~061都是像素映射的准备工作,都含以下的两行命令。

    代码:

    1. (from execute012~061)
    2. setblock 0 1 0 grass_block(特征颜色方块)
    3. scoreboard players set Global handled 1
    接下来要做什么你应该懂了吧。(详情见[像素映射]


UV材质
仅将方块作为像素点,对于近景等细节的还原,显然是不够的。因此我们需要精确获取方块材质中的像素点,才能满足对细节的需求。
简单地来讲,仅对MC中的方块而言,"UV"就是方块材质中一个个像素点在平面的相对坐标。

借助我们在地图中用方块制作的伪UV材质(材质的像素画),利用光线投射的结果,结合一系列运算,我们就能利用UV选择像素点来表现近景的细节。
那么如何通过光线投射碰撞后的坐标和投射的方向来计算得UV所选择的像素点呢,下面是整个过程。

看着挺麻烦,不过没关系,以下可视化图像协助你理解这个过程。
源码解析
  • 计算光线投射后与面向交的线段
    光线投射的过程最终让函数继承了两个有关位置的信息:结束时的坐标投射方向
    仅依靠这两个信息,我们怎么获取UV坐标、面信息呢?一切从构造的一条线段开始:

    代码:

    1. (from execute082_ln174)
    2. execute as @e[type=minecraft:area_effect_cloud,tag=Position,limit=1] run function shaded_camera:execute062_ln178
    对"Position"执行函数execute062.mcfunction。

    代码:

    1. (from execute062_ln178)
    2. #将Position传送到当前位置
    3. tp @s ~ ~ ~ ~ ~
    4. execute store result score Global x2 run data get entity @s Pos[0] 1000
    5. execute store result score Global y2 run data get entity @s Pos[1] 1000
    6. execute store result score Global z2 run data get entity @s Pos[2] 1000
    7. execute as @s at @s run tp @s ^ ^ ^-0.02
    8. execute store result score Global x1 run data get entity @s Pos[0] 1000
    9. execute store result score Global y1 run data get entity @s Pos[1] 1000
    10. execute store result score Global z1 run data get entity @s Pos[2] 1000
    "Position"只是一个用于获取坐标的Marker。光线投射修正的结束时的坐标也就是本函数的执行坐标在方块内,而向后传送0.02格的坐标必在方块外(因为光线投射修正每次的位移是0.02格)。
    最终得到两坐标B(x1,y1,z1)与A(x2,y2,z2)(保留三位小数),其中B在方块内,A在方块外,|AB|=0.02,用AB来表示这条与面相交的线段

  • 世界坐标系转方块坐标系

    代码:

    1. (from execute082_ln174)
    2.   #取A(x1,y1,z1)的小数部分为A'(bx1,by1,bz1)
    3. scoreboard players operation Global bx1 = Global x1
    4. scoreboard players operation Global by1 = Global y1
    5. scoreboard players operation Global bz1 = Global z1
    6. scoreboard players set Global ray2_scratch0 1000
    7. scoreboard players operation Global bx1 %= Global ray2_scratch0
    8. scoreboard players operation Global by1 %= Global ray2_scratch0
    9. scoreboard players operation Global bz1 %= Global ray2_scratch0
    10.   #计算方向向量D=(dx,dy,dz)
    11. scoreboard players operation Global dx = Global x2
    12. scoreboard players operation Global dy = Global y2
    13. scoreboard players operation Global dz = Global z2
    14. scoreboard players operation Global dx -= Global x1
    15. scoreboard players operation Global dy -= Global y1
    16. scoreboard players operation Global dz -= Global z1
    17.   #B'(bx2,by2,bz2) = A'(bx1,by1,bz1) + D(dx,dy,dz)
    18. scoreboard players operation Global bx2 = Global bx1
    19. scoreboard players operation Global by2 = Global by1
    20. scoreboard players operation Global bz2 = Global bz1
    21. scoreboard players operation Global bx2 += Global dx
    22. scoreboard players operation Global by2 += Global dy
    23. scoreboard players operation Global bz2 += Global dz
    将世界坐标A(x1,y1,z1)除以1000取余数,就得到了该方块坐标系下A(x1,y1,z1)的相对坐标A'(bx1,by1,bz1)。
    利用世界坐标A(x1,y1,z1)和B(x2,y2,z2)计算得方向向量D=(dx,dy,dz),那么B'=A'+D。
    为什么不用同得到A'的方法得到B'呢?因为A所在的位置上的方块是投射主体,B在主体外,如果用相同的方法得B',那么A'和B'两者所在的方块坐标系不同,无法进行下一步操作。

    将世界坐标系下的两点坐标转化为方块坐标系后,我们就能直接根据两点坐标的数值关系判断线段与哪个面相交,进而计算交点的UV;同时化大为小,防止因计分板的位数限制使相关运算溢出。
  • 计算线段在面外的长度占比 与 面信息的记录

    代码:

    1. (from execute082_ln174)
    2. #bx2大于1000,则穿过了x=1000的面
    3. execute if score Global bx2 matches 1001.. run function shaded_camera:execute063_ln188
    4. #bx2小于0,则穿过了x=0的面
    5. execute if score Global bx2 matches ..-1 run function shaded_camera:execute064_ln191
    6. #bx2在0到1000,则没穿过x方向上的面
    7. execute if score Global bx2 matches 0.. if score Global bx2 matches ..999 run scoreboard players set Global px 0
    以x方向为例,分为三种情况:穿过x=0的面;穿过x=1000的面;没穿过x方向上的面。
    在y、z方向上处理与其相同。
    • > 1000

      代码:

      1. (from execute063/065/067)
      2. scoreboard players set Global px 1000
      3. scoreboard players operation Global px -= Global bx1
      4. scoreboard players operation Global px *= c100 Constant
      5. scoreboard players operation Global px /= Global dx
      6. (from execute065)
      7. scoreboard players set Global top_face 0
      8. scoreboard players set Global bottom_face 1
      以x方向为例,计算的表达式为:

      代码:

      1. px = (1000 - bx1) * 100 / dx

    • < 0

      代码:

      1. (from execute064/066/068)
      2. scoreboard players operation Global px = Global bx1
      3. scoreboard players operation Global px *= c100 Constant
      4. scoreboard players operation Global ray2_scratch0 = Global dx
      5. scoreboard players operation Global ray2_scratch0 *= minus Constant
      6. scoreboard players operation Global px /= Global ray2_scratch0
      7. (from execute065)
      8. scoreboard players set Global top_face 1
      9. scoreboard players set Global bottom_face 0
      以x方向为例,计算的表达式为:

      代码:

      1. px = bx1 *100 / -dx

    这个过程会得到精确到小数点后两位的px或py或pz,如果px或py或pz≠0,则说明线段穿过了在该轴上的面
    本质上是面外的点到面的投影距离占线段在该轴方向的总距离的百分比,即线段在面外的长度占线段长度的比。假设光线投射的碰撞点为O,那么p=|A'O|/|D|.

UV处理
获取UV像素,实际上也是获取方块。通过将计分板转实体坐标坐标的方式,我们可以将UV映射到我们提前预设的方块材质(像素画)上。
  • 获取UV 及 所投射的面

    代码:

    1. (from execute082_ln174)
    2. execute if score Global px matches 1.. run function shaded_camera:execute072_ln232
    3. execute if score Global py matches 1.. run function shaded_camera:execute073_ln240
    4. execute if score Global pz matches 1.. run function shaded_camera:execute074_ln248
    以pz为例

    代码:

    1. (from execute074_ln248)
    2. scoreboard players set Global face_x 0
    3. scoreboard players set Global face_y 0
    4. scoreboard players set Global face_z 1
    5. scoreboard players operation Global u = Global by1
    6. scoreboard players operation Global v = Global bx1
    face_z=1表示光线投射碰撞到方块的面位于z轴方向
    由于0.02距离很小可以估算,所以UV直接就用by1和bx1表示了,也就是说直接用A点的坐标。


  • 代码:

    1. (from execute082_ln174)
    2. execute if score Global fine_step matches 11.. run function shaded_camera:execute075_ln256
    如果投射修正是因为达到最大步数才调用本函数,则说明投射方向上无方块

    代码:

    1. (from execute075_ln256)
    2. scoreboard players set Global face_x 0
    3. scoreboard players set Global face_y 1
    4. scoreboard players set Global face_z 0
    5. setblock 0 1 0 light_blue_wool
    默认天空所在的面在y轴方向(与像素映射有关),并且直接进入到映射准备环节,即在(0,1,0)处设置映射所需的像素点(此处用蓝色羊毛表示天空颜色)。
  • 六面一致
    • 第一部分:分类

      代码:

      1. (from execute082_ln174)
      2. execute if block ~ ~ ~ minecraft:oak_planks run function shaded_camera:execute076_ln151
      3. execute if block ~ ~ ~ minecraft:bricks run function shaded_camera:execute077_ln151
      4. execute if block ~ ~ ~ minecraft:spruce_leaves run function shaded_camera:execute078_ln151
      5. execute if block ~ ~ ~ minecraft:spruce_log run function shaded_camera:execute079_ln151
      该数据包中此类方块有橡树树叶、砖、云杉树叶、云杉原木。仅以橡树树叶为例。
    • 第二部分:方块坐标系下的uv坐标 转 世界坐标系下的三维坐标

      代码:

      1. (from execute076_ln151)
      2. #计算UV
      3. scoreboard players operation Global ray2_scratch0 = Global u
      4. scoreboard players operation Global ray2_scratch0 *= c16 Constant
      5. scoreboard players add Global ray2_scratch0 22000
      6. scoreboard players set Global ray2_scratch1 63000
      7. scoreboard players set Global ray2_scratch2 999
      8. scoreboard players operation Global ray2_scratch2 -= Global v
      9. scoreboard players operation Global ray2_scratch2 *= c16 Constant
      10. scoreboard players add Global ray2_scratch2 100000
      11. #  取像素点坐标为(22.000+u*16, 63.000, 100.999-v*16)
      12. #    即预设材质(像素画)中的一个方块
      13. #    涉及到坐标系的转化,这里就不作解析了

      14. #投射的准备
      15. execute store result entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] Pos[0] double 0.001 run scoreboard players get Global ray2_scratch0
      16. execute store result entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] Pos[1] double 0.001 run scoreboard players get Global ray2_scratch1
      17. execute store result entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] Pos[2] double 0.001 run scoreboard players get Global ray2_scratch2
      18. execute at @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] run clone ~ ~ ~ ~ ~ ~ 0 1 0
      先扩1000倍表示保留三位小数,设置一个起点坐标(22.000, 63.000, 100.999)(尽管z坐标是在最后加上100.000的,但并不影响结果)
      之前我们获取的uv,实际上是在方块坐标系下A点在AB所穿过面上的射影点坐标,范围是0.000~1.000;然而预设材质(像素画)的大小16*16个方块,因此我们要将uv扩16倍,以得到所选像素的真实坐标。
      最后相加,获得(22.000+u*16, 63.000, 100.999-v*16)为像素点的世界坐标。


    利用"Texture"这个Marker,我们将计分板计算的坐标赋给实体的世界坐标,让它来到像素点的位置,然后clone方块到(0,1,0)为映射做准备。
  • 侧面一致

    代码:

    1. (from execute082_ln174)
    2. #按投射的面分类处理
    3. execute if score Global face_x matches 1.. if block ~ ~ ~ minecraft:grass_block run function shaded_camera:execute069_ln222
    4. execute if score Global face_z matches 1.. if block ~ ~ ~ minecraft:grass_block run function shaded_camera:execute070_ln225
    (在数据包中仅对草方块有此操作)

    代码:

    1. (from execute069_ln222)
    2. scoreboard players operation Global ray2_scratch0 = Global u
    3. scoreboard players operation Global ray2_scratch0 *= c16 Constant
    4. scoreboard players add Global ray2_scratch0 22000
    5. scoreboard players set Global ray2_scratch1 70000
    6. scoreboard players set Global ray2_scratch2 999
    7. scoreboard players operation Global ray2_scratch2 -= Global v
    8. scoreboard players operation Global ray2_scratch2 *= c16 Constant
    9. scoreboard players add Global ray2_scratch2 100000
    10. execute store result entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] Pos[0] double 0.001 run scoreboard players get Global ray2_scratch0
    11. execute store result entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] Pos[1] double 0.001 run scoreboard players get Global ray2_scratch1
    12. execute store result entity @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] Pos[2] double 0.001 run scoreboard players get Global ray2_scratch2
    13. execute at @e[type=minecraft:area_effect_cloud,tag=Texture,limit=1] run clone ~ ~ ~ ~ ~ ~ 0 1 0
    我们发现第二部分和[六面一致]中的是一样的,区别只在于第一部分将处理过程按照所投射的面(face_?)进行了分类
  • 顶底相异

    代码:

    1. (from execute082_ln174)
    2. #将顶与底分类处理
    3. execute if score Global face_y matches 1.. if score Global bottom_face matches 1.. if block ~ ~ ~ minecraft:grass_block run function shaded_camera:execute071_ln228
    4. execute if score Global face_y matches 1.. if score Global top_face matches 1.. if block ~ ~ ~ minecraft:grass_block run function shaded_camera:execute080_ln151
    第二部分和[六面一致]的相同,就不赘述了。上面只是将进一步将bottom_face与top_face进行了分类
  • 一些局限
    • Seth在设计时显然并非为了处理全部的六面,只是将Y轴面上的顶面和底面分开了,X轴面与Z轴面并无左右或前后之分,因此无法做出六面各一材质的效果。如果想让X轴面与Z轴面有前后左右之分,则需按照与分开顶面和底面相同的方法处理。
    • 再就是UV精确度,既然我们已经算出了线段A'B'的面外长度占比p,那么A'+D*p就是精确的光线投射碰撞点(之前设为O)的坐标,然而也许出于性能优化或者节省步骤,Seth做了近似处理,因此"p(xyz)"相当于表示投射的面,没有物尽其用。


像素映射
无论是直接选择的像素点,还是uv选择的像素点,都会被放置或复制到世界坐标(0,1,0)处。接下来我们要做的,就是直接复制(0,1,0)处的方块到区块指定位置
具体流程如下(回到平面扫描):

最终生成的结果如下
为什么不直接在一个平面生成呢?原理上是采用了地图像素画的阶梯式(来源于Wiki,不翻译了):

即北向(x+)方块的高低能实现阴影效果,这也是地图为什么能显示出地形的原理。
像素映射也使用了阶梯式,映射出起伏的方块,绘制出体现远近关系的画面

源码解析
  • 映射
    结束了上述操作后,经历返回,又回到了平面扫描中的函数"take_picture_col.mcfunction":

    代码:

    1. (from take_picture_col.mcfunction)
    2. execute if score Global face_x matches 1.. run function shaded_camera:execute004_ln117
    3. execute if score Global face_y matches 1.. if score Global handled matches ..0 run function shaded_camera:execute006_ln122
    4. execute if score Global face_z matches 1.. if score Global handled matches ..0 run function shaded_camera:execute008_ln127
    枚举了投射的面,分别进行操作:

    代码:

    1. (from execute004_ln117.mcfunction)
    2. execute positioned ~ ~-1 ~ run function shaded_camera:execute003_ln94
    3. scoreboard players set Global handled 1

    代码:

    1. (from execute006_ln117.mcfunction)
    2. execute positioned ~ ~1 ~ run function shaded_camera:execute005_ln94
    3. scoreboard players set Global handled 1

    代码:

    1. (from execute008_ln117.mcfunction)
    2. execute positioned ~ ~0 ~ run function shaded_camera:execute007_ln94
    3. scoreboard players set Global handled 1
    上面这样做的目的就是让面向X的像素点低于面向Z的像素点,就是让X方向比Z方向暗



    其实003/005/007内容都是相同的,其中涉及到平面扫描的部分就不分析了,仅分析以下部分

    代码:

    1. (from execute003/005/007_ln117.mcfunction)
    2. # Copy the block into the map area
    3. clone 0 1 0 0 1 0 ~ ~ ~
    4. fill ~ ~1 ~ ~ 254 ~ air
    这就是映射的主要命令了,将(0,1,0)处的方块到函数执行位置
  • 映射坐标的位移
    为什么函数执行位置就是地图区块上的该像素的位置?

    这里又要回到平面扫描的过程了。
    我们都知道平面扫描扫描的对象是玩家摄像机平面,但摄像机平面只是扫描的目标位置。为了映射像素点,我们则是让函数继承了地图坐标


    在平面扫描中我有意没有讲递归函数的执行坐标,原因就是其执行位置与映射的过程有关,所以我将其挪到此处细讲。
    其实每一次递归,都涉及到了函数执行坐标的位移
    • 列递归
      take_picture.mcfunction -> execute002.mcfunction -> while001.mcfunction -> take_picture_col.mcfunction -> while001.mcfunction

      代码:

      1. (from take_picture.mcfunction)
      2. execute positioned -65 128 -64 run function shaded_camera:execute002_ln77
      初始化,设置递归函数位移坐标起点为(-65, 128, -64).

      代码:

      1. (from execute002.mcfunction)
      2. execute if score Global col matches ..127 positioned ~1 128 ~ run function shaded_camera:while001_ln79
      初始化列的右移(x+)

      代码:

      1. (from execute002.mcfunction)
      2. execute if score Global col matches ..127 positioned ~1 128 ~ run function shaded_camera:while001_ln79
      递归中列的右移(x+)
    • 行递归
      take_picture_col.mcfunction -> execute004/006/008 -> execute003/005/007 -> take_picture_col.mcfunction

      代码:

      1. (from execute004_ln117.mcfunction)
      2. execute positioned ~ ~-1 ~ run function shaded_camera:execute003_ln94

      代码:

      1. (from execute004_ln117.mcfunction)
      2. execute positioned ~ ~1 ~ run function shaded_camera:execute005_ln94

      代码:

      1. (from execute004_ln117.mcfunction)
      2. execute positioned ~ ~0 ~ run function shaded_camera:execute007_ln94
      这一部分并没有位移。

      代码:

      1. (from execute003/005/007.mcfunction)
      2. execute if score Global row matches ..127 positioned ~ ~ ~1 run function shaded_camera:take_picture_col
      递归中行的上移(z+)


杂项
  • 遮罩层的用途
    目的是预光照更新。而红石灯作为理想选择的原因是它既是非透明方块,同时又不会被地图显示。
    当应用了遮罩层后:
    • 加快处理速度
      由于光照更新被简化了,区块更新变快了。
    • 避免暗区
      不会出现Minecraft光照更新错误,因此获取的照片不会出现暗区。
  • 处理速度快的原因
    整个数据包的处理不涉及基于tick的循环,而是调用或递归,利用多个函数构造了多层递归网络。因此整个数据包能在1 tick内执行上千万条命令
        那么为什么不卡呢?
      递归的1 tick,实际上是后台的1 tick,也就是说前台依旧能运行,只不过是以跳tick的状态运行(因为后台的运行是单线程的,你不能在此时运行其他命令);而且光照预更新后,大大缩短了区块处理的时间,对方块的操作(映射)变得相当快,畅通无阻。


这是一个精彩的数据包,带给1.13数据包开发尤其是新手以下启示



  • 递归处理
    (总结自“平面扫描”)从1.12前的思维中跳出,对1.13后的命令技术发展是明智之举。
    过去由于一些局限我们经常秉承穷举思想,但到了1.13实际上有很多不同的方式简化;
    同时我们习惯了基于tick的循环思想,实际上递归在1.13后效率更高。
    当然,至于到底用不用递归,还要有一定取舍。如果要在保持即时性的同时进行大量操作,还是循环更佳。

  • 函数坐标的继承
    (总结自“光线追踪”)我们过去习惯了实体标记作为对象,然而递归下的函数结合execute后也能作为对象。
    因为execute的嵌套,函数相当于不仅继承了坐标还能继承方向。
    这个方法其实在投射操作中比较常见。

  • 单实体复用
    (总结自“平面扫描”“UV材质”)整个数据包仅用了三个实体,"Scan"用于平面扫描,"Postion"用于获得光线投射的世界坐标,"Texture"用于赋值坐标选择像素点。
    这三个实体都是预设的,也就是说处理过程没有实体的生成与销毁,只是通过反复变换实体坐标反复利用了实体。
    尤其对于新手,应抵制一种绝对的对象思想,那就是整个过程需要体现对象的生成与销毁。实际这种处理的消耗更大。

  • 变量储存
    (总结自“初始化”)这也是给新手安利的,用假名储存分数。如果对变量的访问是单线程的,假名的分数不受干扰,那么假名完全可以用于储存各种变量。

  • 返回值
    (总结自“光线追踪”)由于递归是单线程的,因此结合上面的变量储存,你完全可以用假名储存一个递归的返回值。
    如本数据包中的handled,实际上是一个公共变量,但在单线程中完全可以当作一个函数的私有变量而返回给父级。

  • 小数与算法
    (总结自“UV材质”)变量扩倍储存,可当做浮点数用于近似计算。如整个数据包唯一涉及到算法的UV材质处理,就是用扩千倍来实现保留三位小数。
    再就是算法了,大量的运算,结合超多假名和计分板,实际上都是可行的,只是需要将其转化到四则等基本运算上。

  • 善用WIKI和特性
    Seth查阅了Wiki后解决了光照更新问题。实际上各位在开发过程中也可以应用类似的方法,追求最优时,除了从命令角度去优化,还可以善用游戏特性另寻新的解决方案。



下载 & 备注
原始Demo存档
链接:https://pan.baidu.com/s/1ZPZS2X-76Wib77qK6C164w
密码:vxah


Datapack下载
  • 原始datapack
  • 解析版datapack(含注释,不能运行,仅供参考学习)

    注:此数据包可与本帖对照食用。


解析工程
链接:
https://pan.baidu.com/s/178r50ObG9NUys_3Q2tubyQ
密码:3dj7

MineCrocodile
大佬解读,先收藏了

brooke_zb
本帖最后由 brooke1999 于 2019-5-12 22:12 编辑

大致原理看了一遍有点头绪,只是没想到有人真会去这么做,MC真是万能

主要就是利用玩家视角指向将距离视线方向10blocks的10x10(blocks)正方形区域圈出来,再把这个10x10正方形进一步细分成128x128像素(也就是地图图像精度)大小,遍历每个像素点,往玩家视线方向(^0.2)移动,直到“碰到”方块或达到800步递归限制(160方块远),然后根据碰到的 方块类型&面朝向 来确定颜色&明暗达到3D视觉效果,最终以放置方块+地图来呈现

也不是完全完美,如果能根据递归步数进一步细分画面明暗的话emmmm

之前在油管有看到Seth大佬关于这个原版摄像机的视频,当时没看懂。。。所以说能有这么详细的分析也是辛苦贰逼了,毕竟反混淆+分析原理肯定花了不少时间

总之,tql,awsl

                             EDIT                              

别看原理貌似被我一讲挺简单的,但是从想法到构思再到实现、优化等一系列过程却是很不容易的,所以能学习到思考问题的方式才是最重要的。

⭐✔️
brooke1999 发表于 2019-5-11 23:16
大致原理看了一遍有点头绪,只是没想到有人真会去这么做,MC真是万能

主要就是利用玩家视角指向将 ...

先针对您提供的改进之处谈一下我的看法:
进一步细分是不可能的,因为地图像素画最多只有四个层次;因为方块特征颜色的缘故,根据地形生成的地图像素画并不如外部生成的像素画色域广,原版下能实现材质和明暗已经是极限了

再就是另外一点感受:
分析很简短其实,但并非想象那么容易;光看每个过程的目的是挺简单的,但实际上实现这些内容不是那么容易的;十分钟看完整篇帖子,且不说我码字做图一个月,实际上Seth开发了近半年更新了四个版本:第一个是界面显示也就是精简版、第二个是利用前一个数据包的原理实现了录制视频、第三个也就是一个月前更新的本数据包、第四个使用本数据原理实现录制gif。徒想没有价值,实现才是值得点赞的

zxcv21202

楼主做教程不容易,我这个新手献上一拜。{:10_512:}

ZZHei
MCBBS有你更精彩~

依然冰奈斯
原理简单,但实现起来会很难吧

c1284177900
溜的飞起,必须支持一下!!!

大鹅哈哈
感谢分享!

icebears
是图片挂了还是我就看不见qwq

Hajime_S
TQL,TQL
脑袋里瞬间想到了某六号的小车车
把主体转换为蠹虫什么的,然后探测玩家微量移动,映射到蠹虫上,再把影像传回玩家手里的地图
想想就刺激啊

小森源
厉害呀,真可以做摄像头

badday
感谢楼主分享~

wjy123888
好耶6666666

丿Luckly
牛的牛的牛的

0707ID
这东西真的能做出来?????

小天1007
MCBBS有你更精彩~

MC名字Miuk
MCBBS有你更精彩~

1287454619

厉害呀,真可以做摄像头可以弄手机不

晓瑶
原理简单,但实现起来会很难吧

寒烟枫叶
呃,那个录视频用的叫啥名啊

Rissica
牛蛙,这个挺不错的

1755504801
看着不错感谢分享

滴滴小强嘿
感谢分享 嘎嘎有用   

huanlan233
太离谱了,第一次见到这种原版摄像机,完全没想到mc可以搞这个(论算法的重要)
还是印证了那句话,有了mc就有了全部游戏(

第一页 上一页 下一页 最后一页