在Unity地形(Terrain)中使用图集(Atlas)

Unity地形集成了许多功能:高度图、树、草等。
本文仅专注于其中一部分:地形贴图。
主要介绍了Unity地形贴图的三种实现方式:

  • Unity自带地形贴图
  • Terrain转Mesh(T2M)解决方案
  • 图集(Atlas)解决方案(重点内容)
    • 导出所需贴图
    • 使用贴图渲染
    • 后续改进
    • 和T2M对比
    • 更多功能

文末提供Demo下载。

三种实现方式

一 Unity自带地形贴图

Unity自带的Terrain组件是支持刷多层贴图的:

设置了五层贴图

二 Terrain转Mesh(T2M)解决方案

自带Terrain因为集成了众多功能,扩展不方便,且顶点数目较多影响性能。一般会利用诸如TerrainToMesh等插件将Terrain转为Obj,由于本文的测试地形十分简单,所以用极少的顶点数即可:

转为Obj之后顶点数少了许多
T2M材质

查看Shader代码,5层贴图采样了7次(包括两张控制图),而随着贴图增加,采样次数也会相应提高

    half4 splat_control = tex2D (_V_T2M_Control, IN.uv_V_T2M_Control);

    fixed4 splatColor1 = fixed4(0, 0, 0, 0);

    splatColor1 = splat_control.r * tex2D(_V_T2M_Splat1, IN.uv_V_T2M_Control * _V_T2M_Splat1_uvScale);

    fixed4 mainTex = splatColor1;
    fixed4 splatColor2 = splat_control.g * tex2D(_V_T2M_Splat2, IN.uv_V_T2M_Control * _V_T2M_Splat2_uvScale);
           mainTex += splatColor2;
           testColor = splat_control.g;
    
    #ifdef V_T2M_3_TEX
           fixed4 splatColor3 = splat_control.b * tex2D(_V_T2M_Splat3, IN.uv_V_T2M_Control * _V_T2M_Splat3_uvScale);
        mainTex += splatColor3;
    #endif
    #ifdef V_T2M_4_TEX
        fixed4 splatColor4 = splat_control.a * tex2D(_V_T2M_Splat4, IN.uv_V_T2M_Control * _V_T2M_Splat4_uvScale);
        mainTex += splatColor4;
    #endif


    #ifdef V_T2M_2_CONTROL_MAPS
         half4 splat_control2 = tex2D (_V_T2M_Control2, IN.uv_V_T2M_Control2);

         fixed4 splatColor5= tex2D(_V_T2M_Splat5, IN.uv_V_T2M_Control2 * _V_T2M_Splat5_uvScale) * splat_control2.r;
         mainTex.rgb += splatColor5.rgb;

         #ifdef V_T2M_6_TEX
         fixed4 splatColor6 = tex2D(_V_T2M_Splat6, IN.uv_V_T2M_Control2 * _V_T2M_Splat6_uvScale) * splat_control2.g;
            mainTex.rgb += splatColor6.rgb;
         #endif

         #ifdef V_T2M_7_TEX
            fixed4 splatColor7 = tex2D(_V_T2M_Splat7, IN.uv_V_T2M_Control2 * _V_T2M_Splat7_uvScale) * splat_control2.b;
            mainTex.rgb += splatColor7.rgb;
         #endif

         #ifdef V_T2M_8_TEX
            fixed4 splatColor8 = tex2D(_V_T2M_Splat8, IN.uv_V_T2M_Control2 * _V_T2M_Splat8_uvScale) * splat_control2.a;
            mainTex.rgb += splatColor8.rgb;
         #endif
    #endif

三 图集(Atlas)解决方案

T2M解决方案主要的问题在于贴图采样次数较多,对性能损耗较大。
同时,贴图数受Sampler最大数(最多16个)影响,也导致表现的多样化受限。

最多注册16个Sampler

是否可以如UI中那样,将所需的贴图制成为一张图集,从而降低采样次数呢?
经过验证,答案是可行的。

1 导出所需贴图

首先,我们需要生成所需的贴图(Demo中调出窗口:Window -> TerrainAtlas -> ExportMaps)

导出贴图的入口

接下来,看一下如何实现这些接口:

1)导出基础图集

需要将地形中使用的基础图集拼为图集的形式

基础图集

从TerrainData中读取基础贴图数组,利用UnlitShader将这些贴图信息渲染到RenderTexture上,然后新建一个预设分辨率大小的图集,并从之前的RenderTexture中读入图集之中,最后将图集保存到指定的文件夹。

    void ExportBasemap()
    {
        TerrainData _terrainData = _sourceTerrain.terrainData;
        SplatPrototype[] prototypeArray = _terrainData.splatPrototypes;

        //创建相应数目的RT
        RenderTexture[] rtArray = new RenderTexture[prototypeArray.Length];

        int texSize = BasemapRes / COLUMN_AND_ROW_COUNT;
        Texture2D[] texArray = new Texture2D[prototypeArray.Length];

        for (int i = 0; i < prototypeArray.Length; i++)
        {
            rtArray[i] = RenderTexture.GetTemporary(texSize, texSize, 24);
            texArray[i] = new Texture2D(texSize, texSize, TextureFormat.RGB24, false);
        }

        //使用一个UnlitShader来将贴图绘制到具体的RenderTexture上
        Shader shader = Shader.Find("Unlit/UnlitShader");
        Material material = new Material(shader);

        //将这些图读入相应数目的Tex2D中
        for (int i = 0; i < prototypeArray.Length; i++)
        {
            Graphics.Blit(prototypeArray[i].texture, rtArray[i], material, 0);
            RenderTexture.active = rtArray[i];
            texArray[i].ReadPixels(new Rect(0f, 0f, (float)texSize, (float)texSize), 0, 0);

            //如果有法线贴图,则将这些值读入
            if (prototypeArray[i].normalMap != null)
            {
                Graphics.Blit(prototypeArray[i].normalMap, rtArray[i], material, 0);
                RenderTexture.active = rtArray[i];
            }
        }


        //走一遍之前的流程(不过需要将写死的数值灵活对待咯!)
        Texture2D tex = new Texture2D(BasemapRes, BasemapRes, TextureFormat.RGB24, false);

        for (int i = 0; i < prototypeArray.Length; i++)
        {
            //需要根据图片的序号算出当前贴图在大贴图中的起始位置
            int columnNum = i % COLUMN_AND_ROW_COUNT;
            int rowNum = (i % (COLUMN_AND_ROW_COUNT * COLUMN_AND_ROW_COUNT)) / COLUMN_AND_ROW_COUNT;
            int startWidth = columnNum * texSize;
            int startHeight = rowNum * texSize;
            for (int j = 0; j < texSize; j++)
            {
                for (int k = 0; k < texSize; k++)
                {
                    Color color = texArray[i].GetPixel(j, k);
                    tex.SetPixel(startWidth + j, startHeight + k, color);
                }
            }
        }

        tex.Apply();
        // Encode texture into PNG
        byte[] bytes = tex.EncodeToPNG();
        string directoryPath = Application.dataPath + DIRECTORYNAME + "/" + _directoryName + "/";
        if (!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }
        File.WriteAllBytes(directoryPath + "MainTex.png", bytes);

        Debug.Log("Basemap Exported");
    }
2)导出索引贴图

索引贴图类似Terrain中的控制图:标记了基础贴图所占的区域。而不同的是,r通道标记了该位置占权重最大的贴图在图集中的索引,而g通道则记录了次要权重贴图在图集中的索引。

rg通道分别记录权重最大和次之的贴图索引

从TerrainData中读出混合贴图,然后新建一个同样大小的贴图作为索引贴图:

void ExportIndexMap()
    {
        TerrainData _terrainData = _sourceTerrain.terrainData;
        SplatPrototype[] prototypeArray = _terrainData.splatPrototypes;
        int _textureNum = prototypeArray.Length;

        //获取混合贴图
        Texture2D[] alphaMapArray = _terrainData.alphamapTextures;
        int witdh = alphaMapArray[0].width;
        int height = alphaMapArray[0].height;

        //新建和混合贴图一样大小的贴图
        Texture2D indexTex = new Texture2D(witdh, height, TextureFormat.RGB24, false, true);

        Color indexColor = new Color(0, 0, 0, 0);

        //对每一个像素进行计算
        for (int j = 0; j < witdh; j++)
        {
            for (int k = 0; k < height; k++)
            {
                //默认都是第一个贴图
                //这里支持将三层索引的信息导出,可供后续的Shader使用
                ResetNumAndWeight();

                //遍历所有Control的所有通道,识别出最大的通道所在的贴图序号
                for (int i = 0; i < _textureNum; i++)
                {
                    //根据贴图的序号算出当前应该计算的是哪个值
                    int controlMapNumber = (i % 16) / 4;
                    int controlChannelNum = i % 4;
                    Color color = alphaMapArray[controlMapNumber].GetPixel(j, k);
                    switch (controlChannelNum)
                    {
                        case 0:
                            CalculateIndex(i, color.r);
                            break;
                        case 1:
                            CalculateIndex(i, color.g);
                            break;
                        case 2:
                            CalculateIndex(i, color.b);
                            break;
                        case 3:
                            CalculateIndex(i, color.a);
                            break;
                        default:
                            break;
                    }
                }

                //将识别出来的序号写入IndexMap的r通道
                //需将此值转换到(0, 1)的范围内,因为最多支持16张贴图,而序号是0到15,则除以15即可
                indexColor.r = _maxTexNum / 15f;
                indexColor.g = _secondTexNum / 15f;
                indexTex.SetPixel(j, k, indexColor);

            }
        }

        string directoryPath = Application.dataPath + DIRECTORYNAME + "/" + _directoryName + "/";
        if (!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }

        indexTex.Apply();
        byte[] bytes = indexTex.EncodeToPNG();
        File.WriteAllBytes(directoryPath + "IndexTex.png", bytes);

        Debug.Log("Exported Index Map");
    }

对于每个像素,将控制图的各个通道所代表的贴图索引以及权重值传给以下函数:记录权重最大和次之的贴图索引,之后再将这两个值写入索引贴图的r和g通道中

    void CalculateIndex(int index, float curWeight)
    {
        //如果比最大的元素大,则取当前为最大,取之前第一为第二
        if (curWeight > _maxChannelWeight)
        {
            _secondChannelWeight = _maxChannelWeight;
            _secondTexNum = _maxTexNum;

            _maxChannelWeight = curWeight;
            _maxTexNum = index;
        }
        //如果仅是比第二的元素大,则取当前为第二
        else if (curWeight > _secondChannelWeight)
        {
            _secondChannelWeight = curWeight;
            _secondTexNum = index;
        }
    }
3)导出混合权重贴图

导出混合权重贴图则相对简单,因为之前在生成索引贴图的时候已经有权重信息了。
这里我们只需要新建一个权重贴图,然后将最大权重和第二权重填入相应的通道中即可

        Texture2D blendTex = new Texture2D(witdh, height, TextureFormat.RGB24, false, true);
        Color blendColor = new Color(0, 0, 0, 0);

                //计算Blend因子,将其填入到贴图通道中
                blendColor.r = _maxChannelWeight;
                blendColor.g = _secondChannelWeight;
                blendTex.SetPixel(j, k, blendColor);

        blendTex.Apply();
        byte[] blendBytes = blendTex.EncodeToPNG();
        File.WriteAllBytes(directoryPath + "BlendTex.png", blendBytes);

点击导出按钮,便得到了混合权重贴图,由于r通道代表了权重最大贴图的权重,所以在仅有两层混合(后续会介绍更多层的混合)的情况下,此值是大于0.5的

rg通道分别代表最大和第二权重数值

2 使用贴图渲染

有了这些贴图之后,就可以开始渲染了,主要经过以下几步:

1)新建材质及Shader

在顶点着色器(vertex shader)中,计算了两张贴图的tiling以及offset,世界坐标和世界法线向量则是为光照计算做准备

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv.zw = TRANSFORM_TEX(v.uv, _IndexTex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);

                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }


在片元着色器(fragment shader)中,取出索引及其权重。根据索引算出具体的贴图在图集中的位置,获取颜色,并对其进行混合,最后应用上Lambert光照模型

            fixed4 frag (v2f i) : SV_Target
            {
                //将贴图中被压缩到0,1之间的Index还原
                float indexLayer1 = floor((tex2D (_IndexTex, i.uv.zw).r * 15));
                float indexLayer2 = floor((tex2D (_IndexTex, i.uv.zw).g * 15));

                //利用Index取得具体的贴图位置
                float4 colorLayer1 = GetColorByIndex(indexLayer1, i.worldPos.xz);
                float4 colorLayer2 = GetColorByIndex(indexLayer2, i.worldPos.xz);

                //混合因子,其中r通道为第一层贴图所占权重,g通道为第二层贴图所占权重
                float2 blend = tex2D (_BlendTex, i.uv.xy).rg;
                half4 albedo = colorLayer1 * blend.r + colorLayer2 * blend.g;

                //Lambert 光照模型
                float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half NoL = saturate(dot(normalize(i.worldNormal), lightDir));
                half4 diffuseColor = _LightColor0 * NoL * albedo;
                return diffuseColor;
            }

根据索引获取颜色的具体代码:先算出行和列号,找到在图集中的起始位置,然后利用世界空间中xz平面的坐标来得出uv坐标

            half4 GetColorByIndex(float index, float2 worldPos)
            {
                float2 columnAndRow;
                //先取列再取行,范围都是0到3
                columnAndRow.x = (index % 4.0);
                columnAndRow.y = floor((float((index % 16.0))) / 4.0);

                float2 curUV;
                //由于是4x4的图集,所以具体的行列需要乘以0.25 
                //如1就是(0.25, 0),刚好对应第二张贴图的起始位置
                curUV = ((columnAndRow * 0.25) + frac(worldPos) * 0.25 );
                return tex2D(_MainTex, curUV);
            }
2)Copy地形Obj并赋予材质

将之前从TerrainToMesh插件导出的地形复制一份,然后将新建的材质赋值给它,并将导出的贴图填入相应栏

填入对应贴图

看一下效果,貌似和预期的不太一样 。。。 :(

初步效果
3)贴图设置

忘了贴图导入设置(Import Settings)了!
主贴图因为是图集,所以在Mipmap层级较高的地方会变为灰色,这也是产生灰色的边缘线的原因

最高级Mipmap整个为灰色

取消勾选Mipmap之后:

朝预期效果迈进了一大步

再设置好索引贴图和混合权重贴图的参数:

索引贴图设置
混合权重贴图设置

之后,再看看效果

修改贴图设置之后的效果

3 后续改进

近距离观察发现远处会比较“碎”,这是因为一个像素对应了多个纹素所致

远处“碎”是因为一个像素对应了多个纹素

但如果开启Mipmap则会出现之前的白线。。。

白线再现。。。

这是因为如果用tex2D采样,则在边缘部分会因为uv变化太大,而取到级别很高的Mipmap上。
要解决此问题,可以用tex2Dlod限制Mipmap的采样级别:
首先,在顶点着色器中,记录观察空间的Z值(距离摄像机距离)

                //记录观察空间的Z值
                o.worldPos.w = mul(UNITY_MATRIX_MV, v.vertex).z;

其次,在片元着色器中,算出一个最低Mipmap采样level,然后再用tex2Dlod对纹理采样

                half lodLevel = min(-i.worldPos.w * 0.1, 3);

            half4 GetColorByIndex(float index, float lodLevel, float2 worldPos)
            {
                float2 columnAndRow;
                //先取列再取行,范围都是0到3
                columnAndRow.x = (index % 4.0);
                columnAndRow.y = floor((float((index % 16.0))) / 4.0);

                float4 curUV;
                //由于是4x4的图集,所以具体的行列需要乘以0.25 
                //如1就是(0.25, 0),刚好对应第二张贴图的起始位置
                curUV.xy = ((columnAndRow * 0.25) + frac(worldPos) * 0.25);
                curUV.w = lodLevel;
                return tex2Dlod(_MainTex, curUV);
            }

限制了Mipmap最小值

可以看到情况好了不少,但还是有依稀的白点。。。 继续改进~
将边缘的像素预留出来,这样应该就好一些了:

                curUV.xy = ((columnAndRow * 0.25) + ((frac((worldPos)) * 0.23) + 0.01));

可以添加一个参数_BlockParams来控制相关的参数,其中z值代表对基础纹理贴图的缩放,类似tiling的效果

                curUV.xy = ((columnAndRow * 0.25) + ((frac((worldPos * _BlockParams.z)) * _BlockParams.yy) + _BlockParams.xx));

最终效果:


消除了白点

4 和T2M对比

效果已经做完,来和T2M对比对比,总结一下利弊:

1)优点
  • 贴图数目少
    仅需3张贴图即可支持最多16层贴图混合

  • 采样次数少
    也正是因为贴图数目较少的缘故,采样次数较少,手机上实测,较TerrainToMesh四层采样性能更高

  • 表现多样性
    当前最多支持16层贴图混合,不用受Sampler数量的限制,能很好地表现地形的多样性

2)缺点
  • 远处依然有些“碎”
    由于图集的存在,当前是在不开mipmap和mipmap层级过高之间取了个平衡,在离得远的地方还是会因为多个纹素对应一个像素而看起来有些“碎”

  • 边缘锯齿
    将摄像机拉近之后查看,在混合边缘处能看到明显的锯齿


    混合边缘锯齿

为了抗锯齿,纹理采样模式(FilterMode)一般设为Bilinear或Trilinear。
而在当前的图集模式下,两种颜色权重是写到同一个通道的,这会在边缘采样时混合而造成突变,也就会产生较明显的锯齿

不同颜色权重混合

而T2M则因为是每层一个通道,所以能有一个较平滑的过渡

T2M每层贴图权重单通道
  • 不支持分层缩放
    使用多个贴图,可以分别设置Tiling,但是使用图集,则只可使用统一的参数控制,这样的话要想表现各层大小差异,则需要体现在贴图本身了。这会导致美术在实际使用中调节起来没那么方便。

5 更多功能

1)添加法线贴图

导出法线图集和导出基础图集类似,只是如果当前层没有法线贴图的话,需要将其设置为(0.5,0.5,1)((0,0,1) * 0.5 + 0.5)

                    Color normalColor = (prototypeArray[i].normalMap == null) ? new Color(0.5f, 0.5f, 1) : GetPixelColor(j, k, normalMapArray[i]);
                    normalTex.SetPixel(startWidth + j, startHeight + k, normalColor);
2)三层混合

需要在计算权重的时候加入第三权重

    void CalculateIndex(int index, float curWeight)
    {
        //如果比最大的元素大,则取当前为最大,取之前第一为第二,取之前第二的为第三
        if (curWeight > _maxChannelWeight)
        {
            _thirdChannelWeight = _secondChannelWeight;
            _thirdTexNum = _secondTexNum;

            _secondChannelWeight = _maxChannelWeight;
            _secondTexNum = _maxTexNum;

            _maxChannelWeight = curWeight;
            _maxTexNum = index;
        }
        //如果仅是比第二的元素大,则取当前为第二,取之前的第二为第三
        else if (curWeight > _secondChannelWeight)
        {
            _thirdChannelWeight = _secondChannelWeight;
            _thirdTexNum = _secondTexNum;

            _secondChannelWeight = curWeight;
            _secondTexNum = index;
        }
        //如果仅是比第三的元素大,则取当前为第三
        else if (curWeight > _thirdChannelWeight)
        {
            _thirdChannelWeight = curWeight;
            _thirdTexNum = index;
        }
    }

然后将第三权重的贴图索引和混合权重分别写入到索引贴图以及混合权重贴图的b通道中

                //将识别出来的序号写入IndexMap的通道中
                //需将此值转换到(0, 1)的范围内,因为最多支持16张贴图,而序号是0到15,则除以15即可
                indexColor.r = _maxTexNum / 15f;
                indexColor.g = _secondTexNum / 15f;
                indexColor.b = _thirdTexNum / 15f;
                indexTex.SetPixel(j, k, indexColor);

                //计算Blend因子,将其填入到贴图通道中
                blendColor.r = _maxChannelWeight;
                blendColor.g = _secondChannelWeight;
                blendColor.b = _thirdChannelWeight;
                blendTex.SetPixel(j, k, blendColor);

接着在shader中计算:

                //将贴图中被压缩到0,1之间的Index还原
                float indexLayer1 = floor((tex2D (_IndexTex, i.uv.zw).r * 15));
                float indexLayer2 = floor((tex2D (_IndexTex, i.uv.zw).g * 15));
                float indexLayer3 = floor((tex2D (_IndexTex, i.uv.zw).b * 15));

                //利用Index取得具体的贴图位置
                float4 colorLayer1 = GetColorByIndex(indexLayer1, lodLevel, i.worldPos.xz);
                float4 colorLayer2 = GetColorByIndex(indexLayer2, lodLevel, i.worldPos.xz);
                float4 colorLayer3 = GetColorByIndex(indexLayer3, lodLevel, i.worldPos.xz);

                //混合因子,其中r通道为第一层贴图所占权重,g通道为第二层贴图所占权重,b通道为第三层贴图所占权重
                float3 blend = tex2D (_BlendTex, i.uv.xy).rgb;
                half4 albedo = colorLayer1 * blend.r + colorLayer2 * blend.g + colorLayer3 * blend.b;

最终效果:

三层混合

以上即为全部内容,Demo在Github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容

  • 我们都知道,一个三维场景的画面的好坏,百分之四十取决于模型,百分之六十取决于贴图,可见贴图在画面中所占的重要性。在...
    自由的天空阅读 12,375评论 0 12
  • 111. [动画系统]如何将其他类型的动画转换成关键帧动画? 动画->点缓存->关键帧 112. [动画]Unit...
    胤醚貔貅阅读 12,992评论 3 90
  • 今天还是吃了晚饭,不行太饱了,明天开始晚上不能吃!轻断食执行到底!!空中自行车➕拜日式拉伸➕天鹅臂➕拉伸!!第4天打卡
    冰冰嗨皮阅读 399评论 0 0
  • 被驯化的早晨如约而至 天空穿上残忍的外衣 静溢的街道隐藏在浓雾中 大马路凑响断肠曲 我独自一人怀着梦想 站立在露冷...
    南宫木舍阅读 283评论 11 5
  • 技术都是工具 思维才是灵魂
    一枚小工匠阅读 187评论 0 1