从unity3d编程旋转角度的角度来看,Minecraft 是怎么样设计的

{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"contributes":[],"title":"答@兔子先生文","author":"std4453","content":"前些天回答了个问题:先在此对 的疑惑做出解答。或有涉及人身攻击之语,一概不谈,也希望各位都能做到文明讨论。先抓重点。我仔细看了之前的评论,发现确实出言不妥。在此向 道歉,但是对您对我能力的怀疑、诽谤以及错误论点,也烦请您做出一定的表示。注:下文引用内容均真实不虚,为读者方便直接复制文本,内容版权归原作者所有。请被引用者为了维护知乎社区的和谐不要删除自己的文字,重要部分我也已截图保存证据。谢谢配合。前因后果是这样的:我起先看了这个答案:,因为个人也对MC(游戏Minecraft,标准译名《我的世界》)有一定的研究和了解,所以对回答问题产生了兴趣。另有同问题下的另一个回答:。此回答主要讲的是MC在设计和编码上的问题以及少量架构及地形生成算法方面的评价和介绍,再次不予置评。之后我在此回答的评论中看到了上述 的评论内容:(所有引用中以//开头的行均为我的评论)兔子先生:欸!原来那个Java的PerlinNoise就是答主大大写的啊~// Perlin noise是由ken perlin开发的n维随机数生成算法,稍有编程知识的人都能知道,// 一个已经被人提出的算法只能被“实现”,并且绝大多数算法都可以由很多种编程// 语言实现,perlin noise的Java实现光是Github上能搜索到的版本就有多达52种,// (见,截止日),而其中名叫PerlinNoise的项目// 就有13个(包括变体的perlin-noise,手动计数,欢迎验证)。不知道评论中说的// “Java的PerlinNoise”指的是哪一个(答主的也不叫PerlinNoise)。有理由认为,// 不知道一个算法可以有很多个实现并且仅用名称引用容易产生歧义的人,对整个// 编程行业不甚了解。答主大大能不能简单介绍一下矿物和房屋是怎么放进PerlinNoise的地图里的呢?答主 KAAAsS:1.不知你说的是哪个……不过应该不是我写的吧。我只是渣渣而已。2.最好看源码,不过简单说一下,就是通过2-pass,也就是在生成好的世界里进行加工,生成矿物,可以看看OreGeneration兔子先生:电脑现在没装eclipse……mc源码也丢了好久了,你提到的OreGeneration是Minecraft的代码还是别的paper之类的?我前两天搜索过这个关键字,一无所获KAAAsS:挨我智障了,这是项目里的233333看看net.minecraft.world.gen包兔子先生:多谢!我这就去看~兔子先生:……然而看了并没有多大帮助,全是混淆过的变量,甚至找不到相关的代码段。我:旁友你真的会写代码吗。。混淆也能看的啊。。// 当时我并不知道他的代码没有mapping,所以认为他手上的是和我一样的代码,// 这些代码方法名和类名都以及还原,费一点心思完全可以看得懂,并且我认为// 对代码进行逆工程是任何一个程序员必备的素质,这是一种对计算机逻辑的// 理解能力以及在人类语言和计算机语言之间互译能力的体现,因此我认为可能// 其实力并不甚高。兔子先生:我特么看了整整半个月的minecraft源码就是为了找到世界生成的逻辑,其中改了不知道多少混淆变量的名字,到你嘴里就特么变成了我不会写代码混淆也能看,你以为你是谁啊?// 请各位忽略不文明用语,我们就事论事,不谈人品。兔子先生:你看过minecraft的源码?兔子先生:现在网上有的反编译的Minecraft源码里面只有关键变量给了名字,其它的全都是什么dabaa_1647285这类玩意儿,你告诉我一大堆这玩意儿叫“也能看”?// 因此我现在知道他的源码非常晦涩,之后的评论也说明了原因,然而任何一个Minecraft// Mod的开发教程都会教你下载FML(Forge Modloader)所发布的源码,并运行一个// 命令行工具来反编译并构建开发环境。这个反编译得到的源码是较为易懂的,也是我// 手上的版本。如果这个dabaa_1647285的版本是FML给出的的话,也一定是一个中间// 产品,或者此人完全没有能力或意识去找到正确的开发方法。// 这也侧面证明了其不专业性。KAAAsS:今天才看到……你估计是没有更新mapping,混淆名没那么多其实// 看来这才是其源码难懂的真正原因,而dabaa_1647285应该是一个FML的中间产品。// 然而会把中间产品当做最终结果,这本身就是难以想象的,因为自动生成的完美开发环// 境中的源码就是最终结果的源码,只能说他可能连这一步也没能做到。兔子先生:吐兔血三升……我折腾了好久,硬啃下来了确实没更新mapping,我是拿的电脑上存的几年前的版本做的// 注意到其前面有说道“MC源码已经丢了好久了”,这里又说“电脑上存的几年前的版本”,// 烦请解释前后矛盾的原因。至此我得出临时结论:兔子先生在这方面是一个新人,可能对编程这整一个行业都不甚了解,对此也只想对其“混淆过的代码无法看懂”做出了评价,而事实证明这句评价是在不同的前提条件下做出的,也就是说我说的不应适合于他的情况,抱歉。然而其他的回答中还有 兔子先生 的回答:地图使用chunks,指针八叉树。 地形生成使用Perlin Noise。渲染直接用lwjgl调用OpenGL。因此我先入为主地认为这几句话是在说MC的卡顿原因,作出回复:兔子先生:偶然搜索到的而已,我就是一普通码农。//(中间省略无关评论,欢迎查证)我:chunk和perlin noise不同意。。区块就生成一次,simplex noise没好到哪里去,二维的chunk基本对性能影响不大。。lwjgl就是个opengl到java的简单包装,不“直接调用”怎么搞?要知道大部分大厂引擎主要是为了静态场景+高质量光照+少量动态角色设计的,MC这种沙盘游戏还没有什么质量特别高的引擎可以用。这个答案根本没有说到点子上,而是随便搜了一下强答的。果断反对。兔子先生:perlin noise是Notch自己在wiki上写的,chunk是大家反编译的时候天天见到的东西,lwjgl我说错了吗?// 应该是notch在博客中写的,可以看我的文章末尾的翻译+链接,欢迎查证。Minecraft// wiki是MC爱好者自行创办的,notch本人并没有参与编撰。然而我需要重申两点:1. 我认为专业问题就应该让专业人士回答,如果质疑我的“相对”专业性,我会在之后的内容中证明。然而只是“偶然搜索到”就来回答,除非只是谦虚,否则颇具所谓“强答”的含义,我认为这是不妥当的。2. 此问题为“从编程的角度来看,Minecraft是怎样设计的?”。我认为一款完整的游戏的设计最基本地也要从游戏的流程、各子系统组成、组织结构、基本运行原理来讨论,而兔子先生的答案只是提出了:地形生成系统中的一小部分、数据储存方式的一小部分(只有数据结构没有其操纵原理)、以及游戏渲染所使用的库。无论如何,我认为这样并没能很好的回答问题。确实我一开始先入为主的错误理解导致了接下来一系列的误会,因此我在此向道歉。————————分割线————————然而讨论并没有到此结束。在我在此问题下也发表了我的回答之后,也做出了他的评价,发在评论区中,而我将在此意义做出回答。(回答顺序按回复顺序,省略无关评论,欢迎查证,内容截止日,截取之前的评论请见评论区,不再引用)兔子先生:废话说那么多,装作自己很懂的科普了一大堆,我问你的最关键的几点你还是避而不谈。1.你反对我的时候说了【用的不是chunks也不是perlin noise】,现在一会儿改口说“simplex noise也很好”(强行解释的能力真厉害,佩服),一会儿又改口说“反对你是因为你说的不是设计”,做人不能食言而肥啊小兄弟。这一点我前面已经承认错误了,也不希望在这个问题上多做纠缠。兔子先生:再说了,就算按你现在改口的,chunks不是设计?perlin noise不是设计?拜托。喷人喷错了就该道歉,别再在那里越描越黑,你越强行给自己辩护,就越显得你无知。同样,见上文。至于之后的议论,鉴于我接下来会对他的一系列问题作出解答,兔子先生这句话是否正确,我是否无知,也将一清二楚。兔子先生:2.你嘲笑我说【就算混淆也能读懂】,一副自己水平很高的样子,现在又改口说【有magic number就读不懂】,你倒是嘴硬起来不要怂啊?而你所谓的magic number,却正好是地图生成算法的核心部分,这部分没懂,整体功能知道再多也没用。我答案中的中文注释:// 这里往下的这么一大段都是在插值,密度大于0的填上石头,低于海平面而且不是石头的// 填上海水,别的都是空气。// 话说这段真是乱七八糟,一堆magic number,估计fml的人也懒得看懂,// 既没加注释也没改变量名。。。请问哪句是在说“有magic number我就读不懂”?(相信他这句话的宾语是我用不着解释)我只是吐槽说看得很累,可能FML的人没有认真看就贴上去了,并不代表我看不懂。我在后面的代码分析中写了原理,如果你觉得没说清楚,可以提出疑问,但不能想当然。magic number,译作“魔法数字”,是编程中极为忌讳的手法之一。编程本来应该是有逻辑的,然而有些逻辑蕴含在数学公式中,并不容易理解。而魔法数字指的正是在代码中不加解释地加入并不显然的公式,特别是公式中的系数 / 参数 / 常数。举个例子:二次方程的通解是,相信大家都知道。然而为什么是而不是或者呢?当然在大家都了解这个解的情况下不需要注释,然而如果这个值的意义不甚明确,比如12345,就需要一个清楚的解释,如果是实验得出的结果,应该明确地标记出来,如果可以理论推导,也应该写出推导过程,比如:这样的来源就很清楚了,读者也不会为这个结果而感到困惑了。当然,在编译过程中,这样解释性的注释都会被直接丢弃,也不会包含在反编译出来的代码中。那么为什么FML的反编译代码中对各种数值的来源都没有明确的说明呢?或许是因为他们没有能力或精力去看懂,或许他们认为没有看懂这个的必要——毕竟他们的目的是让人能够基于Minecraft去开发mod,而不是做一个一模一样的Minecraft。这也无可厚非。所以magic number到底算不算得上“核心部分”呢?我是怀疑的。兔子先生说我看不懂,那我就详细地解释一遍setBlocksInChunk里面这段代码,其中也会提到所有没有做注释的数字,所谓magic number,看看它们是不是真正意义上所谓的“核心”。同时也回答下面的这段:3.即使你摆出了一大堆wiki上翻译出来的玩意儿,到现在【依然没找你所谓的另一个插值】——因为你理解错了,所以你根本找不到。【这就是我提出的建设性意见:你对地图生成的核心算法理解错误。】最关键的部分你不懂,别跟我说你有多厉害。我会找到插值给你看,证明我的理解是正确的,证明你是错误的,请看:for (int i = 0; i & 4; ++i)\n{\n
// 为什么是33和5呢? 33=32+1, 5=4+1\n
int j = i * 5;\n
int k = (i + 1) * 5;\n\n
for (int l = 0; l & 4; ++l)\n
int i1 = (j + l) * 33;\n
int j1 = (j + l + 1) * 33;\n
int k1 = (k + l) * 33;\n
int l1 = (k + l + 1) * 33;\n\n
for (int i2 = 0; i2 & 32; ++i2)\n
double d0 = 0.125D;\n
double d1 = this.heightMap[i1 + i2];\n
double d2 = this.heightMap[j1 + i2];\n
double d3 = this.heightMap[k1 + i2];\n
double d4 = this.heightMap[l1 + i2];\n
// 于高度y方向线性插值\n
double d5 = (this.heightMap[i1 + i2 + 1] - d1) * 0.125D;\n
double d6 = (this.heightMap[j1 + i2 + 1] - d2) * 0.125D;\n
double d7 = (this.heightMap[k1 + i2 + 1] - d3) * 0.125D;\n
double d8 = (this.heightMap[l1 + i2 + 1] - d4) * 0.125D;\n
// 于x和z方向线性插值\n
for (int j2 = 0; j2 & 8; ++j2)\n
double d9 = 0.25D;\n
double d10 = d1;\n
double d11 = d2;\n
double d12 = (d3 - d1) * 0.25D;\n
double d13 = (d4 - d2) * 0.25D;\n\n
for (int k2 = 0; k2 & 4; ++k2)\n
double d14 = 0.25D;\n
double d16 = (d11 - d10) * 0.25D;\n
double lvt_45_1_ = d10 - d16;\n\n
for (int l2 = 0; l2 & 4; ++l2)\n
if ((lvt_45_1_ += d16) & 0.0D)\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, STONE);\n
else if (i2 * 8 + j2 & this.settings.seaLevel)\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, this.oceanBlock);\n
d10 += d12;\n
d11 += d13;\n
d1 += d5;\n
d2 += d6;\n
d3 += d7;\n
d4 += d8;\n
}\n}\n我们从i2循环开始切入:for (int i2 = 0; i2 & 32; ++i2)\n循环体开头的这段:double d0 = 0.125D;\ndouble d1 = this.heightMap[i1 + i2];\ndouble d2 = this.heightMap[j1 + i2];\ndouble d3 = this.heightMap[k1 + i2];\ndouble d4 = this.heightMap[l1 + i2];\n// 于高度y方向线性插值\ndouble d5 = (this.heightMap[i1 + i2 + 1] - d1) * 0.125D;\ndouble d6 = (this.heightMap[j1 + i2 + 1] - d2) * 0.125D;\ndouble d7 = (this.heightMap[k1 + i2 + 1] - d3) * 0.125D;\ndouble d8 = (this.heightMap[l1 + i2 + 1] - d4) * 0.125D;d\nd5是heightMap[i1+i2+1]和d1的差的八分之一,而d1是heightMap[i1+i2],所以d5就是heightMap中相邻两个值得差的八分之一。再看下一个循环:for (int j2 = 0; j2 & 8; ++j2)\n循环次数是8,这说明什么?然后看到这个循环的最后:d1 += d5;\nd2 += d6;\nd3 += d7;\nd4 += d8;\n如果你是一个熟练的程序员,你可能已经看出了端倪,如果没有也不要紧,我来解释。为了方便理解,设:(以下使用知乎自带的公式功能)则:(Δ = d5,为了表示数理公式中常见的“变化量”特地这么用)然后设j2=n时d1=v(n),有:有中学数学知识的朋友不难发现,这样就是在a和b之间做了一个8步的线性插值,也就是说,它是经过(0,a)和(8,b)的一次函数。这正是线性插值的含义,而之后的怀疑也就不攻自破了。(见后文)d2和d6,d3和d7,d4和d8,也是同理。对了,d0就是0.125,而这个八分之一也在之后几行中频繁出现,因此我猜原来的代码是这样的:const double d0 = 1 / 8f;\ndouble d1 = this.heightMap[i1 + i2];\ndouble d2 = ...; // 省略\ndouble d5 = (this.heightMap[i1 + i2 + 1] - d1) * d0;\ndouble d6 = ...; // 省略\n因为java编译器会把设定为const(常量)的值直接替换进去,所以所有的d0都被替换成了直接计算出来的0.125,而d0也保留了下来。至于为什么是八分之一,别着急。知道了这个,再来看j2的循环:for (int j2 = 0; j2 & 8; ++j2)\n{\ndouble d9 = 0.25D;\ndouble d10 = d1;\ndouble d11 = d2;\ndouble d12 = (d3 - d1) * 0.25D;\ndouble d13 = (d4 - d2) * 0.25D;\n\nfor (int k2 = 0; k2 & 4; ++k2)\n{\n
// 此处省略若干行\n\n
d10 += d12;\n
d11 += d13;\n}\n\n// 讲过了\nd1 += d5;\nd2 += d6;\nd3 += d7;\nd4 += d8;\n}\n发现了什么?k2的循环次数是4,而0.25正是四分之一。同样地,d12就是d3和d1差的四分之一,而d10随着k2从0到4(或者说到3,因为到4的时候就退出循环了)的变化从d1变到d3,巧了!清楚一点:(这里除号“/”都表示数学除法,不是编程的整数除法)k2 = 0时,d10 = (4 / 4) * d1 + (0 / 4) * d3, d11 = (4 / 4) * d2 + (0 / 4) * d4k2 = 1时,d10 = (3 / 4) * d1 + (1 / 4) * d3, d11 = (3 / 4) * d2 + (1 / 4) * d4k2 = 2时,d10 = (2 / 4) * d1 + (2 / 4) * d3, d11 = (2 / 4) * d2 + (2 / 4) * d4k2 = 3时,d10 = (1 / 4) * d1 + (3 / 4) * d3, d11 = (1 / 4) * d2 + (3 / d) * d4这,不又是一个线性插值吗?不又是一个4步的线性插值吗?是不是感觉发现了什么规律?我们再来看k2的这个循环:for (int k2 = 0; k2 & 4; ++k2)\n{\ndouble d14 = 0.25D;\ndouble d16 = (d11 - d10) * 0.25D;\ndouble lvt_45_1_ = d10 - d16;\n\nfor (int l2 = 0; l2 & 4; ++l2)\n{\n
if ((lvt_45_1_ += d16) & 0.0D)\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, STONE);\n
else if (i2 * 8 + j2 & this.settings.seaLevel)\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, this.oceanBlock);\n
}\n}\n\nd10 += d12;\nd11 += d13;\n}\n可能大家要疑惑了:这个lvt_45_1_又是什么东西啊?先普及一下,if ((lvt_45_1_ += d16) & 0.0D)这句怎么理解:+=是一个Java的运算符,就是把左边给出的变量加上右边的值,然后把加上之后的值返回。或者说(假定函数print能够打印出求值的结果):int i = 1;
// 定义一个整数i,值是1\nprint(i);
// 输出1\nprint(i += 1);
// 输出2\n// 此时i的值已经被赋为2,因为:\nprint(i);
// 输出2\n现在我们来改写一下:int i = 1;\nprint(i);
// 1\ni += 1;\nprint(i);
// 2\nprint(i);
// 2\n是不是一样?再看前面的代码,我们如果把这个+=改掉,就变成了:for (int k2 = 0; k2 & 4; ++k2)\n{\ndouble d14 = 0.25D;\ndouble d16 = (d11 - d10) * 0.25D;\ndouble lvt_45_1_ = d10 - d16;\n\nfor (int l2 = 0; l2 & 4; ++l2)\n{\n
lvt_45_1_ += d16;\n
// 看这里\n
if (lvt_45_1_ & 0.0D)\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, STONE);\n
else if (i2 * 8 + j2 & this.settings.seaLevel)\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, this.oceanBlock);\n
}\n}\n\nd10 += d12;\nd11 += d13;\n}\n嗯,l2的循环次数又是4,d16又是d11和d10的差的四分之一。你说,不对啊,怎么 += d16的句子跑到l2循环的前面来了?不要紧,我们再列一张表来看,每次执行到“// 看这里”的时候,lvt_45_1_都是几:l2 = 0时,lvt_45_1_ = (4 / 4) * d10 + (0 / 4) * d11l2 = 1时,lvt_45_1_ = (3 / 4) * d10 + (1 / 4) * d11l2 = 2时,lvt_45_1_ = (2 / 4) * d10 + (2 / 4) * d11l2 = 3时,lvt_45_1_ = (1 / 4) * d10 + (3 / 4) * d11看,多么相似!又是线性插值!————以防疲劳的分割线,可以去喝口水再看————注:接下来会用图片讲解,图片均为原创,转载必究。前面k2的循环,或者说l2的循环里面,还有一段东西没讲:if (lvt_45_1_ & 0.0D)\n{\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, STONE);\n}\nelse if (i2 * 8 + j2 & this.settings.seaLevel)\n{\n
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, this.oceanBlock);\n}\n别的猜猜看倒也不难看懂:setBlockState大概就是设置方块,当lvt_45_1_大于0时就设置某个地方的方块为石头(STONE),如果小于等于0,而且位置在海平面(seaLevel)以下时,就设置它为水方块(oceanBlock)。你可能有点懵:i*4+k2, i2*8+j2, l*4+l2又是什么?慢慢来。看else if这里,(i2 * 8 + j2 & this.settings.seaLevel),小于海平面,不对,应该说,是低于海平面。所以很显然,i2 * 8 + j2就是方块的高度(竖直方向上的位置)。于是你又会做一个大胆地猜测:是不是i * 4 + k2就是水平方向上的位置、l * 4 + l2就是前后方向上的位置呢?没错!当然不能拍脑袋就说没错,说没错是要有证据的。我们回过头去看这整一个东西的前三重循环:for (int i = 0; i & 4; ++i)\n{\n
// 为什么是33和5呢? 33=32+1, 5=4+1\n
int j = i * 5;\n
int k = (i + 1) * 5;\n\n
for (int l = 0; l & 4; ++l)\n
int i1 = (j + l) * 33;\n
int j1 = (j + l + 1) * 33;\n
int k1 = (k + l) * 33;\n
int l1 = (k + l + 1) * 33;\n\n
for (i2 = 0; i2 & 32; ++i2)\n
double d0 = 0.125D;\n
double d1 = this.heightMap[i1 + i2];\n
double d2 = this.heightMap[j1 + i2];\n
double d3 = this.heightMap[k1 + i2];\n
double d4 = this.heightMap[l1 + i2];\n
// 于高度y方向线性插值\n
double d5 = (this.heightMap[i1 + i2 + 1] - d1) * 0.125D;\n
double d6 = (this.heightMap[j1 + i2 + 1] - d2) * 0.125D;\n
double d7 = (this.heightMap[k1 + i2 + 1] - d3) * 0.125D;\n
double d8 = (this.heightMap[l1 + i2 + 1] - d4) * 0.125D;\n
// i2循环的剩余部分,讲过了,这里就省略掉了\n
}\n}\n取每重循环的第一个变量,你看:for (int i = 0; i & 4; ++i)\n{\n
int j = i * 5;\n\n
for (int l = 0; l & 4; ++l)\n
int i1 = (j + l) * 33;\n\n
for (i2 = 0; i2 & 32; ++i2)\n
double d1 = this.heightMap[i1 + i2];\n
double d5 = (this.heightMap[i1 + i2 + 1] - d1) * 0.125D;\n
// i2循环的剩余部分\n
}\n}\n我们来手动运行一下:i = 0, j = 0, l = 0, i1 = (0 + 0) * 33 = 0, i2 = 0, d1 = heightMap[0], d5 = (heightMap[1] - heightMap[0]) * 0.125i2 = 1, d1 = heightMap[1], d5 = (heightMap[2] - heightMap[1]) * 0.125i2 = 2, d1 = heightMap[2], d5 = (heightMap[3] - heightMap[2]) * 0.125...i2 = 32, d1 = heightMap[32], d5 = (heightMap[33] - heightMap[32]) * 0.125还记得前面说过的吗?i2的循环里面是从heightMap[i1+i2]到heightMap[i1+i2+1]的线性插值,那现在再看,是不是就是从第1个到第2个、从第2个到第3个……从第32个到第33个的一连串线性插值?(按照程序的数组下标是第0个到第1个,……为理解方便都加了1)对!正是这样。再看,j1 = (j + l + 1) * 33, i1 = (j + l) * 33,所以j1 = i1 + 33。那么d2是heightMap[j1 + i2],d6是(heightMap[j1 + i2 + 1] - heightMap[j1 + i2]) * 0.125,带入i=0, l=0来看,d2和d6正好组成了从第34个到第35个,第35个到第36个……第65个到第66个的一连串线性插值。再往回带一下,k = (i + 1) * 5, j = i * 5,所以k = j + 5。j1 = (j + l) * 33, k1 = (k + l) * 33 = ((j + 5) + l) * 33 = (j + l) * 33 + 5 * 33 = j1 + 5 * 33。d3是heightMap[k1 + i2],d3和d7组成了从第166个,到第167个,……到第198个的线性插值。l1 = (k + l + 1) * 33 = k1 + 33,d4是heightMap[l1 + i2],d4和d8组成了从第199个,到第200个,……到第231个的线性插值。然后l变成了1,i1, j1, k1, l1都加了33,d1开始成了第34到第35……66个的线性插值,诶,这不就是原来d2开始的那一串吗?没错。这是d2开始是第67,第68……第99个的线性插值,而当l再加上1变成2的时候,d1开始又成为了第67,第68……第99个的线性插值。同样地,l=1时,d3是从第199个到第231个的线性插值,d4是从第232到第264个的线性插值;l=2时,d3又成了从第232个到第264个的线性插值,d4又……以此类推。还不够清楚的话,这么说如何?(前面都加上1也是因为第X个是从1开始数的)l = 0时,166 = (1 + 5 * 33) + 0 * 33 + 0, 199 = (1 + 33 + 5 * 33) + 0 * 33 + 0,167 = (1 + 5 * 33) + 0 * 33 + 1, 200 = (1 + 33 + 5 * 33) + 0 * 33 + 1,168 = (1 + 5 * 33) + 0 * 33 + 2, 201 = (1 + 33 + 5 * 33) + 0 * 33 + 2,l = 1时,199 = (1 + 5 * 33) + 1 * 33 + 0, 232 = (1 + 33 + 5 * 33) + 1 * 33 + 0,200 = (1 + 5 * 33) + 1 * 33 + 1, 233 = (1 + 33 + 5 * 33) + 1 * 33 + 1,l = 2时,232 = (1 + 5 * 33) + 2 * 33 + 0, 265 = (1 + 33 + 5 * 33) + 2 * 33 + 0,233 = (1 + 5 * 33) + 2 * 33 + 1, 266 = (1 + 33 + 5 * 33) + 2 * 33 + 1,……我们可以把l看做x轴(左右方向),i2看做y轴(上下方向),那么d1……d8的索引值(heightMap[X]里面的X)就是C + x * 33 + y,其中C是对于每个dn都固定的常数,x, y是一个点在这两个方向上的位置。再进一步想,现在j1 = i1 + 33, l1 = k1 + 33又代表了什么呢?它代表了由j1决定的d2比i1决定的d1其实x轴上大1,由l1决定的d4比k1决定的d3在x轴上也要大1:266 = (1 + 33 + 5 * 33) + 2 * 33 + 1 = (1 + 5 * 33) + (2 + 1) * 33 + 1i2变大1使d1~d4的y轴位置都变大1;l变大1使i1, j1, k2, l1都变大33的实际意义就是把d1~d4的x轴位置都变大1。又按照我们的定义,x轴是左右方向,y轴是上下方向,那也就是说,如果i2的循环是每次往上移动一格,从heightMap[0]到heightMap[1],从heightMap[12]到heightMap[13]的话,l(以防看不清,小写的L)的循环就是每次往右移动一行:heightMap[0]到heightMap[33],heightMap[33]到heightMap[66]。这样的:那最外面一层循环i呢?或许大家已经猜到了,i在这里就对应了三维中的最后一维——z轴(前后方向)上的位置。那最外面一层循环i呢?或许大家已经猜到了,i在这里就对应了三维中的最后一维——z轴(前后方向)上的位置。重新贴一遍代码:for (int i = 0; i & 4; ++i)\n{\n
// 为什么是33和5呢? 33=32+1, 5=4+1\n
int j = i * 5;\n
int k = (i + 1) * 5;\n\n
for (int l = 0; l & 4; ++l)\n
int i1 = (j + l) * 33;\n
int j1 = (j + l + 1) * 33;\n
int k1 = (k + l) * 33;\n
int l1 = (k + l + 1) * 33;\n\n
for (i2 = 0; i2 & 32; ++i2)\n
double d0 = 0.125D;\n
double d1 = this.heightMap[i1 + i2];\n
double d2 = this.heightMap[j1 + i2];\n
double d3 = this.heightMap[k1 + i2];\n
double d4 = this.heightMap[l1 + i2];\n
// 于高度y方向线性插值\n
double d5 = (this.heightMap[i1 + i2 + 1] - d1) * 0.125D;\n
double d6 = (this.heightMap[j1 + i2 + 1] - d2) * 0.125D;\n
double d7 = (this.heightMap[k1 + i2 + 1] - d3) * 0.125D;\n
double d8 = (this.heightMap[l1 + i2 + 1] - d4) * 0.125D;\n
// i2循环的剩余部分,讲过了,这里就省略掉了\n
}\n}\ni增大1时,j和k增大5, i1, j1, k1, l1增大5*33。再看:i, l, i2 = 0时,d3是heightMap的第166个,166 = (1 + 5 * 33) + 0 * 33 + 0 = 1 + (0 + 1) * (5 * 33) + 0 * 33 + 0d1是heightMap的第1个,1 = 1 + 0 * (5 * 33) + 0 * 33 + 0i = 1, l, i2 = 0时,d3是heightMap的第331个,331 = 1 + (1 + 1) * (5 * 33) + 0 * 33 + 0d1是heightMap的第166个,166 = 1 + 1 * (5 * 33) + 0 * 33 + 0看,是不是?每次i增大1,便是将d1~d4的位置往后移动一层。那么heightMap最大应该有多大呢?当i1 = 3, l = 3, i2 = 31时,d4的索引值是:(3 + 1) * (5 * 33) + (3 + 1) * 33 + (31 + 1) = 824 = 5 * 33 * 5 - 1所以我说,heightMap其实是一个三维的立方阵,索引值为 i * (5 * 33) + j * 33 + k 代表的是在这个立方阵中(x, y, z)坐标为(i, j, k)的点上的值。那之所以用5*33*5而不是4*32*4,是因为在选定i(代表z轴位置)、l(代表x轴位置)、i2(代表y轴位置)之后,选取(i, i2, l)到(i+1, i2+1, l+1)所代表的立方体,用作接下来的插值。在这个过程中,虽然i只能取到3,但是当我们求值z=i+1位置上的点的时候,就需要z轴上多出一层——也就是4层不够,要5层。而其他两个维度也同理,所以就要5*33*5。至于这个选出来的立方体做什么用,我们接下来再说。————再次喝水分割线————前面我们讲到,i*4+k2, i2*8+j2, l*4+l2是最后一重循环里面设置方块位置的x, y, z坐标。刚刚我们看到,i, i2, l又分别代表在5*33*5的立方阵中选取小立方体的x, y, z坐标,这两个就对应上了。可是i为什么要乘4,i2为什么要乘8,l又为什么要乘4呢?是因为,一个完整的区块是16*256*16,而前面的立方阵只有5*33*5——或者说,去掉一层,4*32*4。通俗地说,我只给了你一张100*100的图片,要你给我一张200*200的图片,怎么办?——放大。这可不算一个答案。放大的方法有很多种,这里实现的方法是其中最快也最简单的:线性插值。还是打个比方说吧。我有一个小方格,它的四个顶点上都有一个数:现在我想让它变成3*3=9个小方格,于是我这么做:1. 把小方格变成大方格:2. 设大方格的边长为1,数字所在的点都是三等分点,我们来线性插值:相信看到这里的朋友们稍一动脑,就能想出其中的原理。那么现在我们要把4*32*4变成16*256*16,只要把原来每一个1*1*1的小立方体放大成新的4*8*4的立方体就行了——这个小立方体就是前一节中前三个循环中我们取出的这个立方体,而放大的方式就是刚刚说的:线性插值。而这也是MC这段代码中后三个循环所做的。d1~d4代表了小立方体底面的四个顶点,而另外四个顶点,则是在d5~d8中体现了:d5*8+d1正是d1正上方顶点的值。现在再来看i*4+k2, i2*8+j2, l*4+l2这三个坐标的意义,应该已经相当清楚了:i, i2, l是前三个循环中选取小立方体的x,y,z坐标,4,8,4是放大的倍率,而剩下的k2, j2, l2就是另外三个循环中生成的x,y,z坐标,用于遍历线性插值放大后4*8*4的大立方体中的每一个点——也就是最后我们生成的区块中的每一个方块。总结一下就是说,这段代码遍历所有的小立方体,然后用线性插值把他们放大,填充到区块中去。至于为什么这么说,后文自有描述。兔子先生:4.我从头至尾都是以个人身份跟你讲话,你偏要搞什么尊重不尊重工作室成员。我哪里用工作室来压你了吗?我用工作室来证明我自己的水准了吗?倒是你在用什么【mod开发者和“许多同行”研究好几年】的身份来强行为自己增加权威性,要不要太无耻?.不知道表达尊重有什么问题。我是听说兔子先生是某国产工作室成员,怕有人喷我不尊重他人的努力,才出此言,现在看来他并不在乎这个,也算是我多此一举。至于某些人身攻击之类,略过便是。p.s.是你先到我的答案下面主动吵架,毫无理由的挑衅一通,现在又贼喊捉贼,说是我在找你吵架。我认为文明用语是讨论问题的先决条件。拿着别人翻译的东西,拿着随便试试猜到功能的东西,就敢说自己懂了,还敢出来误人子弟,上来乱讲乱教,实在不敢恭维。“别人翻译”:请在您能找到的任何平台通过任何途径搜索我翻译的文章的中文,如果能找到比我发表答案(日晚)更早的、内容几乎一模一样翻译版本,我甘拜下风——猴子拿打字机也有几率打出莎士比亚的文章,我自然不能和莎翁相提并论。“随便试试猜到功能”:同样没有任何证据,当然你可以把任何人类活动归结于“概率作用”,大体也都是“随便试试”,而是否“误人子弟”、是否“乱”,我会一点点地用完全可以验证的代码和逻辑推导向各位证明。我说的确实少,但我在2014年的时候了解的就那么多,不懂的知识,只懂一半的知识,实在不敢强行说自己懂了。也不知我是否当原话奉还:就敢说自己懂了,还敢出来误人子弟,上来乱讲乱说,实在不敢恭维。若是有理,证明之,否则就不要随便发言,这一点相信没有人是不懂的。兔子先生:总之,你也就拿一点翻译出来的东西和乱实验的玩意儿唬弄唬弄外行吧。搞这么长答案,没几句在点子上,浪费阅读者时间。旧事重提。fml里每个类是干什么的类名上面都有写,只要接触过的人一眼就能看懂,贴一张目录截图都更省事,用你研究?用你教?我不希望这个问题只让“接触过的人”来看,知识不应该只局限于“圈内人”,而是希望能够把知识分享给所有想要了解的人。当然,如果能够培养一两个国内mod开发者,作为他们的领路人,那自然是最好的。顺便,Minecraft足有上万个类,我想就算每个类看一眼名字就能看得懂,也远远没有前人理解之后总结提炼的介绍来的易懂、便捷。你做mod就做你的mod,随便搞几个GUI,搞几个新方块,本来懂了每个类的功能也就够了。你不知道复杂算法我不怪你无知,没人什么都懂。但是本来咱们井水不犯河水,但你非要嘴贱到我这里来撒野,还漏洞百出,我不扒一扒你,显得我好欺负么?如果有一个更好的用来表达无语的词,那现在的我一定会用它的。我怎么觉得,这个口吻,有点像小时看的武侠小说呢?兔子先生:另外,你连白噪声和Perlin Noise都分不清,别再科普了求你了。Perlin Noise比白噪声多一个相关性,不懂就不要乱讲!两点之间的距离越近,相关性越强,简单白噪声不具备这种特征!兔子先生:普通白噪声和柏林噪声的区别分不清,这种初级错误实在太丢人了。别再强行撑场面了。我劝你啊,肚子里有十分教八分,有八分教五分,像我这样自知肚子里有五分的,讲三分就够了。强行讲太多,越讲越错。perlin noise是我要讲的第二个重点。这个问题相较前一个更为专业一些。我知道如果我只是用“提问-回答”的模式可能让大家搞不清楚我俩谁说的是真的,谁说的又是为了反驳而杜撰出来的,那么我会改用一种更为清晰的手段。我会完整地解释perlin noise的算法,并且使得每一步都是尽量可验证的:各位可以翻原始论文,可以翻英文维基或者各种中文百科,相信这一步的能力各位都是拥有的。请看:——————perlin noise讲解的分割线————本节内容参考资料为相对权威的英语维基百科: 以及发明者Ken Perlin的实现:在此直接引用+翻译,欢迎原创性查证,欢饮准确性查证。Algorithm Detail算法细节Perlin noise is most commonly implemented as a two-, three- or four-dimensional , but can be defined for any number of dimensions. An implementation typically involves three steps: grid definition with random gradient vectors, computation of the
between the distance-gradient vectors and interpolation between these values.柏林噪声通常被实现为一个2、3或4维函数,然而它有任何维度上的定义。一个算法的视线通常涉及到三步:使用随机梯度向量的网格定义,距离向量与梯度向量的点积计算以及在这些值之间的插值。Grid Definition网格定义Define an n-dimensional grid. At each point on the grid (node) assign a random gradient vector of unit length in n dimensions. For a two-dimensional grid each node will be assigned a random vector on the unit circle, and so forth for higher dimensions. The one-dimensional case is an exception - here the gradient is a random scalar between -1 and 1.定义一个n维的网格。对网格上的任意一点(节点)赋上一个n维的随机单位梯度向量。在一个二维的网格上每个节点都会被赋上一个单位圆上的向量,在更高维上以此类推。一维是一个特殊情况:这里的梯度就是一个从-1到1的随机标量。Computation of the (pseudo-) random gradients in one and two dimensions is trivial using a . For higher dimensions a Monte Carlo approach can be used where random Cartesian coordinates are chosen in a unit cube, points falling outside the unit ball are discarded, and the remaining points are normalized to lie on the unit sphere. The process is continued until the required number of random gradients are obtained.对这些(伪)随机的向量的计算在一维和二维的情况下是平凡的,只需使用一个随机数生成器(按照ken perlin的实现,这里用的就是白随机 ——译者注)。在更高维上可以使用蒙特卡洛逼近:在单位立方体中选取随机的笛卡尔坐标(正交直角坐标系 ——译者注),而落在单位球(广义的n维球,即离远点距离不超过单位长度的点的集合 ——译者注)之外的点被丢弃,剩下的点被标准化,使得他们刚好落在单位球的球面上。这个过程将被一直重复下去,直到已经生成了足够量的随梯度向量。In order to negate the expensive process of computing new gradients for each grid node, some implementations use a
for a finite number of precomputed gradient vectors.The use of a hash also permits the inclusion of a random seed where multiple instances of Perlin noise are required.为了避免为了网格上的每个顶点去花费大量时间生成新的梯度向量,一些实现使用一个哈希表来储存一定数量的预先计算好的向量(这也是ken perlin实现中的用法 ——译者注)。哈希表的使用同时允许在有多个perlin noise被需要的时候用一个随机种子来区别。Dot Product点积Given an n-dimensional argument for the noise function, the next step in the algorithm is to determine into which grid cell the given point falls. For each corner node of that cell, the distance vector between the point and the node is determined. The
between the gradient vector at the node and the distance vector is then computed.为这个噪声函数给定一个n维的输入后,算法的下一步就是来确定给定的n维点落入的那个单元格。(这里就用到了单元格,单元格外的梯度向量并不被涉及 ——译者注)对单元格的每一个顶点都要计算给定点到这个顶点的举例,然后再计算这个举例向量以及顶点上的梯度向量的点积。For a point in a two-dimensional grid, this will require the computation of 4 distance vectors and dot products, while in three dimensions 8 distance vectors and 8 dot products are needed. This leads to the
complexity scaling.每一个二维网格上的点都要计算4次距离向量和梯度向量之间的点积,而在三维中就要计算8次点积。这就导致了的时间复杂度。Interpolation插值The final step is interpolation between the
dot products computed at the nodes of the cell containing the argument point. This has the consequence that the noise function returns 0 when evaluated at the grid nodes themselves.最后一步则是在这个计算好的点积之间关于包含着这个给顶点的单元格的各个顶端插值。这样做的后果就是如果在格点本身上面对这个噪声函数求值的话,函数将会返回0。Interpolation is performed using a function that has zero first
(and possibly also second derivative) at the
grid nodes. This has the effect that the
of the resulting noise function at each grid node coincides with the precomputed random gradient vector there. If n=1, an example of a function that interpolates between value
at grid node 0 and value
at grid node 1 iswhere the
function was used.插值是通过执行一个在这个单元格顶点上一阶导数(甚至二阶导数)为0的函数来实现的。这样的效果就是最终噪声函数在单元格顶点上的梯度恰好就是那里的预先计算好的梯度向量。当n=1时,一个在节点0的值和节点1的值之间进行插值的函数示例是:而这里正使用了smoothstep平滑函数。Noise functions for use in computer graphics typically produce values in the range [-1.0,1.0]. In order to produce Perlin noise in this range, the interpolated value may need to be scaled by some scaling factor.在计算机图形学中使用的噪声函数典型地输出在[-1.0, 1.0]范围内的值。为了在这个范围内产生出这个范围内的perlin noise,插值之后的值可能需要乘以一个缩放系数来缩放。那么smoothstep又是什么呢?请看维基:Smoothstep is an
function commonly used in
and .Smoothstep平滑函数是一个在计算机图形学和视频游戏引擎中十分常用的插值函数。The function depends on two parameters, the \"left edge\" and the \"right edge\", with the left edge being assumed smaller than the right edge. The function takes a real number x as input and outputs 0 if x is less than or equal to the left edge, 1 if x is greater than or equal to the right edge, and smoothly interpolates between 0 and 1 otherwise. The slope of the smoothstep function is zero at both edges. This makes it easy to create a sequence of transitions using smoothstep to interpolate each segment rather than using a more sophisticated or expensive interpolation technique.这个函数有两个参数:“左端”和“右端”,而不妨设左端小于右端。这个函数以一个实数x为输入,当x小于等于左端时输出0,当x大于等于右端时输出1,并且在0和1之间平滑(一阶导数连续 ——译者注)插值,如果x在左端和右端之间。平滑函数的斜率(=导数 ——译者注)在两端都是0,这也使得在一系列变化中可以轻易地用平滑函数对每一段进行插值,而不是使用一个复杂得多的或者耗时巨大的插值技术。 suggests an improved version of the smoothstep function which has zero 1st and 2nd order
at x=0 and x=1:Ken Perlin (perlin noise发明人 ——译者注)建议了一个更好的平滑函数版本,因为它的一阶和二阶导数都在x=0和x=1时连续:这也解答了\"smoothstep是什么\"的问题。我想perlin noise到这里已经很明白了。请看点积中的那一步,先要选取点所在的单元格,然后(仅)根据单元格各个顶点上的值来决定最终函数的产出。也就是说,如果我们保持所有单元格顶点上的值不变,并且求值的点在单元格之内的相对位置也不变, 不管单元格移到哪里,函数的产出都是一样的。这样说来,哪来什么“相关性”?不重合两个点,谁的值能决定另一个的值?哪怕说的是正相关性,只要跨两个点所属的单元格的顶点没有交集,两个点上算出来的值仍然根本没有什么“相关性”。我前面说的是,先用白随机在每个节点(grid point / node)上选取值(在二维上的值表现为梯度向量),然后再每个单元格之内插值,并不是说“perlin noise就是白噪声”。这一点各位也可以读我的评论验证,迫于篇幅不再引述。以防歧义,我再解释一小点:我回复里面说“三次方程”,是因为smoothstep主词条的平滑函数方程就是一个三次方程,本来也是常用的平滑函数,后来ken perlin因为它的一阶导数有线性分量而会在单元格边界处显得不自然,所以就改成了现在的五次方版本。如果各位对有仔细看我之前写的这么多,可能会发现一点:perlin noise是在一个网格中插值,如果把网格中的每一个单元格(以三维为例)看作是1*1*1的小立方体,那么生成的perlin noise实际上是回更大的,这样才需要用插值填充其中原先缺乏定义的格点(节点/点)。而setBlocksInChunk这个函数的作用,也是拿4*32*4网格中每一个小立方体放大成4*8*4的大立方体,最后组成16*256*16的区块。两者十分相像,而实际的区别在于,前者使用的是刚刚讲的平滑函数插值,而后者用的是再之前讲的线性插值。这么做的根本原因其实是效率。在我的评论中也说道过,三次的平滑函数至少需要计算3次浮点数乘法和三次浮点加法,而按照ken perlin的建议改成五次的平滑函数的话,就要5次浮点乘法和加法了,对比线性插值只要1次乘法和一次加法,肯定要慢上不少。perlin noise和setBlocksInChunk都是一个放大的过程,只有插值方法不同,这实际上也是处于性能的考量。notch先生成4*32*5(或者说5*33*5)的perlin noise图,然后把它用线性插值放大,也是因为这样做比直接生成16*256*16的perlin noise要快得多,而且最终效果也不差。这样应该能够比较好地解答的问题,如果还有异议,请先证明我上述哪一步有问题,谢谢。我相信从原文到翻译,知乎上能够验证的人大有人在,完全算不上什么难题,每一步都可以被验证为正确。毫无理由的批评,如果只是拿一堆词汇来糊弄外行人的话,可是行不通的。兔子先生:最后,把你的话原封不动还给你:我知道您被挂了很不开心,但是我只想说一句,水平不够就不要装X乱答,糊弄糊弄外行人就算了,糊弄内行人就不要想了。您要是还有任何问题,可以私聊或者评论,但是讨论奉陪,骂人不奉陪,说的有理我接受,说的没理我也照批不误。如果对自己的“实力”有信心的话,尽管来战吧。——谢邀。我:。。。。。。。。。。。。。好,你等着,这里空间不够,我到回答里面回复。到现在为止你也没有提出真正的问题,也就是说,你只是说“这个不对”,从未说过“怎么不对”,更没说“正确答案是什么”,恐怕是死鸭子嘴硬啊。我:不在电脑前,先说一两句。首先perlin noise是在单元里面选点,每个单元里面都是平滑过的,而单元顶点的值是白随机。只有在同一个单元里面的点有相关性。我:顺便,我希望大家有一点都要做到:不要骂人。兔子先生:哟,一边说对方死鸭子嘴硬一边希望大家不要骂人。我纠正你那么多基础知识错误,你说我没说怎么不对,也没说正确答案是什么。所以到底是谁死鸭子嘴硬呢。现在又装作空间不够,把撕逼搞进回答正文里。你这人简直高风亮节。兔子先生:另外,你这句也依然说错了。perlin noise不是在单元里选点,perlin noise是递归式无限细化的计算,根本不存在“单元”的概念,只存在递归次数的概念。所以根本不存在“单元顶点的值是白随机”这种说法。你现在说一句错一句,回复的错误率是100%哟。见上文。我也很想知道把这篇文章整个拆分开来放到评论区是种什么样的体验。最后,总结:兔子先生:既然你说我没指出你怎么不对,我来总结一下前文:1.你在我的评论里说chunk和perlin noise不是mc的设计,mc用simplex noise,这是不对的,修改:chunk和perlin noise都是mc的核心设计。2.你认为fml里的各种变量未经解析也一样能够阅读,然而你自己证明了自己无法阅读。3.你认为你能读懂mc的源码,然而你自己证明了自己没有读懂。你认为是magic number的地方,事实上是perlin noise的算法的一部分。4.插值算法是perlin noise的必要步骤之一,那段话是你的理解错误。你坚持认为Notch在另外的地方实现了perlin noise的插值算法,却无法提供出对应代码,所以你是错的。5.你强行科普的“白噪声也就是perlin noise”是错误的,普通的白噪声与perlin noise之间有非常大的区别,就是两个点距离越近,相关性越强。6.你说“perlin noise里同一个单元里的点才具有相关性”,然而perlin noise的算法是递归式的,并不存在“同一个单元”的概念。7.你来我的评论下挑衅然后反咬我挑衅你,你骂我之后反而提倡不骂人,你自己不懂强装懂还说我死鸭子嘴硬,你不要脸。1. 参考本文第一段。2. 参考本文第二段。3. 参考本文第二段+第三段。4. 参考本文第三段。5. 参考本文第三段。6. 参考本文第三段。7. 参考全文。以上。","updated":"T03:12:23.000Z","canComment":false,"commentPermission":"anyone","commentCount":6,"collapsedCount":0,"likeCount":13,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","titleImage":"","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"游戏"},{"url":"/topic/","id":"","name":"游戏开发"},{"url":"/topic/","id":"","name":"算法"}],"adminClosedComment":false,"titleImageSize":{"width":0,"height":0},"href":"/api/posts/","excerptTitle":"","tipjarState":"closed","annotationAction":[],"sourceUrl":"","pageCommentsCount":6,"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T11:12:23+08:00","url":"/p/","lastestLikers":[{"bio":null,"isFollowing":false,"hash":"f572cd5b9ebaff8e44749ea","uid":015600,"isOrg":false,"slug":"da-shi-luo-mu","isFollowed":false,"description":"","name":"大世落幕","profileUrl":"/people/da-shi-luo-mu","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"户外旅游","isFollowing":false,"hash":"0fba4ab9e2df1b9ac851","uid":211100,"isOrg":false,"slug":"shinonomenya","isFollowed":false,"description":"","name":"ShinonomeNya","profileUrl":"/people/shinonomenya","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":null,"isFollowing":false,"hash":"4761976ded0ce8342fbd","uid":287600,"isOrg":false,"slug":"zhang-chen-38-44","isFollowed":false,"description":"还债,磨练,平静,坚持。","name":"张忱","profileUrl":"/people/zhang-chen-38-44","avatar":{"id":"v2-061f3a03ffaef56b82ba7b","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":null,"isFollowing":false,"hash":"07d08ccf1a49fa6c2db1bb","uid":389600,"isOrg":false,"slug":"ma-xi-cheng-27","isFollowed":false,"description":"","name":"马希成","profileUrl":"/people/ma-xi-cheng-27","avatar":{"id":"v2-72d161de146ddac","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"用思考代替发问","isFollowing":false,"hash":"3e45d2efb","uid":422800,"isOrg":false,"slug":"ye-yu-qian-luo","isFollowed":false,"description":"","name":"夜雨千落","profileUrl":"/people/ye-yu-qian-luo","avatar":{"id":"v2-754ae35dcb1bf4eeb861a0f2c3a2d19b","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false}],"summary":"前些天回答了个问题:先在此对 的疑惑做出解答。或有涉及人身攻击之语,一概不谈,也希望各位都能做到文明讨论。 先抓重点。我仔细看了之前的评论,发现确实出言不妥。在此向 道歉,但是对您对我能力的怀疑、诽谤以…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"annotationDetail":null,"commentsCount":6,"likesCount":13,"FULLINFO":true}},"User":{"std4453":{"isFollowed":false,"name":"王远","headline":"29×47×193×887","avatarUrl":"/0bda_s.png","isFollowing":false,"type":"people","slug":"std4453","bio":"根本就不敢打开IDE只能假装自己是程序员的程序员。","hash":"59a37d22faddedbd441d3c66213c10ad","uid":802600,"isOrg":false,"description":"29×47×193×887","profileUrl":"/people/std4453","avatar":{"id":"0bda","template":"/{id}_{size}.png"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{},"columns":{"next":{}},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{}}

我要回帖

更多关于 unity3d编程旋转角度 的文章

 

随机推荐