【3ds max入门教程】Unity游戏模型制作之草丛的搭建——第三节: 3ds max模型设计从简到繁
第三节3ds max模型设计从简到繁上一节：[post cid="876"cover="http://"/]我们入门先设计一些简单的模型1.草丛的模型制作1.a. 草的制作做一个竖直的圆柱体，高5段，面1，边环6段即可调整尺寸至高30cm，半径2cm左右即可。R键调整形状scale，变成扁平。然后转为editable poly形状按自己喜好调整，这里先调整叶子中部两条竖直控制线靠近中部：v+f或者F键获得前视角镜头，然后选择顶点模式，框中中间两排所有顶点，Alt+X透视模式查看背面顶点是否选中，如果你开启了右侧菜单selection的Ignore backfacing背面忽略，建议取消勾选，或者通过旋转后按住ctrl选中背面的顶点都行，此刻注意Selection菜单提示24 Verticaes Selected辅助确认已选中的顶点个数。然后点击右侧菜单Edit Geometry项目里Constraints约束选项，选中Edge按边约束，然后R键就可以调整顶点往中间移动了，获得合适位置后，Constraints约束选中默认的None，继续调整顶点位置获得合适的叶面厚度Editable Poly选中模式(selection)切换为Edge，通过swift loop增加竖直居中环绕线圈控制叶脉，再选择叶面的两条控制线稍微凸出（注意正反两面都操作一次），获得叶片基本形状，或者调整两侧叶脉线凹进也可，自己灵活调整。Editable Poly选中模式(selection)切换到Polygon面模式，选中上下面删除。Editable Poly选中模式(selection)切换到Vertex(顶点)，选中顶部的顶点，通过R键调整到扁平贴合，点击上方菜单weld合并多余顶点，如果合并范围出现异常，点击weld右侧设置箭头，调整合适的weld范围即可。weld完毕后测试点是否成功合并，尤其新手，如果你的weld范围设置过小，可能部分顶点并未成功合并。（我们这里的草宽度设置的2cm，而weld阈值默认是10cm，一般来说是无需手动调整范围）草的模型调整满意后，切换vertex为选择Editable Poly,我们开始快速展UV map，选中右侧modifier list的UVW map编辑器，如果你之前按我推荐的增加了修改器列表快捷按钮，只需要点击一下即可。然后V-F选择正面视角方便快速匹配UV，mapping参数选中planar（平面），点击view Align,使得展uv的平面与当前front视角一致，然后再点fit部署拓展到整个叶面，collapse all，保存当前uvw map配置，就算完成展uv了点击M打开材质编辑器，我们为这根草快速添加材质，这里演示下用基本渐变色彩（gradient）示意材质分布：展开Blinn basic parameter（明暗器基本参数栏目）这些参数里面一般常用的就是ambient(环境光） , diffuse（漫反射）和specular（高光），其中diffuse漫反射通常用来选择贴图材质，物体表面漫反射效果是最主要的材质表现通道，我们一般经常在diffuse设置里选择基本色或者bitmap材质位图，给模型赋予基本的皮肤而其他的参数主要用来模拟真实环境一些物理现象，让三维物体表现更加自然用的。如果你的材质编辑器不是我这样的，可以选择Modes >compact material editor即可，任意右键一个材质格子即可选择材质格子分布尺寸3x2，5x3，6x4，或者点击编辑器右边的齿轮按钮，在设置里选择需要的排版大致介绍下，入门阶段大致了解下即可，因为材质编辑器是最常用也最容易熟悉的模块，不用刻意记忆接下来我们修改diffuse参数，选择漫反射材质为Gradient，用基本调色盘渐变色来赋予草材质贴图。如果你嫌颜色梯度不够用，可以选择Grandient Ramp范围，自由增加控制点Gradient Type和Interpolation选择linear线性过渡即可，如果你想要卡通效果，interpolation过渡效果选择Solid固定色即可，角度Coordinates选择VW方向，因为这里我们颜色是竖直方向过渡，具体方向按自己的模型需求更换。然后点击Assign material to selection ，Show shaded materials in view point ,应用，这样我们就能看到渲染后的效果了。我们的草算是基本完成了，接下来自行发挥调整尺寸和色彩，做一些基础草的变种，为我们的草丛构建提供一些基础素材。如果你想要添加材质，可以自己拍照或者网络找非商业素材，加工获得贴图导入，另外，我们也可以通过substance designer制作材质，为模型赋予更加真实细腻的材质效果，当然一般情况下，草丛不需要做太精细，毕竟数量多还是比较吃配置的。Substance designer 的设计以后专门再说，这里简单演示下通过photoshop制作贴图材质：导入合适的图片，网络的，自己拍照的，画出来的都行，商业项目注意版权问题这里导入一张我随手拍的照片，通过ps加工提取合适的叶子一般使用魔棒工具加上一点涂抹和仿制图章修复，差不多就行，草丛毕竟实际场景注意不到多清楚的细节的使用编辑>>操控变形，开始拉伸展平我们的草叶根据实际情况自行调整模式和密度，这里选中正常模式，较多的密度，然后就可以在叶子合适位置加点，移动控制点到合适的竖直位置，通过不断加点和位移，将弯曲的叶子贴图展平，获得较合适的效果就行，选中草叶，图像>>裁剪，保存此正常贴图为png格式或者tga格式然后魔棒工具shift在草叶外部点击选中所有空白部分，ctrl+shift+i反选获得草叶选区，点击通道选项，新增通道，ctrl+delete将草叶选区填充背景白色(x键快速切换前背景色，或者点击左侧菜单前背景色上的切换按钮)，或者shift+F5填充纯白色到选中的草叶选区，获得白草叶黑底图案，我们可以在选择菜单>>修改>>平滑，选择合适像素点，获得比较柔和的选区，然后选择>>修改>>收缩2-3个像素点，然后编辑>>描边8个像素，位置选择居外，不透明度90%，模式:变暗，这样可以修正叶子边缘留白的问题。最后ctrl+A全选此通道，ctrl+C复制，点击图层选项，ctrl+shift+n新建图层，ctrl+V粘贴通道图案到此图层，然后保存此黑白图案为alpha贴图打开3ds max，我们选中一根草，按m进入材质编辑器，在diffuse选项后的小方框点入选中bitmap，导入我们的正常叶子贴图。然后在opacity选项后方框点入，导入alpha贴图。回到此材质父目录，应用所有材质到选中的草模型，然后显示渲染后效果，就可以得到简略的透明贴图了。如果你想微调贴图大小和位置，进入材质管理器的bitmap对应cropping/placement选框，点击view Image,手动拖动裁剪框直到满意，返回勾选Apply即可生效，注意对应的alpha贴图也需要相应调整合适的位置，以免出现留白。另外，我们也可以直接调整草的uvmap和贴图形状相匹配，不过这个方法会出现比较明显的拉伸变形，自己酌情调整，可以看到，贴图和草的uv map大小是不一致的，我们可以简单调整一下uvw map大小，使贴图材质匹配uv，点击modifier list预设的unwrap uvw按钮>>edit uvs选项下的Open uv editor编辑器，框选所有uv map，右上角调整视图为贴图材质，灵活使用移动，旋转，缩放功能，调整uv map大小到草的材质贴图覆盖范围即可。如果你需要较为贴合的效果，可以点击freefom mode，选中需要的控制点，改变uv map形状，当然，如果贴图材质形状和uv map形状差异较大，会不可避免出现材质的拉伸，扭曲变形，在高标准的制作中，这样的方式并不可取，一般材质绘制我们都会拿到uvw map后手工绘制相匹配的材质，或者使用substance painter之类的3d 材质制作工具直接在模型上绘制，才能得到真实合理的贴图效果和较高的品质。好了，草的雏形算是基本完成，接下来我们开始制作草丛并渲染获得高质量低面数的草材模型，导入unity使用1.b. 草丛的组建（1）基础拐角柔化选择草叶，modifier list 点击edit poly按钮，我们对草叶进行柔化，选择polygon面模式，再次全框选所有的草叶，然后填入数字75，代表75°阈值以下角度任意相邻平面夹角纳入柔化区域，点击auto smooth。查看效果（2）FFD自由形变+使用我们继续在modifier list 点击FFD 4x4x4 自由变换模组，点开编辑模块，选择control points，这样我们就可以选择需要的控制点进行拉伸旋转缩放，个性化草叶形状了。调整合适后，collapse all我们对调整后的叶面，勾选使用use NURMS 曲面分割功能，就可以获得比较顺滑自然的弯曲效果了接下来我们对剩余草叶进行FFD自由形变和NURMS曲面柔化，加工每一颗草，根据需要我们可以搭建长中短绿色草叶各一根，枯黄草叶长短各一根，带贴图材质草叶长两根，一根直，一根柔软圆润，两根grandient ramp 一瘦一肥，总共11根。当然你也可以创建更多的类型，先制作标准模型再分化成高低两类，以及各种色彩风格搭配，可以自由发挥：然后列表全选，rename，基础名字Grass_Base_，Numbered命名，初始数字01，step步进1即可（3）灵活组合搭配草丛选取中等长度草叶4根做第一个标准高度草丛，灵活使用前上视角和旋转扭曲，获得比较美观的风格，复制一份基础草丛，对其进行微调旋转，组合成更多的草丛，最后保持草丛草叶总数量20根左右合适，获得一个完整的草丛主体，重命名为Grass_Base_Mid_01系列，保存项目副本，以便后期修改调整。保存完毕后框选此草丛所有物体，alt+q隐藏其他物体，通过规范命名组件物体名称和隐藏无关模型，再通过Attach >>attach from list就不容易错选漏选，命名合并的整体为Grass_MId_Green，alt+U恢复隐藏物体如有需要调整此草丛的pivot归零，选中此草丛状态下，点击右侧齿轮Utilities中的Reset Xform 两次（第一次是以local坐标系将xForm归位，第二次是将xForm以世界坐标系归位），然后Modify页面查看效果Xform恢复标准形状后，collapse all，再点击Hierarchy面板选择affect pivot only，调整轴心为坐标为0，即可再此选择中短草叶3根搭配1根短黄草叶，做一个低矮高度草丛，灵活调整高度，两侧较矮，总数量16根左右，搭配点缀草丛边缘用，完毕后记得Attach组合，命名Grass_Low_Green,调整pivot再选择1高1低绿色草叶搭配一长一短黄色草叶，做一个枯黄风格高草丛，调整黄色草叶长度，使得整体黄色中高草叶为主，1最高，2次高，2中等，4-5中矮，2最矮，搭配两根中长绿草点缀，1中绿草，和4根短绿草点缀，整体20根左右合适，用于单独枯黄草丛或者点缀在绿色草丛中，attach组合命名Grass_High_Yellow再选择中高绿叶4根，搭建中高绿色草丛，总数量16根左右，8高8中高，适用于点缀绿色草丛中央，与枯黄高草丛对应。attach组合命名Grass_High_Green推荐你自由发挥，打造自己心目中的效果，最后演示草丛如图：这里简单制作一丛水仙花，凑个整：拉三条圆柱体，调整形状，可以手动调整顶点vertex，或者modifier list选择bend弯曲皆可获得简单的花茎接下来制作水仙花瓣，类似叶子的制作，由于花瓣很薄，我们改成5x1x3规格的plane即可，调整vertex获得花瓣形状即可，可以尝试使用bend弯曲或者之前的FFD整体形变皆可，适当搭配NURMS曲线/turbo smooth和polygon:auto smooth，获得柔和的形状和轮廓,别忘记给花瓣做UVw map后rotate生成copy旋转多份，自行调节，花朵部分attach完毕后，调整合适角度与花茎摆放合并，最后attach成整体后，别忘记reset XForm 两次。我们再新建一个plane 1x1尺寸，x轴旋转90°，垂直放置于原点，shift拖拽所有草丛到plane前端合适位置，注意不得触碰到plane接下来做草丛的平面投影渲染到plane平面上，获得高清渲染贴图材质，这样最低仅需1个平面模型，就可以获得草丛的较为真实细腻效果了，当然我们会做一定的裁剪和弯曲，实际一张草丛贴图模型大概10个面以内，可以承载16-20根高清草丛，性价比还是非常不错的，如果你直接用高清模型，一个经过了ffd和NURMBS弯曲的草丛，起码有600多个三角面了，如果场景有数千草丛，代价是非常大的2.a. 草丛的高清贴图渲染接下来我们做渲染高清材质的一些预设和配置渲染这是一门技术和经验并用的领域，专业的渲染工具和参数配置，已经是一门专业的领域，这里我们仅使用3ds max默认的渲染插件，做简单的渲染配置，能满足自己的基本需求就好，不深入，因为实在需要时间的沉淀，是一门经验活Rendering>render to texture ,点击左侧列表添加plane进入渲染清单，Projection mapping投影选择列表4个草丛，模式投射即可，设置Projection option投影参数，选择filtering option，筛选过滤菜单打开global supersampler全局超级采样，选择默认的2.5 star模型即可，即每个采样点和周围4个点进行分析处理，对于草丛取色足够，关闭filtering option，回到projection option选择ray miss color为贴近整体绿叶颜色，并微调较浅色域即可，用来填充草丛材质外部空白区域，防止一些贴图周围出现醒目的边缘，这是一个简单的柔化处理以防贴图不匹配导致空白区域亮眼白色被显示渲染。关闭 projection option菜单，我们回到初始的renderring to texture菜单，选择output 选项，这里输出3个贴图：diffuse，alpha，normal，修改size为2k，配置一下输出文件夹和文件类型为tga最后Baked material选项勾选render to files only，仅输出渲染文件，keep source materails，保留初始材质。点击最下方render，查看render效果如果颜色偏暗或者偏亮，可从rendering菜单，environment环境配置修改ambient环境亮度为合适的亮度，达到满意为止。这样，我们的草丛高清材质渲染就完成了2.b.最终草丛模型加工我们新建空白plane，1x1规格，大小随意，这里用的100cmx100cm，然后旋转为竖直，右键菜单选择object properties，勾选backface culling，使得背部渲染透明，获得正反方向，渲染模式换成standard ，edged faces，M键打开材质编辑器，添加我们的diffuse ，alpha贴图到此平板，alpha贴图记得选择Mono Channel output为alpha，作为opacity透明通道贴图，应用后即可获得高清贴图效果，接下来选择Modify面板》edge模式，swift loop添加新的边，隔断每块草丛贴图，完毕后选择polygon模式将每块隔断的草丛选中，shift移动新添copy副本，这样就可以开始制作我们的低模高清材质草丛了，每块草丛低模，我们勾选Edit Geometry的Preserve UVs，保持所有草丛UV来源于原plane，这样我们微调每块草丛模型时，就可以实时获得原贴图区域，而不会出现扭曲拉伸了。接下来按需增减线条，选中edge后ctrl+Backspace删除此线段和它两端交点，Backspace则仅删除线条而保留交点。Swift loop添加水平方向线条，按草丛高矮配置3-4条不等，方便之后我们设置每条线上顶点alpha权重，控制草丛随风飘荡的幅度，alpha权重位于vertex模式的vertex properties中，0表示最低权重，受风力影响为零，不会摇摆，100则完全展现风力效果，从草根到顶部建议配置为0-25-50-75-100所有草丛中央增加竖直edge，方便我们制作十字形草丛，以及缩小transparency透明部分面积，降低系统运算占用，通过调整四周顶点位置，尽量让模型贴近贴图，减少不必要的开支，部分草丛可以考虑增加顶点或者竖直边（edge），以适应草丛规模，或者减少透明部分面积，注意顶点的增加也会增加系统运算负担，请慎重取舍，获得平衡点。所有草丛制作完毕，我们新建一个1unit方块，用于参考草丛高度，缩放调整所有草丛高度合适后，Untilities菜单，选择reset xform后collapse all通过s和a键的snap吸附开启，将所有草丛贴紧地面全部摆放规整来到hierarchy面板，affect pivot only，归位轴心为 center to object，取消affect pivot only后，右键select and move 移动图标，将x和y轴位置归零，使草丛模型回到原点，再次点击affect pivot only后将z值归零，此刻轴心pivot应该位于原点，即草丛模型正下方中间位置，再右键select and rotate按钮，x轴输入90度，保证unity模型预览方向正确。将此草丛命名Grass_Clump_HG(high/middle/low,green/yellow),然后隐藏，依次处理加工所有草丛后，保存save+，作为阶段性还原点我们将草丛逐个显示，shift拖拽copy备份，通过snap angle角度对齐，旋转copy 90度后，合并成十字形草丛，再attach，检查vertex的alpha值和pivot位置，如果你要增加三角形草丛和一定倾斜角度的交叉十字，请再次检查pivot方向，如果发生变化，请再次调整hierarchy面板的pivot位置使之居于原点。这里注意三角形草丛搭建，建议旋转copy 60度后，我们选择local视角后，z轴方向移动2-3cm即可，attach合并后如果pivot位置发生变化，输入-2-3cm归位原点即可。如果是倾斜角度交叉十字模型，pivot 方向发生倾斜，依然是hierarchy面板选择affect pivot only后，再次选择align toworld 即可对准归位世界坐标系，再次调整后回到原点。保存每个选中的草丛最终模型，菜单File>>save selected…保存为max格式即可，将plane高清平板保存为Plane001DiffuseMap.tga，tga格式，作为unity中albedo map使用，所有草丛都是公用此plane的贴图uv，节省资源3.a.将草丛导入unity首选新建meshes>vegetation 植被文件夹于assets目录下，拷贝所有草丛max文件于vegetation文件夹下，内部新建Materials和Textures文件夹,将Plane001DiffuseMap.tga文件粘贴到Textures文件夹下，在unity里新建材质球命名GrassAlbedoMap于Materials文件夹下，选择贴图为Textures文件夹下的tga map贴图，shader选择nature leaves，自行调整alphaoff，全局动态光照等参数合适后，将所有草丛模型贴图元素选择此materials材质球后apply，再放置到场景里观察调整效果，满意后，在Assets文件夹下新建Prefabs文件夹，拖拽加工满意后的草丛模型到prefabs文件夹下，统一管理这些预制体。这样我们的草丛算是顺利导入unity了，接下来就是地形的绘制和草丛detail刷刷刷了。
【3ds max入门教程】Unity游戏模型制作之草丛的搭建——第二节: Photoshop平面地图布局设计
第二节 Photoshop地图设计流程上一节：[post cid="868" cover="http://"/]根据我们设计的原画布局(个人练手仅供参考，节约时间，草图一枚)，绘制对应的平面地图场景创建一个4096x4096的项目Shift+Ctrl+N新建一个4096x4096的地图map图层1.a. 参考方块图案制作在地图图层上方直接ctrl+shfit+N新建一个grey mask遮罩图层再文件菜单>>新建一个256x256画布，利用铅笔工具，选择画笔>>旧版画笔>>方头画笔>>任意硬边方形画笔，设置像素大小为128，ctrl+R调出刻度尺，拖拽横竖两条参考线均分画布四等分，shift用1像素硬笔圆画上蓝色垂线，左侧菜单油漆桶工具G将右上和左下选择深灰色50%涂抹，右下和左上选择较浅灰色25%涂抹，，编辑菜单>定义图案，命名Grid回到地图画布，选择grey mask遮罩图层，shift+F5填充，选择图案，找到刚才新建的图案，确定。如果不喜欢这种明暗相间的方块图案，我们可以直接做128x128、64x64规格的纯格子图案画布，仅用1像素铅笔工具填充左下角两边深黑色，中间填一个浅灰单色即可1.b. 再次新建图层Land Sketch绘制陆地草图，同时新建Sea图层，等待绘制海洋区域Land Sketch主要将城堡和河流以及城墙，森林的位置摆放调整大致位置,简图只是用来整体分布定位，免得出现模型尺寸严重不协调的问题，具体布局后期会按需随时调整，不需要多精准的位置分布。1.c. 然后制作Icons Mask 深灰遮罩和Icons图层（所有icons放置在Icons Mask遮罩范围内，以区分图例和地图范围），绘制填入相关的图例。1.d. 简单比例尺的制作使用矩形选框工具（M),样式：固定大小，设置像素128x6，然后新建Scale图层到Land Sketch图层上方，合适位置放置此选区，然后编辑菜单>描边>居中描边1个像素，鼠标拖动选区平移到右侧贴紧此描边，再次编辑菜单>填充黑色，以此类推，做出4段比例尺效果即可，然后上方T键输入刻度16，32，48，64，单位为米（4096/128=32格子，我们稍后unity内地形尺寸设置为512unit，即512/32=16米/unit，所以每段比例尺代表距离是16米）Ctrl+G新建Labels组管理所有图标文字，Scale Unit组管理比例尺Scale图层和相关文字刻度基本地形位置绘制完毕，输出地图草图然后Unity内新建cube，scale修改为512，0.1，512，命名Base Terrian，position改成256，0，256，让terrian的左下角对齐原点然后新建cube，scale改为1，2，1，位置归零后改为0，1.05，0，命名Character，选中角色状态下，f键聚焦，平移到合适镜头位置，选中Main Camera物体后，在GameObject菜单，Align with view对齐当前game视角为镜头视角选择game视图，比例16：9，调整合适镜头位置后观察实际游戏效果布局感觉，再对Land Sketch地图布局新建大致尺寸的城堡方块，炮塔方块，河流模型，进行位置放置调整，检查布局和比例合适后，挂载一个fpscontroller丢到player内，设置position归零，测试看看效果就可以开始正式进入建模阶段，开始按着清单搭建模型了下一节：[post cid="880" cover="http://"/]
【3ds max入门教程】Unity游戏模型制作之草丛的搭建——第一节:3ds max一些初始推荐设置
草丛，碎石，天空几乎是游戏场景中必有的元素，作为入门初级阶段的学习目标，从草丛的制作入手，是个理想的选择，一套不错的草丛贴图，可以显著改善游戏场景表现，而草丛的优化处理也是提高游戏帧数的必修课程，那么，闲话少说，我们直接开始吧！第一节 3ds max一些初始推荐设置每个人都有自己偏好的3ds max个性设置，那么作为入门第一阶段，我在此分享下个人比较推荐的常用设置，尤其是游戏制作通用的一些设置，可以方便你的制作和后期修正一、网格和对齐设置：Tools>Grids and Snaps>Grid and Snap Settings ,Snaps选择中一般我们勾选Grid Points（网格点） 和Vertex（顶点），在执行模型对齐操作方面会比较方便准确Options选择Enable Axis Constraints(启动轴向约束),对于Display rubber band(显示移动路径)，按个人喜好选择是否开启Home Grid选项中将Grid Spacing（网格步进/间距）改成100.0cm，即一个标准米间距，作为人物和普通地形常见参考单位，对于我们游戏场景制作比较合适二、单位度量设置打开Customize>Units setup,显示单位设置度量单位为Centimeters即1个厘米，然后System Unit Setup选择1meter作为1个单位，这样3ds max默认一个单位长度就是1米了，这样方便我们模型在unity里面的显示。三、缩放可视范围调整默认缩放观察范围是有一定限制的，我们调整为最大，将我们Perspective 视图Alt+W或者点击右下角放大到最大，然后选择Perspective视角菜单，下拉Viewpoint Clipping，这样右侧就会出现一个竖直的范围区间，将最小和最大约束小三角标调整到上下极限范围即可四、常用视角快捷键Alt+X 透视显示F3，F4：网格，面框渲染和默认渲染视图切换Ctrl+R或者Alt+鼠标中键，旋转轨道视角查看，我们可以按U或者P键切换正交Orthographic或者透视Perspective模式。鼠标中键按住拖拽即可进入平面视角Pan查看Customize>Hotkey Editor修改快捷键，个人喜欢将重做设置为和ps一致的Ctrl+Alt+ZAlt+滚轮：慢速缩放Ctrl+滚轮：快速缩放ctrl+Alt+按住鼠标中键平移鼠标：无限制缩放z建快速聚集已选择物体Alt+q快速隐藏所有未选中物体，聚焦当前模型2020版本更改清单参考：3ds max 2020键位更新五、自定义修改器列表快捷按钮点击屏幕右侧Modify修改菜单，看到修改器列表Modifier List右键，设置Modifiers sets 配置：我们在弹出的配置窗口为自定义配置命名，比如my set然后Total Buttons按钮增加到12个，按键盘对应字母可以快速浏览左侧清单首字母修改器我个人常用修改器list供参考：Mesh Select UVW Map Unwrap UVW TurboSmooth FFD 2x2x2 FFD 3x3x3FFD 4x4x4 FFD(box) Chamfer（切角，倒角） Spherify（球体化） Tessellate（细化） EditPoly然后Save，再点OK退出。如果按钮列表未显示，右键Modifiers list选择显示按钮Show Buttons即可类似的，我们点击最右边这个工具扳手按钮，也可以设置自定义的快捷按钮，比如增加一个UVW Remove按钮，方便今后清除不需要的材质效果如果你对3ds max不太熟悉，可以去customize>>preferences首选项，修改General常规选项的scence undo次数默认20为50或者更多，不过注意内存占用也会随之增加。六. 创建参考方块新建一个box，选择Modify修改长宽高为100cm，获得一个标准立方，w选择移动模式，直接在下方菜单修改位置起点为原点（0，0，0)修改物体名称为Scale_Box,颜色自己选择喜欢的，然后File菜单保存为Scale_Box_One_Uint即可。我们回到Unity，导入看看效果七、Unity基本设置我们从Unity官方下载Unity hub管理包，安装需要的版本，学习用可以用最新的，稳定生产建议用LTS（Long Term Support）稳定版本，打开Unity我们在Asset文件夹下新建Meshes文件夹，存放我们的基础原型模型，从3ds max的max文件拖拽进即可，然后展开我们的模型图标，选择No Name材质文件，右键Extract from prefab释放到Assets>Materials文件夹下或者你喜欢的目录，这样我们就可以修改预制体模型的材质了，这里随意将no Name 材质颜色修改下，然后通过unity新建一个Cube，我们对比下大小确认一致，说明单位是正确匹配的。由于目前版本3ds max和Unity坐标轴系统差异，我们点击两个方块模型可以发现，坐标轴方向蓝色Z轴和绿色Y轴会出现相对x轴r旋转了rotate 90°的情况，所以保存你的3ds max文件前请调整对应坐标轴方向在3ds max里，我们新建一个2米高的模型，根据刚才的分析，需要以X轴为轴心正向旋转90°即可, Hierarchy面板，选择只影响pivot，右键菜单选择旋转选项，旋转指定X轴向90°即可, 或者你也可以在顶部菜单找到select and rotate 选择旋转这个按钮上右键鼠标，也可以调出旋转参数，x轴输入90回车即可实现切回移动模式，将unit two归零，接下来我们保存selcet as，导入Uinty，方向就是正确的了如果觉得unity光线太亮，去菜单Edit>Project settings >Player>Other settings 将色彩空间由伽马改成线性Linear，就会柔和很多。下一节：[post cid="876" cover="http://"/]
Let's build an indie game!《吾王大战全明星》独立游戏设计制作日记 Chapter 1 - 初章: 项目回顾和重启
[post cid="853" cover="http://"/]初章 回顾和重启《吾王大战全明星》独立游戏设计制作日记Chapter 1 at 24th Apri , 2021.琐事耽搁，项目重启进度如意料般延后，抽空抓紧时间布局第一阶段的项目目标第一个场景，主菜单界面敲定需要制作的基本模型和场景清单将核心模型和可重复利用的模型作为优先制作目标，整理列表如下：参看原画设计的第一关序幕设计内容，我们将需要制作的模型列出清单如下（按类别和复杂程度排列）类别一般包括：天空盒，地形，地表植被，环境装饰物品，道具，互动道具，焦点物品，粒子特效清单有助于时间安排和对场景的理解，查漏补缺，正规制作流程必备环节。[collapse status="false" title="模型清单列表"]编号模型名称所属类别备注白盒阶段高模阶段贴图阶段1天空盒天空盒unity内制作，昏黄战场风格 2主地形地形unity内制作，平原，海岸线 3悬崖地形预制体，按距离远近布局多个 4海洋地形 5河流地形 6树木1环境装饰 7树木2环境装饰 8树木3环境装饰 9横木环境装饰 10枯树枝环境装饰 11草类1（短）地表植被 12草类2（中）地表植被 13草类3（长）地表植被 14灌木1环境装饰 15灌木2环境装饰 16灌木3环境装饰 17花丛1地表植被 18花丛2地表植被 19花丛3地表植被 20小碎石环境装饰 21石块环境装饰 22大块石头环境装饰 23高坚果城墙道具（动画） 24bilibili地雷道具（动画） 25土豆地雷道具（动画） 26达柯鸭炮台道具（动画） 27二战飞机焦点道具（动画） 28飞鸟轰炸机道具（动画） 29武装坦克焦点道具（动画） 30桥梁道具（动画） 31天线宝宝1道具（动画） 32天线宝宝2道具（动画） 33天线宝宝3道具（动画） 34草泥马道具（动画）参考照片 35士兵道具（动画）暂未定下原型 36城门焦点道具 37建筑1（方形）焦点道具 38建筑2（尖塔）焦点道具 39建筑3（方形高塔）焦点道具 40主城（宝石核心）焦点道具 41太阳环境装饰血红色 42云层粒子效果黑色调，搭配红色晕染 43枪口火焰1粒子效果飞机类 44枪口火焰2粒子效果炮台类 45枪口火焰3粒子效果坦克类 46火堆粒子效果战火场景烘托 47烟尘粒子效果战场气氛烘托 48河水shader增加场景动感 49海洋shader映衬天空的战火 [/collapse]清单整理完毕，最重要的思考就是如何将以上物件高效优美地制作出来从理性和科学角度出发，之前的顺序是比较合理的：草丛>>花朵>>灌木>>树枝>>树丛为一个系列，小碎石>>石块>>大块石头也是不错的顺序建筑、动物、水域和各种天空盒，特效为其他类型，除了水域和天空盒在技术和重复利用方面有一定价值，建筑和动物等互动道具优先度不高，排为最低顺序基本安排完毕，接下来就是思考可能出现的难点和进度拖延点:①从树枝到树木的建模，这个过程是比较耗时的，需要注意；②草泥马，天线宝宝这种带头部的生物建模，技术要求比较高，在模型制作中，可能会占用大量时间，以及产生挫败感；③飞机坦克等hard surface机械建模，可能需要大量的icon和贴图，才能加速制作过程，对于精度控制，需要一定的经验，容易陷入追求精致的无底洞；④天空盒具体细分需要精力了解细节，敲定合适的精度和技术层次需要一定的学习时间和技术要求；⑤枪口火焰和烟尘视觉效果制作，需要进一步学习完善，可能会占用大量时间；⑥场景牵扯到一些基本的动作制作和动画曲线，需要大量的温习回顾时间熟悉上手。精简难点为：枯燥的建模过程，生物/脸部建模难度挑战，硬面机械建模技术，基本动作设计重拾信心建立节点：①草丛花木基本环境搭建完成；②碎石石块布局完善；③基本建筑外形加入；④土豆地雷，可达鸭等简单互动道具建模完毕；⑤枪口特效简单实现并部署⑥水面简单shader实现并部署时间花费计划：草丛建模制作约2-3天，开始阶段重温3ds max需要额外时间，给自己一个缓冲；花朵建模需要约1-2天；灌木建模制作约1-2天，做成树木，需要额外2-3天时间；碎石和石块制作约1-2天；①草丛花木基本环境搭建完成 此节点完成需要6-10天；②碎石石块布局完善 此节点完成需要1-2天；结合以往经验，后续时间安排心中有大致了解即可，先完成本月内可预见目标吧,后续进度更新中...
Let's build an indie game!《吾王大战全明星》独立游戏设计制作日记 Chapter 0 - 序章
序章 梦想的起点《吾王大战全明星》独立游戏设计制作日记Start from 3rd Apri , 2021.做出属于自己的独立游戏，一直以来都是很多玩家的梦想吧！将梦想变成现实，是很多人追寻的目标，甚至是人生的意义所在。很多人倒在了半路，很多人转型。这是个美好又显得幼稚的话题，不过想做的话，一步步先走下去，做出个再丑再有bug的东西，也算是梦实现的第一步吧。Part 1.取舍有限的时间和对品质的追求，高级技能学习和自身能力水准，都是相互制约，充满矛盾的，平衡与取舍是非常重要的一步，这是我在过去三年摸爬滚打中得出的最重要的经验，这种矛盾在三A大厂甚至整个业界都是很难处理的一个痛点。如何确定价值点和降低品质?谈到游戏制作，相信很多人都擅长列举出价值点：浅显的来说，有人在意好玩（游戏性），有人在意代入感（设计、渲染等），有人在意画面（次世代高精度贴图，高模设计），有人在意策略性（数值策划，关卡设计），从入门萌新到业界精英，讨论游戏的价值点亮点，都可以娓娓道来，那么为什么这么多人最终失败？当然，市场营销和资金成本是很重要的一环，那么分析许多拥有一个良好的运营团队和较充裕的资金支持环境的制作公司，依然会有很多的游戏口碑收益双双暴雷，那么，决定一个游戏好坏/成功的本质，并不见得是闪光点不够，不好玩。这是个空泛和缺乏说服力的论点，仅仅是一些个人心得感悟记录，从我个人喜欢玩的游戏来分析，我对游戏制作成功有一些自己的看法：游戏玩法：新手最大的失策，就是认为随着时代的发展，新制作的游戏，肯定比旧的优秀，新时代的人类，探讨研究出来的玩法，肯定比老游戏完善，我认为，这是非常错误的。流传下来的玩法，才是最值得保留融入的游戏玩法核心。这不是守旧，也不是大型团队不敢创新的借口，哪怕现代好玩的开放游戏代表如《我的世界》《泰拉瑞亚》，看似创新，实际内核是人类玩泥巴的天性。又如策略、对抗类游戏经典，其实象棋围棋军旗这种实体游戏老古董玩法机制才是老祖。一些新近冒出的独立小游戏的成功和爆火，实际上玩法类似甚至同样玩法的游戏，早在黑白机时代各种暴雷游戏或者沉寂在历史洪流中无数小游戏中早已开发出来过。甚至同时代同类产品有很多，但是却“莫名其妙”失败。游戏面对的是人，而不是数据/机器这是我一直的坚持，诚然市场导向，金钱至上的环境，会让各种数据分析，人类心理研究肆无忌惮占据市场，玩弄玩家，让玩家不得不边骂边掏钱。是的，它们是成功的，胜者为王。但是任何市场需要多样化，无论人类社会组成，还是大自然的生物环境，这也是我坚持做一个独立小游戏的信心所在。游戏，一直伴随着人类文明的成长，人类对游戏的喜爱和探索，决定游戏行业是一个有价值，可贵的研究领域。尤其是男人对于游戏的追逐不亚于女人对于包包的渴望，哪怕再多再好的三A游戏诞生，依然满足不了玩家对于心目中游戏的渴望，男人（当然女人也一样）总觉得自己最想要的游戏是下一款。欲望和需求是市场的根本动力，所以，制作游戏是一门古老而持久的行业，不用担心落伍和衰败。而在各种大厂夹缝中生存的独立游戏开发来说，不用太在意盈利指标和用人成本（用爱发电甚至不需要多少工资），玩家对产品的可接纳宽容度，都是优势。其实无论是玩游戏还是开发游戏，本身都应该起源人类本性对于美好的事物的追寻，无论是开发者，还是玩家，本身其实是一类人。看起来很矛盾很奇怪的观点，是吧？这方面的成功典范，就是很多人熟悉的valve公司，也是我挺喜欢的一家游戏制作公司因为它做到了：我做了一些好玩的东西，邀请大家一起来玩。是的，游戏的玩法核心，本该如此简单朴实无华。此类游戏代表，很多就是开放类生存种田游戏，然后就是v社为代表的--游戏做好了，玩家来做mod。大师或者核心玩家用比较专业的技术构建出比较好玩的游戏规则，大家玩家一起来玩，听取玩家意见，改善规则，让强有力的规则和模式支撑玩家实现更多的可能，这就能成功。当然，这类游戏，对于新入行的开发者来说，挺难的，不光是技术，主要是经验眼光积累和时间消耗非常大，所以可以作为今后的理想，但是对于新人，我建议舍弃直接开发此类游戏，因为好酒需要时间沉淀，游戏亦然，新人开发自由世界，很容易只剩下空洞和单调；所以，独立小游戏，真的需要做小一点，价值点少一点，多舍弃一点。是的，为什么那么多人做游戏，无论独立游戏还是开发公司，很多都失败，是他们想要实现的闪光点太多了并不是闪光点多不好，而是小型团队很难支撑面面俱到，而更多情况下是，一款游戏只可能适合一类人群，普通团队很难作出来人人都喜欢，老少通吃类似自由世界探索类型的游戏。钻石不需要绚丽的色彩，也不需要玫瑰的芬芳。所以，在你想做一个自己的游戏之前，夸夸其谈，洋洋洒洒策划部署了天花乱坠的可玩性亮点吊炸天的设计和剧本之后，我真心地建议你，一步步砍掉所有的价值点闪光点，最后只留下一个核心，这个东西就是支撑你这个小游戏，让你触摸成功的可能的基底。出个点子，出很多点子，很多人都会，砍掉不是那么有价值的点子，舍弃那些虚浮不擅长模块，将核心品质打磨完善，降低/舍弃不需要的内容，反而是最难的。所以，我用自己的游戏构思策划思路流程，给大家展现一次思维的斗争，取舍的平衡，希望我的心路历程，对你有所帮助：Part 2.构思2018年某一天，我想做个小游戏我的能力入门级，所以，3d大作，剧情复杂，场景精美，全部与我无关，看来我还是很有自知之明（结果证明我还是太太太高看自己了）岛屿生存类游戏看起来是个不错的选择，地图大小可控，世界架空，剧情不需要多么精彩，普通生存探索，做的好玩的话，应该会有一些不错的反响-----这是我当初的认知哗啦啦，画了个岛屿草图。布局了些自认为有特色的地形环境，沙漠，隔壁，瀑布，针叶林，热带丛林之类的，甚至包括一个隐藏的神秘城堡，嗯，很不错的进展，很好。然后开始构思剧情玩法，从风暴中生存下来，落难无名小岛，从海滩打捞零碎工具食物补给，开始制作简单生存工具，一步步探索小岛，简约而朴实，很符合我的胃口接下来利用我的一些地形制作基础：https://www.bilibili.com/video/BV1Qt41167Wf/尝试制作一个简单的岛屿，嗯，还行，虽然经过一番周折，舍弃了自动生成无限地图，用unity自带地形刷刷出来一个岛屿也能符合我的简单需求，就凑合用着了，一切秉着解决问题就行的思维，尽量简化节省时间。接下来发现我需要一个还不错的地图设计，就琢磨着学习了手绘数字地图的方法：https://www.bilibili.com/video/BV1nE411Y7Us/弄一个奇幻地图设计，还算满足了我的需求；接下来就是模型设计，简单动作设计，基本的一些ps入门，3ds max建模，重温了下以前的建模基础[post cid="291" cover="http://"/]又拓展学习了substance painter一些使用环境搭建方法[post cid="429" cover="http://"/]之后开始制作最简单而又最普遍的模型，草丛，树木结果你猜怎么着，当我把3ds max渲染效果不错的草丛放入unity里面后，发现就是些朴实无华单薄的纸片后，我突然对自己构建的环境很失望是的，第一时间我觉得是我目前unity技术不行，然后继续学习unity pre-render预渲染一些技术，如何调试渲染更合适的画面。。。之后又尝试对比了下unreal引擎下默认环境细节表现，又有些心动。举棋不定，见异思迁之下，在设计小人儿海滩行走动作流畅度提升的同时，又沉迷海滩高品质pbr材质substance painter设计学习后，不知不觉各种问题累积和家庭琐事拖延，我发现这游戏不怎么有意思而且很花时间，这个本着务实精神的练手项目，就这样无声无息消散了。是的，作为一个比较理性的人，我降低了期望,“合理”评估自身能力后，制作刚迈开步子，还是出乎意料扯到蛋了。人不多迈几步，就永远不会知道自己的步子是多么的浮夸.是的，新手常犯的错误就是，没有认真的规划，小功能实现以为很好解决，以为时间很充裕，以为自己很勤快哈哈哈，现实就是这么真实。承认自己是懒人，自己不会比成功的人更优秀，再次降低自己的眼光，做好计划安排，是我当时吸取的经验所以，第二个构思策划项目在我深思熟虑下逐渐清晰：《吾王大战全明星》游戏开发构思依然是一个以爱好作为起点的策划，这次学乖了点，我写了个上万字的剧情策划，附上草图关卡布局，设计亮点，以及剧情结构图：[collapse status="false" title="剧情结构图"][/collapse]相应的还做了日程安排，虽然知道不现实，还是想挑战下自己为了防止创意被淡忘，设定发生偏移，我紧锣密鼓把各种大纲设定推敲一二，同时做了整体关卡设计概念草图，然后沉迷Photoshop绘画操作，研究一番人体画法，又赶上吃鸡大火，天天被网友拉着四排，随着琐事耽搁（吃鸡游戏时间莫名其妙破千），本来的计划又付诸东流。没错，其实这次的规划，本来目标是先把大纲设定好，能在一年内做个一两章出来，就心满意足了，虽然构思了很多内容，不是一个人能做完的量，但是预想中最低标准都未能完成。Part 3.经验总结（更新中）结合两次项目失策的事实，我目前分析了下失败的原因：1.个人制作独立游戏本身确实是很艰苦的，但是一个项目本身确定没什么问题的情况下，开头没能坚持下去，最关键的原因，是自己没能在最短时间内，构建出一个基础的核心玩法/场景，也就是核心机制mechanics，玩法中的原型prototype，场景的白盒white box阶段，你最在意最自豪的游戏核心实现，用最简单的几何体和核心玩法代码去实现它，这样你才能在最短时间感受到创造的价值体现，获得成就感，从而不断完善它，进入状态后，才可能进行枯燥的马拉松；2.任何新模块或者具体制作过程遇到的效果实现，如果不太熟练，一定要先做一个最简单的，能展现出来的效果，再去精雕细琢，游戏制作需要耗费的心力，是无底洞，游戏设计制作和绘画一样，线稿草图Rough shape占80%比重，refined细节展现雕琢只占20%，而两者耗费的时间，却是相反；3.如何克服懈怠和迷茫，这个问题真的很苦涩，其实所有未能成功的人，在人生旅途中，甚至一辈子，都可能伴随着迷茫和懒惰终老，这聊着又要涉及到人生意义的哲学问题上了，或许很多人都知道简单的目标细分，学会将马拉松分段，定好了一个个小目标了，但是如何带着动力去实现它，如何让每个目标的实现过程让自己感受到不同的体验，这才是核心问题，具体实现，我会在后续慢慢补充；4.不要什么时候，都想着把游戏做成心目中的样子，而是要思考为什么一定要做这个游戏，它与市面上的游戏为什么不一样，它的独特价值所在是什么，这点真的很重要。每一步都是要严格去实现游戏的核心，而不是关卡最终效果。游戏的最终效果提升，实际上技术要求反而最低，但是最耗费时间精力打磨雕琢的。对于新手，打磨视觉效果可能会占据你开发的85%以上时间，但是对你本身提升没什么卵用，至少在入行阶段来说，提升很慢，成就感很低。我不否认很多人最后想成为游戏原画设计或者模型设计师，包括我自己也很喜欢这块，但是，在成功之前，先吃上饭，别在街头学艺吧，当然你要一开始就做到这么极致和挑战刺激的人生，我也不反对；5.不反对劳逸结合，但是制作游戏的话，还是别经常玩游戏了，除非考察分析需要。时间确实是挤出来的，想偷懒的时候，单纯的休息散步即可，否则人的天性会通过心理暗示让游戏制作觉得越来越困难，同时，经常性体验优秀的游戏，同时对比自己的半成品，也会不断积累挫折感。关于工作时间，正常人很难有连续性2个小时完美的安静时间去享用的，经常性的，你会被迫中断休息。独立游戏开发，除非自己搬出去拼命，否则还是学会如何快速中断自己思路，如何快速恢复工作状态，这点，是必修技能，没这个本事，千万别尝试。当然独享别墅父母双忙的天之骄子除外；6.计划很重要，回顾更重要。完善的计划，科学合理的时间安排，可以让每一步都明晰，但是每个阶段的回顾总结，分析调整更重要，如果一味的执行计划，而没有分析已完成部分的不足和效率，进行反思回顾，那么人很容易疲惫，项目也容易超时或者拖延，没有日志记录的话，也不利于中后期快速定位每个阶段做过什么，对于项目维护，会增加工作量。当然还有很多的问题需要完善，不过正如我所说，先做，再说，哪怕这篇序章目前多么不完善，言语逻辑多么混乱，但是它已经有自身的价值，就把它写出来，后续工作中有所获就不断完善，而不是一定要打磨完美，以上，共勉。[post cid="867" cover="http://"/]
第一节 Substance painter基础应用第一部分，一些简单的桌椅材质添加，substance painter最常用的layer（层）和mask（遮罩）以及generator（生成器）搭配brushes（笔刷）和alpha（透明通道）还有material（材质，智能材质等）组合操作得到以下效果：第二节 自定义材质的获取和加工第一部分，通过日常照片或者不规则尺寸贴图作为参考，在ps里制作修正为2048x2048标准2k尺寸贴图 转换为 转换为 转换为 通过位移+1024像素点进行tile map边界衔接处理，为了兼容性和质量建议保存为tga文件格式第二部分，在bitmap2material软件中，将上述标准尺寸位图转换为material材质图，我们需要输出四张图，一个base color基准颜色贴图，一个根据基础颜色贴图生成的normal map法线贴图，一个roughness map粗糙度贴图，还有一个ambient occlusion环境光遮蔽图调整流程基本逻辑为：output 微调选项中，先关闭所有项目，只选择base color，调整defuse|light微选项，light cancellation等降低光线效果，同时调整各种档位阴影，总之目的还原纯粹颜色贴图，排除掉环境阴影光线影响，得到纯粹的颜色材质。base color设置完毕，再到output微调选项中开启normal法线，如果是深色砖头，系统会自动将暗处处理为凹进，所以如果效果和自己感受的不太一样，也就是深色区域过多，而实际你需要这些区域显示凸起法线效果的话，去Global微选项下，选择invert relief即反转表面浮雕纹理方向，达到较为合适的凸起效果output 微选项下开启roughness，然后对粗糙度微选项进行设置output 微选项下开启ambient occlusion，对环境光遮蔽进行设置，如果阴影太黑，在AO平衡中，将对应Low Mid High频率进行负值降低效果，达到满意为止，最后可以在2d视图依次查看四种贴图效果，查看无误后，选择全局2048x2048分辨率后输出改命名为Room_Wall_%U这种格式，将自动输出对应用途的四种贴图命名，方便管理之后依次修正ceiling和floor，得到三套，总共12张成品材质贴图第三部分 导入材质贴图到substance painter进行加工，添加display渲染项目ambinet occlusion,对三个面进行渲染，将三套材质放到一个方块上进行调整方便获得整体协调色彩和质感导入对应贴图到渲染后的项目，获得正确的初始状态如图：第三节 substance painter内材质的加工和完善使用立方体进行多个材质加工，为了直观和整体的色调统一墙壁增加一些喷漆条纹，增加fill layer添加black mask，在mask上增加fill layer，绘制dirt效果，多个dirt效果可以在一个层，改对应内部fill layer效果为exclusion模式，防止覆盖最下方fill layer 效果，达到叠加多样化效果的目的，在地板的加工中，可以多设置几个污渍叠加fill layer，对单个mask做到多个fill 效果叠加还可以使用智能fill layer，对条纹进行dirt风化效果实现地板的话，主要使用多个fill layer对 black mask进行蒙版加工，注意位于结构栏上方的fill layer选择渲染模式为exclusion，layer都是从最底部的绘制，逐个往上运算。外部主fill layer颜色可以微调为暗黄，作为污渍主体色调，可以在mask内部处理完毕后随时调整最终效果，依据素材色调酌情考虑新建外部主fill layer进行暗色mask处理天花板这里过于亮白，加入暗色fill layer和一层fill layer+black mask，针对mask增添fill layer进行暗色污渍处理，与之前的主fill layer 暗色mask叠加达到理想效果最后输出，记得分辨率2048x2048，文件格式tga，unity5 metallic默认设置即可第四节 材质导入unity做好屋子模型，增添对应三个材质球，将导入的三套总共9张材质贴图对应赋予到材质球上，屋子的基本效果就出来了，细节问题，之后再更新第五节 opacity透明材质加工和projection投射decal贴纸实现以透明塑料袋为例我们开始渲染贴图时，为了获得opacity贴图，在添加ambient occlusion环境光遮蔽贴图项之后，还需要新增加opacity贴图，小材质导入的时候选择1024分辨率即可，显示选项可以显示网格，作为小材质，渲染时可以选择将低精度模型作为高精度模型处理渲染完毕，开始加工：搜索share网站，找到一个白纸智能材质，提取内部fold褶皱效果，导入贴花png图片为texture，然后新建paint layer进行projection投射，投射前调整projection选项为xy水平竖直方向拉升，然后将咱们的png贴图载入为投射base color，s键进行操作，此paint 层命名为decals层投射完毕，新建浅蓝色paint layer进行mask绘制opacity透明层，记得调整opacity值为0，开始涂抹，边缘也别漏掉，标签附件可采用shift加鼠标左键拉直快捷处理，然后选择soft brushes，调整flow为20，进行细节涂抹，最后对label贴图附近使用更小size 的brush加8左右的flow值进行最终细节处理，命名为color层，方便后期调整透明浅蓝透明度和颜色我们回到decals层加mask，对mask设置generator，添加两个alpha效果，brush smudge和brush conner large以及stain napkin都是比较合适的，贴图遮罩global invert反置开启，调整global balance为一半左右，world space normal opacity为30%左右，然后调整两个texture的opacity获得较好的灰白色水渍效果到我们的两个封条和name label标签上基础效果实现完毕，新增一个fill layer，命名为dirt fill, 进行常规dirt mask操作，记得layer颜色改为黑黄颜色，调整dirt level到合适浓度，rough为0.65左右，获得一些塑料的搓揉起伏高点效果。最后增加一些血渍效果，新增paint layer，开启color rough和height即可，rough设置0.8左右，获得不太反光的干血渍效果，height设为0.15左右即可，获得稍微的高度效果即可。可以添加一些alpha血手印都行最后效果如下第六节：tile map边界细节增添tile map省时省力，但是一些区块需要新的细节，使得整体环境不太单调，对此采用新的layer叠加新的细节贴图，需要注意的是，substance painter内opacity调整为0进行遮罩图层边缘过渡，unity遮罩贴图渲染模式改成fade，同时可以调整albedo alpha通道强度获得细腻的遮罩浓度首先photoshop内对边角遮罩贴图进行加工，增加污渍或者新增蒙版图层，进行渐变过渡效果设置，让地板边界增添暗色阴影效果导出tga格式，1024x1024分辨率即可，使用bitmap2material软件，按着basecolor， normal map+roughness，heightmap，metallic，ambientocclusion顺序一项项调整六张贴图效果，输出，得到一套六张贴图材质打开substance painter，关键点：display显示渲染shader改为with alpha blender的，需要修改opacity，然后新增ambientocclusion和opacity渲染项目，去掉id map，开始渲染贴图渲染完毕，导入6张贴图材质，新增fill layer,拖放对应材质到对应项目，再新增一个paint layer，仅选择op开启，调整opacity 值为0，开始利用dirt brushes刷遮罩贴图两个边，形成透明过渡效果，使得与地板过渡自然再新增一个paint layer，增添血迹，开启basecolor和heightmap，增加一些高度起伏，视效果增添一些dirt最后输出材质，unity5 metallic默认，得到三张材质贴图，导入unity，记得normal map类型设置为normal map修改遮罩模型plane 的scale，对需要的角落进行模型遮罩，修改albedo里alpha通道强度，调整材质浓度，达到最终效果，第七节，血渍遮罩细节绘制，更多的多层遮罩细化效果KYLE喷溅画笔，深浅红色，喷出大体形状，根据形状，敲定中心范围，使用柔边圆画笔，暗红色涂抹中心区域，90%透明度左右，浅红色涂抹外层区域，透明度60%左右，然后根据喷溅分布密度，使用30%-10%透明度，柔边圆小尺寸画出外部形状，达到如下效果使用KYLE墨水盒点出中心几个核心深色区块，再搭配绘画盒潮湿混合器进行晕染，最后增加压力感应喷溅，补上血溅效果，差不多就像那么回事了选择Action-video_action，为当前的血迹图层增加alpha通道ctrl+a，ctrl+c单独复制alpha通道信息，回到图层，ctrl+v粘贴为新图层，单独选择此alpha图层血迹，只显示黑白alpha通道效果，保存项目为blood_alpha.tga,新建空白图层，填充为淡红色，与血迹一起选中，输出为前景血迹贴图，方便在substance painter中与alpha遮罩一同使用第八节 血渍在substance painter中细加工导入一个plane.fbx简单平面即可，1024x1024足够选择显示渲染+alpha blender模式，渲染项目增加op和ao，渲染完毕后导入上面的两个texture素材。新增fill layer，我们调整ambient occlusion为0.5，保持平衡的环境光，metallic归零，不需要闪耀的金属效果Roughness设置为0.15左右，保持较为光亮的新鲜血液反射效果即可base color 和opacity拖拽刚才的两张texture，height稍等新增paint layer设置，这里可以关闭此项基本设置完毕，接下来我们新增一个paint layer进行血渍细节提升命名为Blood_height_roughness,使用watercolor画笔，单独开启roughness选项，边缘采用0.6+roughness，画笔flow设置75左右，大size，对血渍边缘进行粗糙干燥效果处理，越接近中部，roughness设置越低，0.3-0.1不等，size设置变小，进行过度区块精细处理干湿度处理完毕，单独开启height选项，0.05左右height即可，对中部暗色血渍进行高度提升，画笔可以考虑crystal等，越外围越浅色部分，调低0.01每档。黑色溅射血渍可以适当提升高度，最终效果如下最后新增一个paint layer,命名为op_fix,单独开启opacity选项，归零，选择wash画笔，flow设置为2，大size，对原始血晕外围进行淡化处理，包括plane边缘切断的不自然过渡，我们利用非常小的flow进行淡化渐变，以及处理掉太柔和太大的淡色血晕效果。最后输出三张贴图，准备导入导入Unitynormal map记得模式选择normal map，其他照旧。设置一个plane，size x,z方向改为0.1左右比较合适，点坐标系竖直方向进行位置摆放，对准桌面血渍最浓中心然后是颜色校正，还有albedo的alpha通道强度调整为177左右比较合适，roughness可以改为0.9+，避免太过反光。最终效果如下，还行
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第十节：LOD结合视距分区块mesh地形渲染
第十节 LOD结合视距分区块mesh地形渲染上一篇：[post cid="406" /]这一节我们将对不同视距的区块地形进行不同精度LOD区分绘制mesh网格首先我们需要新建一个类，来管理不同精细度的LOD mesh,针对每一个地形区块，获取其对应精度的mesh打开EndlessTerrain脚本：在EndlessTerrain类里面，最后新建：class LODMesh { public Mesh mesh; public bool hasRequestedMesh;//追踪是否已经请求mesh状态 public bool hasMesh; int lod;//当前mesh对应lod档位 public LODMesh(int lod) { this.lod = lod; } void OnMeshDataReceived(MeshData meshData)//②再处理收到mesh数据进行mesh生成，调整状态为true { mesh = meshData.CreateMesh(); hasMesh = true; } public void RequestMesh(MapData mapData)//①，先请求mesh，标记已请求状态，传入对应多线程方法 { hasRequestedMesh = true; mapGenerator.RequestMeshData(mapData, OnMeshDataReceived); } }我们再回到MapGenerator脚本，因为刚才在EndlessTerrain脚本新设置了LOD mesh对应功能而我们MapGenerator脚本也有个levelOfDetail变量，这个是用来管理DrawMapInEditor里面的LOD档位的，我们为了区分两者用途，将原来的levelOfDetal ctrl+r+r重命名为：PreviewLODInEditor，代表这个LOD属于编辑器界面预览LOD效果用的LOD，这样，我们在MapGenerator里面的RequestMeshData方法里请求传入 int lod，就不会弄混淆了，同样的，MeshDataThread方法中也增加此参数传入：int lod，再检查下，RequestMeshData方法里调用了MeshDataThread，这里也要加入 int lod参数，同时将里面调用的MeshDataThread也传入 lod参数，最后，我们检查下MeshDataThread方法，我们这个方法现在是专用来获取不同视距区块不同mesh精度用，上一节为了测试多线程通信，我们简单的对默认的LOD进行接收运算mesh，也就是现在的 PreviewLODInEditor，现在我们不再需要复制PreviewLODInEditor进行处理运算了，我们在这节的主要目的，就是要将所有多线程处理的mesh，改为基于新的lod判断分块体系，获取对应精度lod，再进行处理对应精细度的mesh数据，而不是之前的和PreviewLODInEditor每块地形都是一模一样的mesh精度，于是这里PreviewLODInEditor也要改成 lod。同样的，上节为了测试，与MapGenerator脚本对应的EndlessTerrain脚本中，我们在这里的OnMapDataReceived方法里面，请求执行RequestMeshData方法这句也可以暂时注释掉，稍等我们这节完善新的功能，同样的，OnMeshDataReceived方法暂时也不需要了，我们选中，Ctrl+K+C也全部注释掉，这节稍后我们会重新编写新的。将EndlessTerrain最后方RequestMeshData方法，中间也新增传入 lod参数这样，我们的 lod参数添加已经改造完毕，接下来接着我们在EndlessTerrain脚本需要新建一个结构体，用来管理不同LOD信息，属于哪档LOD，存储对应档位LOD精度，等等信息，方便调取，那么在脚本最后，新建结构体：public struct LODInfo{ public int lod； public float visibleDistThreshold；//可视距离阈值，用来对区块进行距离判定，对不同视距区块划分不同LOD精度，而不仅仅是固定的450视距了为了方便在面板调整视距阈值档位，我们加上 [System.Serializable]修饰，方便我们修改脚本最前方新加入数组：public LODInfo[] detailLevels;保存，来到Unity，去MapGenerator脚本开始手动设置我们LODInfo先将size设为3，来三档视野阈值看看Lod分别设为，0，1，2，VisibleDistThreshold设为200，400，600看看再回到EndlessTerrain脚本，查看我们的MaxViewDistance常数，现在已经不再固定，而是跟随我们最大档位视距阈值改变了，于是我们修改下public static float maxViewDistance；再到Start方法里面，定义 maxViewDistance=detailLevels[detailLevels.Length-1].visibleDstThreshold;数组长度是从1开始计算的，数组的索引值是从0开始的，所以我们最大档位索引值，这里举例就是第三档，对应索引值是长度3-1，即2号索引位置，将这个索引位置的visibleDSTThreshold阈值作为我们最大视距。构建完毕LOD信息相关结构体和存储数组后，我们需要将存储后的信息传入我们TerrainBlock地形里于是我们来到 public TerrainBlock构造函数，随便找个位置，传入参数加一个 LODInfo[] detailLevels,当然了，我们在对应的TErrainBlock类里面也要申明对应的数组信息：LODInfo[] detailLevels；同时在TerrainBlock构造函数里赋值this.detailLevels=detailLevels；//该类里面detailLevels变量赋值为传入参数detailLevels的值对应的，上方UpdateVisibleBlocks方法中调用实例化生成新的TerrainBlock这里也要传入对应数组信息：else{ terrainBlockDictionary.Add(viewdBlockCoordinate,new TerrainBlock (viewdBlockCorrdinate,blockSize,detailLevels,transform,mapMaterials));我们接下来继续完善TerrainBlock地形构建，LODInfo已经顺利加入，我们可以构建对应mesh信息了，TerrainBlock类里面，新加入 LodMesh[] lodMeshes;然后去对应构造函数里新增赋值：lodMeshes= new LODMesh[detailLevels.Length];//然后遍历这个LODInfo信息数组，将里面的LOD信息对应传递给mesh里面； for (int i = 0; i < detailLevels.Length; i++) { lodMeshes[i] = new LODMesh(detailLevels[i].lod); }这些准备工作完成之后，我们终于可以正式处理新的MapData与meshData获取流程了，OnMapDataReceived这里，我们开始对新的地形数据进行处理TerrainBlock类里面新申明两个变量：MapData mapData； bool mapDataReceived；然后去OnMapDataReceived开始构建新的判断逻辑：this.mapData = mapData; mapDataReceived=true;接下来，mesh网格请求前，我们需要新增视距更新，而不是直接调用mapGenerator.RequestMeshData，我们查看下方的UpdateTerrainBlock方法中，我们之前是做了单个区块边界平方和maxViewDistance平方大小比较，仅仅判断是否显示而已，现在要对LODInfo数组里存储的多块地形进行细分，对所有在maxViewDistance内，也就是所有会显示的地形，从i=0开始遍历，将小于当前阈值的地形区块划分给当前的lodIndex，赋值对应精细度lod档位，超过当前阈值距离的地形区块，我们让lodIndex索引+1，划分到更大一档lodIndex再做判断，如果比该档位阈值还大，继续lodIndex+1，直到划分匹配到对应精度LOD，然后break跳出当前循环。在setVisible前插入：if (visible) { int lodIndex = 0; for (int i = 0; i < detailLevels.Length - 1; i++) { if (minDistanceToViewer > detailLevels[i].visibleDistThreshold*detailLevels[i].visibleDistThreshold) { lodIndex = i + 1; } else { break;//与viewer距离小于等于当前LOD阈值，归属正确的lodIndex索引，直接返回，进行下一步遍历 } } }这里为什么不判断 i=detailLevels.Length-1时候的情况呢？因为我们之前已经做了约束，所有的visible区块被 <=maxViewDistance约束着，在visible属性的距离内还比minDistanceToViewer距离大的区块理论上不存在的，实际上 i=Length-1这一堆区块都归属于精度最低那档的lodIndex，在 i=detailLevels.Length-2这步已经将这些区块划分到 lodIndex=i+1这档了，无需再做多余判断。好了，目前我们就拿到了每块地形对应应该显示的LOD档位了，再进行最终mesh绘制前我们需要做个优化，分析下目前获得的lodIndex分类情况和上一帧的index对应分类是否一致，如果一致，我们无需重复再次绘制渲染一遍，避免资源浪费在TerrainBlock类里面，前方我们新申明变量：int previousLODIndex=-1；这样可以确保第一次运行检查对比的时候肯定不会一致，确保首次运行强制绘制一遍初始mesh布局信息我们在 if(visible)内，for循环遍历中，判断当前所有lodIndex后，继续写新的判断：检测与 previous Index的差别：if （lodIndex！=previousLODIndex）{ LODMesh lodMesh=lodMeshes[lodIndex]; if (lodMesh.hasMesh){ previousLODIndex=lodIndex;//将当前Index赋值给下一轮的，相对于下一轮，这轮的算previous meshFilter.mesh=lodMesh.mesh; }else if (!lodMesh.hasRequestMesh){ lodMesh.RequestedMesh(mapData); } } }这边的mesh分LOD精度请求生成和每帧更新机制都 已经完成，UpdateTerrainBlock这整个方法功能已经基本实现，加上最后一个前提条件，就是当mapDataReceived后，才会执行运算mesh，所以将updateTerrainBlock方法内全部内容放在一个大判断框架里，增添if（mapDataReceived）{//将原updateTerrainBlock方法所有内容剪贴进来}按Ctrl+K,D:自动对齐框选内容的格式保存，我们来到unity界面运行看看，查看边界区块果然粗糙很多了，说明LOD分精度渲染mesh起作用了，为了更清楚查看效果，我们改shaded渲染模式为shaded wireframe，这样可以看到mesh网格复杂度区分的更明显了。如果我们改lod档位为0，4，6，会发现三部分阈值对应的lod精细度差别更明显了，拖动viewer应该也更流畅了，毕竟我们降低了周围两档的精度我们甚至可以修改为size 为4档，0，2，4，7对应视野阈值250，500，750，1000，执行后可以看见更精细丰富的lodmesh信息。如果想更加流畅的效果，我们可以考虑在Update方法里，修改原来每帧调用 UpdateVisibleBlocks()方法，为其他条件，比方说，当我们的viewer移动距离超过24个像素点后再调用，就可以降低系统负担也就是说，对viewer移动距离也加一个Threshold阈值，当移动距离超过该阈值的时候，再进行对应UpdateVisibleBlocks方法调用，系统负担应该会有效降低EndlessTerrain脚本开头我们新增常量： const float viewerMoveThresholdToCallBlockUpdate=24f； const float sqlViewerMoveThresholdToCallBlockUpdate=viewerMoveThresholdToCallBlockUpdate*viewerMoveThresholdToCallBlockUpdate;//这个是方便每次运算比较sql平方，运算量比开方小，之前比较maxViewDistance时候分析过这个。 Vector2 viewerPositionOld;//新增旧位置记录，方便做当前位置和旧位置距离比较然后在Update方法开始实现：在viewerPosition赋值语句下一行插入判断：if((viewerPositionOld - viewerPosition).sqrMagnitude>sqrViewerMoveThresholdToCallBlockUpdate){ viewerPositionOld=viewerPosition; UpdateVisibleBlocks();这样的话有两个问题，初始我们地图区块绘制没有被请求，第二，如果viewer不移动，Update里就不会再执行UpdateVisibleBlocks方法了，那么造成问题就是，运行后，相关blocks信息就无法获取到，如果移动viewer，初始所有区块信息的缺失还会造成部分区块信息不加载的问题，我们可以考虑在start方法中，做Blocks信息更新，添加UpdateVisibleBlocks();语句到Start方法里这样就可以解决了初始地图信息获取问题，然后我们来处理下Update方法中，只有当old-当前position值超过阈值后才会UpdateVisibleBlocks，我们可以考虑在请求地图数据的时候加入一个Action ，当收到新的地图数据后，请求callback调用此方法，最后的调用逻辑就是，对UpdateVisibleBlocks方法调用，不仅仅是在Update更新调用，拓展如下：一，每帧检测直到移动距离超过移动阈值，二，获取新的地图数据后，这两种情况下都会进行调用。这样通过简单的移动阈值检测，可以降低每帧自动全部计算一次地图数据的消耗，同时又保证当新的地图数据计算完成，接收地图数据后，必要的绘制请求也能执行获取于是我们在LODMesh类里面，加入一个callback Action：System.Action updateCallback；同时在此LODMesh类构造函数中也传入System.Action updateCallback参数，构造函数内部加入：this.updateCallback=updateCallback；然后在OnMeshDataReceived方法里，我们收到mesh信息后，绘制Mesh数据同时调用此方法：updateCallback();同时我们考虑在一开始生成TerrainBlocks，就绘制好对应LODmesh:TerrainBlock构造函数里，刚才的lodMeshes信息存储时候，我们添加UpdateTerrainBlock方法：for (int i = 0; i < detailLevels.Length; i++) { lodMeshes[i] = new LODMesh(detailLevels[i].lod, UpdateTerrainBlock); }然后就是，在OnMapDataReceived的时候，也进行mesh绘制：来到OnMapDataReceived方法里，我们加入UpdateTerrainBlock();保存，unity运行，应该就ok了，拖动后发现运算也流畅一些了好了，基本功能差不多完成了，我们快速地进行材质绘制功能实现：OnMapDataRecieved()时候，添加材质：Texture2D texture = TextureGenerator.TextureFromColourMap(mapData.colourMap, MapGenerator.mapBlockSize, MapGenerator.mapBlockSize); meshRenderer.material.mainTexture = texture;保存，改shaded模式为shaded，发现彩色材质已经渲染ok，不过目前还是重复绘制的相同色彩分布，我们修正下，打开MapGenerator，找到RequestMapData和MapDataThread方法，传入新参数 Vector2 center，同时在RequestMapData方法里调用到的MapDataThread方法也别忘记加入 center参数。之后，MapDataThread线程里，当我们生成地图数据的时候，传入该 center参数：MapData mapData=GenerateMapData（center）；//在生成地图数据时候传入 center参数然后ctrl加鼠标追踪该GnerateMapData方法，在 GenerateMapData方法传入参数 Vector2 center此方法内部，在生成noiseMap数据时候，我们在offset偏移量这里增加加入 center因子，改为：center+offset最后在DrawMapInEditor方法里，传入 Vector2.zero参数到 GenerateMapData（Vector2.zero)然后回到endlessTerrain脚本，在TerrainBlock构造函数里，最后也有一个地RequestMaapData的调用不要漏掉：MapGenerator.RequestMapData(OnMapDataReceived);我们传入 positon参数，最后效果：MapGenerator.RequestMapData(position，OnMapDataReceived);保存后，查看效果，发现地形算是随机不重复了，不过每个区块间接缝还是有些问题的，我们针对这个问题，下一节再详细研究了。
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第九节：多线程同步协调处理多区块思想和基本实现
第九节 多线程同步协调处理多区块思想和基本实现上一篇：[post cid="403" /]这节我们将继续上节的无限地形生成功能完善，将上节制作出来的视野内三维plane的显示和隐藏功能与我们的MapGenerator相结合，这里将会用到多线程技术，之前我们第一节了解过协程的原理，这里多线程技术比协程更高级一些，这算是真正的多个线程都在一起跑，而不是协程的跳来跳去，适合处理一些短时间内计算量很大，任务数量较多，可以同步单独运算，同时又追求高效完成需要快速拿到多个运算结果的事情。由于Unity系统限制，实际上我们unity本身是没有多线程的，我们的副线程是干不了前台的画面渲染，人物运动，物理碰撞，AI运算这些涉及到Unity SDK或者说继承于Monobehaviour的一些操作的，这些只能交给主线程，但是，这不代表多线程没有用，后台的数据运算，比如说伤害计算，MeshData数据构建，网络交互更新，一些复杂的I/O调取操作，都是可以通过分配给副线程去做的，多线程是c#里实现的，可以辅助unity达到多线程的效果，通过Action和Callback之类的指令，和unity主线程交互，决定下一步的运算或者释放。多线程运算，涉及到的核心主要有两点，首先，unity主线程和其他C#副线程的交互，是一个重点，这个交互思想代码模板需要熟悉；其次，对于任务和计算的分配，这个优先级和排序，也是一个重点，否则大家一起抢着干同一件事情，造成数据混乱，显然是帮倒忙，也是不可取的；这一节，主要涉及到线程间交互方式，基本框架的构建和熟悉，这一块基本上都是标准模板，初学复杂，熟练后都是套路，代码看起来多，实际都是固定格式，不用害怕。对于任务优先级排序，我们稍微了解下入门级的五个优先级排序，和Lock指令锁定分配任务和分派线程，今后有时间的话再聊聊Loom指令委托到主线程上进行调度，差不多就可以实现一些基础的多线程操作了，更复杂的，慢慢来，多线程就像一把封印的神器，需要不断积累经验，解封用法，才会用的更加强大。好了，我们正式开始：在多线程编写前，我们需要打扫下之前的代码，MapGenerator脚本，我们GenerateMap方法里MapDisplay display三种drawMode方式判断和执行，算是一个单独的方法，我们为了今后不断扩充的系统考虑，将方法细分独立出来，目前考虑将这三种生成渲染模式放置到Inspector面板方便调试，我们可以在此脚本新建一个方法：public void DrawMapInEditor（）{ 复制所有Display代码到这里接下来，我们需要从GenerateMap方法中调用相应的Map数据，之前我们display这块代码和地图信息数组在同一个方法里，是可以直接调取GenerateNoiseMap里面这些地图数据的，现在分家了，在其他方法里调用另一个方法里的数据，是需要一个载体存储对应数据，再调用载体，读取数据，这也是基本的编程思想于是我们在脚本最后新建地图数据结构体：public struct MapData{ public float[,] heightMap;//灰阶高度图是二维数据，需要二维数组存储宽度和高度数据 public Color[] colourMap;//彩色图是整个色彩像素点数据，一维数组就可以存储Ctrl+.或者 Alt+Enter，快捷键构建结构体内部数据，勾选刚才新建的两个数据类型，然后确定就可以快速初始化了，//(如果后期不需要修改地图数据，可以将结构体内部所有元素加入readonly关键词)接下来我们在GenerateMap方法中返回MapData类型数据，void改成MapData同时别忘记在方法里，最后返回对应数据类型：return new MapData（noiseMap，colourMap）;这样，我们就可以通过MapData载体，在我们的DrawMapInEditor方法中调取需要的地图类型和对应数据了，在DrawMapInEditor方法里，开头申明赋值，准备调用：MapData mapData=GenerateMap（）;然后我们将noiseMap，colourMap修正为现在的 mapData.heightMap,mapData.colourMap，结构体内对应map类型，就可以进行绘制渲染了如果之前没有手动换行的话，一个快捷键组合：Ctrl+E+W，让系统自动换行，避免页面外的信息遗漏，忘记修改这么修改后，我们的GenerateMap就单独负责地图信息生成了，而与外部交流沟通数据，就交给我们新建的DrawMapInEditor方法了，于是我们可以考虑将 public MapData GenerateMap方法改为private，因为暂时不需要用来外部类调用它了，去掉public关键词即可，默认就是private了为了更精准描述方法定位，修改方法名为：GenerateMapData(),快捷键 ctrl+r+r；保存后回到unity，应该会出现一些报错，涉及到其他脚本调用，我们需要进去修改对应的调用，对接到DrawMapInEditor方法原来是MapGeneratorEditor脚本，修改 mapGen.GenerateMap()方法为：mapGen.DrawMapInEditor(),就ok了好了，折腾这么多，我们终于将MapGenerator内部方法具体拆分，以便接下来将我们的MapGenerator和EndlessTerrain统筹分工，通过协作多线程完成多块地形同步生成，下面的内容是我们整个无限地形模块重中之重，我会详细用图示解释清楚：首先，我们的EndlessTerrain脚本，需要获取MapGenerator里面的MapData，meshData各种数据，然而我们的MapGenerator里面是需要通过多个帧数运算才会生成相应的数据，那么我们从EndlessTerrain脚本发生请求，肯定不能立马收到MapGenerator传回的地图数据，于是，我们在调用MapGenerator脚本中请求地图数据方法的时候，为该方法自带传入一个Action请求处理完毕后传回callback数据：void RequestMapData（Action<MapData>callback){ //在此方法中，开始启动MapData处理线程 }意味着MapGenerator外部EndlessTerrain之类的脚本调用请求后，我执行完毕数据生成后，会callback发送返回地图数据，这个Action传递MapData信息给外部脚本好的，我们回到EndlessTerrain脚本，在申请调用MapGenerator地图生成方法的时候，应该传入一个接收地图数据时候需要执行的方法，方法名可以叫做OnMapDataReceived，当RequestMapData方法执行完毕后，收到Callback的数据后，也就是接收地图数据后，调用执行此OnMapDataReceived方法，对数据进行处理：void start（）{ RequestMapData（OnMapDataReceived）； } void OnMapDataReceived（mapData mapData）{ //在这里处理mapData地图信息 }这样，我们传回MapData行为，callback回到我们EndlessTerrain后，就可以调用咱们的OnMapDataReceived方法，开始对传出来的地图数据在此方法里进行处理了即MapGenerator中RequestMapData方法自带callback 行为，一旦被外部请求调用，必定会自动传出数据，配合EndlessTerrain外部脚本调用该方法的时候指定传入OnMapDataReceived方法，以便对传回数据进行处理，这样就做到了脚本间线程间基本交互实现。接下来，我们需要关注下MapGenerator这个副线程处理逻辑，当我们void RequestMapdata方法启动我们的MapData处理线程：void MapDataThread（Action<MapData.callback){ //在此线程先运算得到mapData //这里是重点，我们知道，直接进行mesh网格渲染，需要用到unity内部组件，而unity本质上没有多线程处理这些unity内部操作的，我们不能在c#这些副线程里直接处理渲染，所以需要添加存储mapData，然后传回到等待队列，因为有多个副线程同时在处理各个区块地形数据，EndlessTerrain脚本不可能随时同时接收完毕所有副线程处理出来的地图数据，所以我们先存储到传回队列，排好队。接下来，副线程在Update方法里，执行队列位置计算，在队列中按顺序一个个发送callback传回mapData：void Update（）{ if（queue.Count>0{ //调用callback指令，传出mapData数据 } }这就是我们的多线程相互协作的基本原理，接下来我们开始逐步实现，这些基本框架第一次接触可能觉得代码量很大，不过没关系，所有多线程基础框架都可以按类似的逻辑构建来到MapGenerator脚本，我们通用的多线程调用基础，先引入对应命名空间下方法类型：using System；//获得Actions各种行为操作调用功能 using System.Threading;//使用多线程功能接下来，按着我们刚才的分析，开始第一步，构建我们的RequestMapData方法，附带Action，callback此Action，附带传回Action参数为MapData类型数据：public void RequestMapData(Action<MapData> callback）{ //请求生成对应副线程执行地图数据处理，我们稍后书写。 }方法内部功能稍后构建，我们接下来构建此方法将调用的启动MapData运算副线程：void MapDataThread(Action<MapData> callback) { }这样，我们就可以在RequestMapData方法开始构建启动计算副线程功能了：ThreadStart threadStart = delegate { MapDataThread(callback); }; //启动线程，委托给带参函数MapDataThread，记住这是一个语句，不是什么方法也不是类，一定要在语句结束后加上分号，新手最容易以为大括号代表结构和类，后面漏掉分号咱们的方法是外面这个大括号，RequestMapData才是我们的方法结构，第一句话委托给指定线程处理指定方法，接下来写第二句别忘了启动对应线程：new Thread (threadStart).Start();//调用线程类型下的Start（）方法，ok，现在我们MapDataThread方法就可以运行在另一个线程里了，我们实现下 void MapDataThread方法里具体的线程内部功能：MapData mapData=GenerateMapData();//请务必记住，副线程里直接调用的方法，只能是独立于Unity功能的方法，例如：FindObjectOfType,GameObject.Find,SendMessage,StartCoroutine等等方法，是Unity内部组件API等接口所含的功能，这些方法不能直接调用，否则会报错我们再回顾下刚才的【地图生成器和无限地形生成脚本间多线程协作基本原理】图示，地图数据处理，我们直接调用对应方法，一句话就轻松实现了，接下来需要创建一个结构体，存储我们的地图数据，方便等会扔到队列里，存进去按顺序一个个传回去我们在MapGenerator类里面，最后添加一个结构体：struct MapThreadInfo<T> { public Action<T> callback; public T parameter;Ctrl+.或者 Alt+Enter快速重构勾选两个变量，快速构建结构体初始化由于这个结构体我们不需要对其进行改变，作为存储容器，保持其固定不变，建议对所有元素加入readonly关键字，类似于刚才构建的MapData结构体，内部元素都可以加上readonly关键词接下来，为了调用队列Queue方法，我们需要引用新的命名空间：using Systemcollections.Generic;List ,Array,Dictionary这些数据集合也是在这个破容器空间里面的我们在当前MapGenerator类里面，开头申明一个Queue并初始化：Queue<MapThreadInfo<MapData>> mapDataThreadInfoQueue= new Queue<MapThreadInfo<MapData>>();接下来，我们就可以在MapDataThread里面安排上了mapDataThreadInfoQueue.Enqueue(new MapThreadInfo<MapData>(callback,mapData));使用enqueue方法将新运算出来的地图数据和callback指令都传入队列，方便之后从队列调取传回mapData数据到外部脚本EndlessTerrain。这里由于是在副线程运行的，地图数据可以被多个线程同时访问，为了保证单张地图数据完整性，这里加上lock锁定此队列lock （MapDataThreadInfoQueue）{ mapDataThreadInfoQueue.Enqueue(new MapTheradInfo<MapData>(callback,mapData)); }lock代表一旦有一条线程获取到此代码，其他线程就无法访问该代码了，直到代码运算完成释放接下来我们实现最后一步，在Update方法里，判断queue队列里面的排队信息，如果存在队列元素，遍历内部存储的所有队员，一个个callback传送出去，同时将该队员元素从等待队列中移除：void Update() { if (mapDataThreadInfoQueue.Count > 0) { for (int i = 0; i < mapDataThreadInfoQueue.Count;i ++) { MapThreadInfo<MapData> threadInfo = mapDataThreadInfoQueue.Dequeue(); threadInfo.callback(threadInfo.parameter); } } }我们MapGenerator这边请求地图数据方法，启动多线程地图数据运算线程功能，计算好后存储数据到结构体，并且加入队列，然后Update方法将队列元素一个个传出，这些都已经实现，现在来到EndlessTerrain脚本看看如何接收数据：首先我们要调用MapGenerator脚本下的方法，自然要先申明一个对应的MapGenerator：static MapGenerator mapGenerator;Start（）方法中实例化对应地图生成器：MapGenerator=FindObjectOfType<MapGenerator>();好了，开始构建我们EndlessTerrain脚本多线程配合方法：TerrainBlock类里面，新增方法void OnMapDataReceived（MapData mapData）{}对了，别忘记先调用下MapGenerator脚本里面的请求地图数据脚本，先得请求那边执行下，我们在TerrainBlock类里，构造TerrainBlock内容里，最后加上一个获取地图数据请求：mapGenerator.RequestMapData(OnMapDataReceived);嗯，这样就可以让那边多线程处理数据了，再收到那边队列传过来的mapData，我们就可以在OnMapDataReceived方法进行接收处理了，我们继续编辑OnMapDataReceived方法，先做个测试输出：print("地图数据已接收");先打印下，检测传递是否起效果然，出现25条信息，说明ok了，我们中间9宫格9块，加上外围16块地图，总共25块都已经运算完毕并且通信传回了，查看MapGenerator物体，下方对应生成了25个plane，说明ok，非常不错这里为啥只传回地图数据而不是同时传回mesh数据呢？因为我们接下来要在这里做LOD系统，得先对地图分块进行筛选，等会单独构建OnMeshDataReceived方法，对远处的地图直接绘制低精度mesh，这样节省资源占用，最后将不同精度mesh单独处理匹配生成完毕，再传入后续脚本进行渲染mesh好了，我们继续- 新目标：Mesh网格数据生成由于已经拿到地图数据了，之前做为演示用的 meshObject=GameObject.CreatPrimitive初始模型plane已经失去测试意义了，修改为：meshObject = new GameObject("Terrain Block");//创建mesh网格模型Terrain Block同时修改下注释我们还需要对其赋予mesh Filter和mesh Renderer组件，构建mesh网格需要：TerrainBlock类里面，开头加上两个变量：MeshRenderer meshRenderer; MeshFilter meshFilter;接下来为刚才的新物体添加对应mesh存储器和渲染器：meshRenderer =meshObject.AddComponent<MeshRenderer>(); meshFilter = meshObject.AddComponent<MeshFilter>(); mesh组件都构建完毕，我们可以开始实现OnMeshDataReceived方法第一个功能了，为我们的物体构建基本mesh信息： void OnMeshDataReceived(MeshData meshData){ meshFilter.mesh=meshData.CreateMesh(); }我们再次回到MapGenerator脚本，这里面我们需要按着RequestMapData方法，做一个类似的请求meshData的方法，偷懒的话全部可以复制MapData的流程改改字母：一，申明新队列 Queue<MapThreadInfo<MeshData>> meshDataTheradInfoQueue= new Queue<MapThreadInfo<MeshData>>();2.创建新的请求meshData方法public void RequestMeshData(MapData mapData, Action<MeshData>callback){ ThreadStart threadStart = delegate { MeshDataThread(mapData, callback); }; new Thread(threadStart).Start();开启新的meshData计算线程 void MeshDataThread(MapData mapData, Action<MeshData> callback) { MeshData meshData = MeshGenerator.GenerateTerrainMesh(mapData.heightMap, meshHeightMultiplier, meshHeightCurve, levelOfDetail); lock (meshDataThreadInfoQueue) { meshDataThreadInfoQueue.Enqueue(new MapThreadInfo<MeshData>(callback, meshData)); } }然后在UpDate方法里也跟着抄一遍MapDataQueue传出队列遍历： if (meshDataThreadInfoQueue.Count > 0){ for (int i = 0; i < meshDataThreadInfoQueue.Count; ++) { MapThreadInfo<MeshData> threadInfo = meshDataThreadInfoQueue.Dequeue(); threadInfo.callback(threadInfo.parameter); }好了，终于复制粘贴完毕了，我们回到Endless Terrain脚本，接收到MapData后，我们再次请求对应的meshData对我们的OnMapDataReceived方法，之前的print测试语句删掉，改成：mapGenerator.RequestMeshData(mapData,OnMeshDataReceived);由于我们meshData也拿到了，之前TerrainBlock类下的plane对应的mesh localScale尺寸设定也可以去掉了： meshObject.transform.localScale = Vector3.one * size / 10f;删除此句。然后我们需要对TerrainBlock构造体传入新参Material materal，终于可以对咱们的地图数据和mesh数据进行材质渲染了- 新目标：texture材质渲染TerrainBlock构造传入新参：Material material结构内部添加材质信息：meshRenderer.material=material;然后同时别忘了在EndlessTerrain类里面，开头申明对应材质变量：public Material mapMaterial；再找找下面的UpdateVisibleBlocks方法，别忘了循环遍历判断生成符合要求的新block时候，也需要传入地图材质，对新创建的block添加材质信息：else{ terrainBlockDictionary.Add(ViewdBlockCoordinate,new TerrainBlock(viewedBlockCoordinate,blockSize,transfrom,mapMaterial));//在这里添加材质参数保存，回到unity，点击MapGenerator物体，然后查看我们Inspector面板，Endless脚本上需要挂载下我们的Map Material，点击assets文件夹下的Materials文件夹，里面之前创建的MeshMat就是了，拖拽上去，ok，运行查看生成的地形，似乎比较诡异导致这些诡异的地形生成的原因，应该是在MapGenerator脚本，执行RequestMeshData的时候，调用多线程：MapDataThread获取 MeshData meshData = MeshGenerator.GenerateTerrainMesh时，我们MeshGenerator脚本里，查看这个GenerateTerrainMesh静态方法里面，heightCurve在顶点生成的时候，heightCurve.Evaluate方法，被多个线程同时访问处理，最后evaluate方法运算数据发生异常我们可以通过Lock指令，对meshData.vertices生成语句的heightCurve曲线进行锁定改为：lock （heightCurve）{ meshData.vertices[vertexIndex]=…..}保存后unity运行，移动一下viewer，发现生成的地形正常了但是我们认真考虑后没有采用此方法，为什么？因为一旦我们锁定顶点运算方法，其他线程就得乖乖躺着等一个个线程运算顶点数据，这意味着咱们二十五块mesh差不多变成单线程处理mesh信息了，辛辛苦苦弄的多线程失去了意义，地形生成速度会大大降低于是我们思考一下，现在是大家都抢着访问传入的一个曲线，研究他，解析它，读取曲线的数值，利用数值来进行顶点运算，我们完全可以实例化一个曲线，让该曲线 的值等于传入的曲线参数的值，有了一个实例化的载体，大家就可以愉悦地同时查阅该曲线值了，而不是把传入的参数来回抢夺，造成这种问题的原理，目前我的能力无法解决，希望有大佬可以指点下，好了，我们继续修复：MeshGenerator脚本的GenerateTerrainMesh方法里，我们在开头新建一个曲线：AnimationCurve heightCurve =new AnimationCurve(basedHeightCurve.keys);我们考虑命名重复问题，将之前GenerateTerrainMesh方法传入的heightCurve和这里要赋值的传入参数全改成basedHeightCurve，这样后面生成顶点时读取的曲线数值，就从这个实例化的curve中获取了，可以支持多线程同时访问，保存后执行，发现获取速度快了很多，效果也ok目前来说，我们生成的这些破玩意，都是一模一样的mesh地形，接下来，我们将添加材质，生成连续不同地形，以及LOD细节划分等等，更多内容，我们下节再见了下一篇：[post cid="411" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第八节：多区块地形生成与隐藏
第八节 多区块地形生成与隐藏上一篇：[post cid="400" /]新建EndlessTerrain脚本public const float maxViewDistance=450; public Transform viewer; public static Vector2 viewerPosition; int blockSize; int blocksVisibleInViewDistance;上一节我们说到实际渲染的mesh网格是地图尺寸-1，我们为多区块地形的每个地形单元新定义变量名blockSize，下面在开始方法中进行赋值：void Start(){ blockSize=MapGenerator.mapBlockSize-1; blocksVisibleInViewDistance=Mathf.RoundToInt(maxViewDistance/blockSize); }接下来我们需要更新可见区块，那么继续构建相应的方法：每个mesh区块240尺寸，我们viewer位置设为（0，0）mesh原点的话，左侧block就是（-240，0），右下角的block就是（240，-240），那么最大视距除以每个block尺寸取整后，在区块坐标来说，左侧block就可以设为（-1，0），右侧就是（1，0），右下角的block所属区块坐标就是（1，-1）这个规则弄明白后，我们继续构建更新可视区块方法： void UpdateVisibleBlocks() { int currentBlockCoordinateX = Mathf.RoundToInt(viewerPosition.x / blockSize); int currentBlockCoordinateY = Mathf.RoundToInt(viewerPosition.y / blockSize); //求出当前区块x，y坐标值，使用取整方法 //通过遍历正负两个方向最大可视区块坐标范围，给所有视野内区块赋值对应坐标位置。 for (int yOffset = -blocksVisibleInViewDistance; yOffset <= blocksVisibleInViewDistance; yOffset++) { for (int xOffset = -blocksVisibleInViewDistance; xOffset <= blocksVisibleInViewDistance; xOffset++) { Vector2 viewedBlockCoordinate = new Vector2(currentBlockCoordinateX + xOffset, currentBlockCoordinateY + yOffset); } } } 接下来，我们就可以实例化这些block区块了，但是实例化之前，我们需要一个数据集合来存储这些坐标值和对应 地形区块信息，因为需要一个值存储坐标一个存储地形区块信息，那么 Dictionary字典是比较合适的，key键存储坐标信息，对应value值指向地形区块信息，这样到时候就可以记录所有地形，以免重复生成或者漏掉。创建字典之前，我们对所有地形区块创建一个类来表示：在我们的EndlessTerrain类里面，后面新建一个public class TerrainBlock{ }然后调用Dictionary数据集合类型需要用到using System.Collections.Generic;//默认已有则不用添加EndlessTerrian方法中新增字典：Dictionary<Vector2, TerrainBlock> terrainBlockDictionary = new Dictionary<Vector2, TerrainBlock>();再来到下方UpdateVisibleBlocks方法if (terrainBlockDictionary.ContainsKey(viewedBlockCoordinate)) { //稍后进行存储调取操作 } else { terrainBlockDictionary.Add(viewedBlockCoordinate, new TerrainBlock()); }//如果不存在，则将符合要求的block和对应坐标存储进字典保存我们再思考下public class TerrainBlock{这个类，当前需要通过一个空物体或者说mesh管理器之类的东西，生成对应block的mesh载体plane，最后再和我们的MapGenerator脚本交互，进行block生成功能实现。于是我们开始构建TerrianBlock类的构造函数：public class TerrainBlock{ GameObject meshObject; Vector2 position; public TerrainBlock(Vector2 coordinate, int size) { position = coordinate * size; Vector3 positionV3 = new Vector3(position.x, 0, position.y);//坐标为负值的话，代表位置在viewer的左侧 meshObject = GameObject.CreatePrimitive(PrimitiveType.Plane);//创建基础模型之平面 meshObject.transform.position = positionV3; meshObject.transform.localScale = Vector3.one * size / 10f;//之前[city builder][3]提到过，平面是10x10units的，所以scale再除以10f } }那么，我们就定义好了每个block的三维plane平面载体位置和尺寸，为mesh网格载入做好准备接下来我们新建一个更新地形区块的方法，好让Terrainblock状态随时更新，更新什么呢？一个距离判断，检查这个block周长上所有的点，与viewer最近的距离，实时监测对比我们maxViewerDistance距离，如果大于最大可视距离，就不生成咱们的meshObject，如果小于最大可视距离，就要纳入可见block，激活对应的meshObject，进行实例化生成并存储到对应terrainBlockDictionary字典里检测周长，我们需要用到Bounds结构，在 public class TerrainBlock{ 类里我们新增申明一个新的Bounds结构：Bounds bounds;然后来到public TerrainBlock构造函数里，对应的，我们生成新的bounds，传入对应block的位置和size尺寸：bounds = new Bounds(position,Vector2.one* size);然后我们就可以通过更新方法开始检测block边框也就是周长上的点和viewer之间最短距离了，用到的是Bounds结构下的SqrDistance方法，获得viewer这个point，到我们的bound结构最短距离的平方值在 public class TerrainBlock类里面新增方法：public void UpdateTerrainBlock() { float minDistanceToViewer = Mathf.Sqrt(bounds.SqrDistance(viewerPosition)); //我们需要计算的是最小距离，所以再用数学方法类里面的求平方根函数（square root）还原 bool visible = minDistanceToViewer <= maxViewDistance;// 做边界值true/false判断开关 }依据此bool 判断，我们就可以新建开启显示方法，对符合距离的block激活显示：public void SetVisible(bool visible) { meshObject.SetActive(visible); }然后将此方法在UpdateTerrainBlock（）中调用，加入：SetVisible(visible);同时在TerrainBlock构造函数里最后增添block初始状态为false，由每帧都执行的Update方法来判断是否激活显示：SetVisible(false);TerrainBlock初始构建基本完善，UpdateTerrainBlock检测判断和执行SetVisible方法都构建完毕，我们终于可以回头来继续填写UpdateVisibleBlocks方法了//新增地形区块，我们传入区块坐标和对应尺寸，然后对已存在于字典里的区块，进行Update实时更新检测，用来时刻更新最小距离判断，决定是否显示该区块 if （terrainBlockDictionary.ContainsKey（viewedBlockCoordinate)){ terrainBlockDictionary[viewedBlockCoordinate].UpdateTerrainBlock();//监测最小距离 }else { terrainBlockDictionary.Add（viewedBlockCoordinate，new TerrainBlock（viewedBlockCoordinate，blockSize））;//完善传入参数为了让每帧都实时更新，我们新增unity系统默认的Update方法里让每帧都能调用执行：void Update（）{ viewerPosition=new Vector2（viewer.position.x，viewer.position.z）；//每帧都要监测的信息有两个：我们viewer的位置以及执行刚才的更新可见block方法 UpdateVisibleBlocks（）； }有人会说，既然我们每帧都要执行 UpdateVisibleBlocks方法，那么刚才的if判断体里面需要多次判断执行 UpdateTerrainBlock方法，而这个方法里面每次要进行float minDistanceToViewer=Mathf.Sqrt(bounds.SqrDistance （viewerPosition）)； bool visible=minDistanceToViewer<=maxViewDistance；平方根计算，效率会比较低，我们可以改成float minDistanceToViewer=bounds.SqrDistance （viewerPosition）)； bool visible=minDistanceToViewer<=maxViewDistance*maxViewDistance；这样，直接比较最小距离平方和最大视距平方值，每次计算一次乘法，从运算量来说，会远小于计算 Mathf.Sqrt计算平方根，保存，回到unity，将我们的这节的成果：EndlessTerrain脚本挂载到MapGenerator物体上Hierarchy面板再新建一个方块cube，改名为viewer作为我们的观察点，reset坐标，把这个cube挂载到我们EndlessTerrain脚本上作为对应的viewer transform物体组件点击hierarchy面板Mesh物体，如果mesh显示勾选了，则取消显示，以免遮挡我们生成的planes。再运行，点击Viewer物体，选中它，开始在Scene界面移动它看看效果发现跟随Viewer四周已经可以成功生成一大堆plane了，说明距离判断和实时生成TerrainBlock基本功能已经实现，但是有的plane超过viewer的最大距离后不会自动消失，我们需要继续完善回到我们的EndlessTerrain脚本，我们可以发现 UpdateVisibleBlocks方法里，仅仅循环判断当前帧小于等于 BlocksVisibleInViewDistance范围内的区块，在新的一帧里，上一帧生成的一些区块，有的超出x,yOffset范围的blocks并没有纳入整个for循环判断，因此无法对这些blocks执行 SetVisible（false）操作，viewer移动的越快，上一帧生成的区块，超出当前帧视野范围的就越多，就会导致大量的blocks不会消失，这就需要我们新建一个List清单，记录上一帧生成的所有blocks，在执行当前帧循环判断前，将上一帧所有的blocks 进行 setVisible（false）操作，然后清空List里面所有blocks。当然，你也可以将当前帧生成的视距内的blocks和上一帧生成的blocks全统计起来，最后一并执行 UpdateTerrainBlock方法，统一筛选，也是可以的，只是后者会增大Dictionary空间，增加判断次数，这里不太推荐使用。我们在EndlessTerrain脚本开头申明一个新的List数据集合，因为只需要存储blocks信息，List足够胜任List<TerrainBlock>olderTerrainBlocks=new List<TerrainBlock>();然后在 UpdateVisibleBlocks方法里，循环判断前，将所有上一帧生成的blocks进行隐藏操作：for (int i=0;i<olderTerrainBlocks.Count;i++){ olderTerrainBlocks[i].SetVisible(false); } olderTerrainBlocks.Clear();//，操作完毕，清空List；然后，对当前帧所有 SetVisible（true）的blocks添加进入 olderTerrainBlocks List里面：那么我们在TerrainBlock类中新增一个判断方法，检测所有可视状态blockspublic bool IsVisible() { return meshObject.activeSelf; }回到UpdateVisibleBlocks方法里的循环体，对所有ContainsKey符合要求的字典内Blocks，进行判断，将所有已存在的acitve的blocks加入olderTerrainBlocks列表，方便下一次执行的时候，再次对这些显示的blocks进行 SetVisible（false）操作在if （terrainBlockDictionary.ContainsKey（viewedBlockCoordinate)){ terrainBlockDictionary[viewedBlockCoordinate].UpdateTerrainBlock();里面新增判断：if（terrainBlockDictionary[viewedBlockCoordinate].IsVisible()){ olderTerrainBlocks.Add(terrainBlocksDictionary[viewedBlockCoordinate]) }这样，每次执行前，上一帧的所有blocks就可以设置隐藏了，保存后测试，功能正常，最后，我们注意到左侧Hirarchy面板生成太多plane到根目录，我们将他们生成位置移到我们的MapGenerator物体内，作为子物体，保持美观：回到EndlessTerrain脚本我们对TerrainBlock结构进行优化，传入一个新参数：Transform parent;然后内部构造新增：meshObject.transform.parent=parent;//对所有新增meshObject plane物体，设置父物体为当前脚本挂载的MapGenerator物体然后找到调用TerrainBlock的语句，在UpdateVisibleBlocks循环体里面，对于没有在字典里存在的符合显示的blocks，我们进行实例化一个新的TerrainBlock，这里也传入Transform parent即可： else{ terrainBlockDictionary.Add (viewedBlockCoordinate,new TerrainBlock(viewedBlockCoordinate,blockSize,transform)); //在这里传入新参transform即可实现所有新实例化出来的plane都生成于当前物体下再次保存，测试，发现所有plane都位于MapGenerator物体下了，我们选择Viewer物体，拖动测试，最后的结果比较ok了虽然这节我们的收获成果只有这么个破方块，但是对于后面的无限地形系统来说，这节是非常重要的基础框架，无限地形动态视野控制区块功能，算是核心模块了。编程的核心内容经常是枯燥的，这也是成为高级程序员必经之路，那些堆砌美工和素材弄出来很精致的画面的demo，很多新手上手几个月堆砌各种素材就能做出来，然而对于核心系统的构建，功能基底的实现，程序员的思维，才是你与众不同的地方，也决定着你今后能做多深入的项目。好了，区块视野控制完成，下一节我们可以开始多区块绘制渲染了，这会牵扯到多线程思想，如果是和我一样的新人，建议自己多找下相关资料学习下，这个非常重要的，有时间的话多了解一些没坏处。下一篇：[post cid="406" /]
【Unity 3D进阶教程分享】EndlessTerrain无限地形搭建 第七节：LOD多细节层次渲染功能实现
第七节 LOD多细节层次渲染功能实现上一篇：[post cid="394" /]我们继续上节内容，如何将平面plane mesh网格拉高形成立体网格，这需要新增变量，高度倍数，打开MeshGenerator脚本，GenerateTerrainMesh方法传入新增 float heightMultiplier方法内获取meshData.vertices顶点信息时将y轴信息加上加倍系数heightMap[x,y]乘上刚才新增的 heightMultiplier然后打开MapGenerator脚本,新增同样的加倍因子public float meshHeightMultiplier;在下方display drawMode里面display.DrawMesh(MeshGenerator.GenerateTerrainMesh方法里传入meshHeightMultiplier保存，然后点击MapGenerator物体，inspector面板可以看到咱们的MeshHeightMultiplier系数出来了，调整数值大小可以看见我们的mesh网格就随之起伏了，通过观察可以发现，水域地形也随着起伏剧烈，这和自然环境不太相符，我们应该分区域调整multiplier系数影响比例，水域范围起伏波动应该缓和一些这样我们可以通过加入一条变化曲线（curve）来模拟。打开MapGenerator脚本public AnimationCurve meshHeightCurve;回到unity，我们应该可以在inspector面板看到我们的curve选项了，打开curve选择第三条曲线，0.4这里右键设置新的拐点然后回到脚本，display.DrawMesh方法里，传入的GenerateTerrainMesh方法新增传入一个meshHeightCurve参数MeshGenerator脚本里面对应方法同样也要加入此参数传入：AnimationCurve heightCurve，然后在此方法里进行实现，原来的meshData.vertices获取数值，y轴存储的是 heightMap[x,y]信息，现在要将传入的heightCure曲线信息约束咱们的 heightMap[x,y]后的值存储为y轴数值，于是改此信息为：heightCurve.Evaluate(heightMap[x,y])*heightMultiplierheightCurve.Evaluate就是求出对应括号内位置curve曲线对应该点的数值，再加成高度倍数就拿到了更自然的高度倍数因子了保存，回到unity，点击generate看看效果，若不满意可以微调我们的曲线，达到满意的表现立体拉升效果实现完毕，接下来我们优化下多块地形细节渲染分层，为后面的无限地形生成奠定基础。这里用到的LOD技术，在游戏领域应用非常广泛，我们通过修改不同视野范围的三维模型mesh网格数，来降低系统渲染负荷，优化游戏体验，对远处或者不太容易关注到的模型降低细节表现来获得帧数的提升我们先解释下实现mesh分层的原理之前我们的meshGenerator脚本，存储mesh顶点信息的时候，用到的遍历累加是for (int x=0;x<width,x ++){//创建顶点}那么假设我们width是9，从0~8一共9个点，当index每次累加1的时候，将会逐步累加存储从0-8总共8个顶点信息和对应三角面那么如果我们index遍历规则改为i=2，跳过相邻点，则会存储五个点信息如果i=4，则会存储三个点信息当i=3这种情况，我们会发现需要第十个不存在的点来存储，这明显是不可取的，所以这就是细节划分的限制：简单来说，我们需要找一个公因数，即LOD分层index累加数值和我们地图width宽度值，两者公有的因数我们的演示体现了index和vertices顶点数关系，i=1，vertices个数为9i=2，vertices个数为5i=4，vertices个数为3我们需要找到通用的关系式i=1，v=ii=2，我们观察左边4条曲线代表四个循环，都是被i=2切分后分成了4条，每段记录一个顶点我们可以尝试将总宽度减掉最右边的1个，再除以i=2，获得左边所有切分后记录的顶点数，再补上最右边的点+1，公式可以是 vertices=[(width-1)/i]+1i=4 时候，我们验证下，减去最右侧终点一个后，除以跳跃值i=4，获得2个循环分块，每个分块存储1个顶点信息，最后加上最右侧的终点，结果是3个，和实际相符。公式是可用的下面解释下为何不超过65000个顶点数：一般的，我们 vertices=width*height对于方形地图 vertice=width^2v<=255^2(65025)本来实际限制是 256^2-1即65535，作为边界取值的话，拿不到256这个值，所以最大的正方形就是255的平方了回到我们目前的情况我们上一节提到过，每一行最后一个点不用来记录绘制三角面，那么实际进行顶点计算的点应该是 width-1width-1和i这个index跳跃度系数找最大公倍数，我们分析如下：我们要思考一个优先度，i跳跃系数，能多几档调节为优，能集中在小跳跃范围为优，什么意思呢清晰度细节显示调节档位，如果就三四档，明显不够细腻，变化突兀太明显第二个，index跳跃系数为何尽量限制在较小范围内？如果跳跃值太大，比如说240的宽度你来个40，80甚至120，就绘制出来几个点，太精简了没啥实际意义，所以说，我们理想来说，最好是7，8个档位，能尽量集中在20以内，使得240的宽度也能有基本12个顶点进行绘制，最粗糙的mesh也可以忍受，这样实际应用起来就比较可行了考虑到最大公倍数的原理，我们最好找一批偶数或者相互都有公因数的一批数字或者一批奇数搭配一批偶数，会比较理想，毕竟全奇数的话，20以内3，5，7，11这些破数字很多互相没有啥重复的公因数，这样很难找到一个255以内的公倍数来实现效果通过一番鬼畜的推导最终结果：我们获得可行的 （width-1）最大合适数值是240i的取值可以为1，2，3，4，5，6，8，10，12，15.接下来看自己理念来筛选了上面十个档位对应顶点数依次为240 ，120，80，60，48，40，30，24，20，16.对于这个结果，我们继续分析：我个人觉得高精度过度到低精度开始阶段最好多分细腻过渡合适一些，所以保留2，3，4，从第五档开始，可以认为是中距离视野，5，10舍弃，采用6，8，12间隔跳，远视野设一档15即可那么我们可以得到顶点数依次为240 ，120，80，60，40，30，20，16.也就是我们留下1，2，3，4，6，8，12，15这八档，对应顶点数为240 ，120，80，60，40，30，20，16.width和i都筛选合适之后，我们继续打开MapGenerator脚本我们定义一个常量，这是非常核心的数值，为了防止手贱乱赋值修改，也为了减轻系统负担，我们后期不会去变动这个数值，而且随机地图生成随时都会调用这个常量进行每个区块地图生成，所以这里定义为const常量：public const int mapBlockSize=241;为了尽可能生成最大单块地图，我们将定义每个区块地图为正方形尺寸，所以所有的mapWidth和mapHeight都修改成这个BlockSize好了选择mapWidth变量，ctrl+r+r全部重命名为mapBlockSize，对于mapHeight变量，同样操作，也重命名，搞定之后，删除这两个int值，只保留一个const mapBlockSize即可再新申明一个LOD变量，用来存储我们刚才的八档LOD值[Range(0,7)] public int levelOfDetail;再去下方MapDisplay方法中 drawMesh里面调用的GenerateTerrainMesh方法增加一个传入参数：levelOfDetail最下方我们用OnValidate方法约束mapBlockSize现在也没有必要了，已经锁定为常量241了，删除控制语句然后回到MeshGenerator脚本里面，同步修改GenerateTerrainMesh方法传入新参数 int levelOfDetail该方法内再定义一个mesh渲染等级变量int meshDetailStage = (levelOfDetail == 0) ? 1 : (levelOfDetail == 1) ? 2 : (levelOfDetail == 2) ? 3 : (levelOfDetail == 3) ?4 : (levelOfDetail == 4) ? 6 : (levelOfDetail == 5) ? 8 : (levelOfDetail == 6) ? 12 : 15 ;好吧，我也觉得这代码丑爆了，能力有限，目前只能写出这档次的代码了，希望有大佬指点一下然后将下方x，y循环遍历存储语句y++,x++都改为 y+=meshDetailStage然后新定义一个每行顶点数，公式就是之前说的 [(width-1)/i]+1，这里的i就是刚才的meshDetailStage了:int verticesEachLine = (width-1)/meshDetailStage+1;然后下方我们之前 MeshData meshData = new MeshData(width,height);实例化新的meshData数据传入的是之前的宽和高，这里都改成我们的 verticeEachLine，然后就是x，y循环遍历里面，我们 AddTraiangle方法里面，width也要改成现在的 verticeEachLine，总共应该是三处，改好后，回到unity选中Map Generator物体，然后点右侧Inspector面板的Generate生成我们全新的241尺寸的正方形地图，Scene界面修改shaded渲染为mesh网格渲染模式，然后通过调整meshDetailStage查看网格精细度变化情况stage调的档位越高，mesh就越简单，适合拉远镜头，看远景效果还是ok的，这样，我们LevelOfDetail一个简单的实现效果就ok了，下一节，我们将进行多线程协程处理多个区块地图，为不同视野范围区块做不同的LOD mesh网格渲染，进一步走向我们的无限地图生成目标下一篇：[post cid="403" /]