Unity陶艺之路

2023-11-08 04:08
文章标签 unity 陶艺

本文主要是介绍Unity陶艺之路,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Unity实现陶艺之路

陶艺制作,如下图:
在这里插入图片描述

最近接到了某历史博物馆的一个小项目,其中之一是允许游客利用触控屏模拟陶艺的制作。
经过两天的研究,将制作工程记录和总结如下:

程序总体流程:

  • 根据精细程度等需要动态生成模型
  • 根据用户操作动态调整模型顶点
  • 平滑接缝处的法线

一、动态生成

动态生成有很多种方式,为了性能考虑,除了必要的接缝(UV展开),这里尽量使用了共享顶点的方式,原因是:第一,可大幅度降低顶点数量,后期需要遍历顶点动态调整顶点位置时可提高效率,第二,共享顶点可使用unity自带的Mesh.RecalculateNormals方法高效平滑法线。
模型总体上分为“外底”、“外柱面”,“顶部”,“内柱面”、“内底”等几个部分。其中生成顶点时,还用到了几个小技巧:
1、外底的中心作为第一个顶点,内底的中心作为最后一个顶点,这样遍历所有顶点时,可以很容易排除这两个顶点,因为中心点是不需要进行动态调整的。
2、生成顶点时顺便生成三角形以及计算好UV,整个创建过程只需要一次遍历。
生成过程如下:

// 从生成外底开始
private void CreateBottom(float deltaAngle, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs )
{// 外底中心点作为整个模型的第一个顶点vertices.Add(Vector3.zero);uvs.Add(new Vector2(0.25f, 0.25f));int index = 1;for (int i = 0; i < Details; ++i){float angle = (float) i * deltaAngle;float cosAngle = Mathf.Cos(angle);float sinAngle = Mathf.Sin(angle);Vector3 v = new Vector3(Radius * cosAngle, 0, Radius * sinAngle);vertices.Add(v);//添加三角形triangles.Add( index );triangles.Add(( index >= Details ) ? 1 : index + 1 );triangles.Add(0);//计算UVVector2 u = new Vector2(0.25f + 0.25f * cosAngle, 0.25f + 0.25f * sinAngle );uvs.Add(u);++index;}
}
// 创建外柱体
private void CreateOuter(float deltaAngle, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs)
{for( int layer = 0; layer <= LayerCount; ++ layer ){float height = layer * LayerHeight;int vIndex = vertices.Count;int lastIndex = vIndex - Details - 1;int vIndexAddOne = vIndex + 1;int lastIndexAddOne = lastIndex + 1;float v = (((float)layer) / ((float)LayerCount)) * 0.4f + 0.5f;for ( int i = 0; i <= Details; ++ i ){float angle = ( i == Details ) ? 0 : i * deltaAngle;float cosAngle = Mathf.Cos(angle);float sinAngle = Mathf.Sin(angle);Vector3 vo = new Vector3(Radius * cosAngle, height, Radius * sinAngle);vertices.Add(vo);if (layer > 0 && i < Details ){triangles.Add( vIndex + i );triangles.Add( vIndexAddOne + i );triangles.Add( lastIndex + i );triangles.Add(lastIndex + i);triangles.Add( vIndexAddOne + i );triangles.Add(lastIndexAddOne + i );}float u = ((float) i ) / ((float)Details);Vector2 uv = new Vector2(u, v);uvs.Add( uv );}}
}
// 创建顶部
private void CreateTop(float deltaAngle, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs)
{float inner = Radius - Thickness;for (int h = 0; h < 2; ++h){float height = (LayerCount - h) * LayerHeight;int vIndex = vertices.Count;int lastIndex = vIndex - Details - 1;int vIndexAddOne = vIndex + 1;int lastIndexAddOne = lastIndex + 1;float v = 0.95f + h * 0.05f;for (int i = 0; i <= Details; ++i){float angle = (i == Details) ? 0 : i * deltaAngle;float cosAngle = Mathf.Cos(angle);float sinAngle = Mathf.Sin(angle);Vector3 vo = new Vector3(inner * cosAngle, height, inner * sinAngle);vertices.Add(vo);if (i < Details){triangles.Add(vIndex + i);triangles.Add(vIndexAddOne + i);triangles.Add(lastIndex + i);triangles.Add(lastIndex + i);triangles.Add(vIndexAddOne + i);triangles.Add(lastIndexAddOne + i);}float u = ((float)i) / ((float)Details);Vector2 uv = new Vector2(u, v);uvs.Add(uv);}}
}
// 创建内部柱面
private void CreateInner(float deltaAngle, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs)
{float inner = Radius - Thickness;int count = LayerCount - 1;for (int layer = 0; layer < count; ++layer){float height = ( LayerCount - layer - 1 ) * LayerHeight;int vIndex = vertices.Count;int lastIndex = vIndex - Details - 1;int vIndexAddOne = vIndex + 1;int lastIndexAddOne = lastIndex + 1;float v = 0.5f - ((((float)layer) / ((float)count )) * 0.5f );for (int i = 0; i <= Details; ++i){float angle = (i == Details) ? 0 : i * deltaAngle;float cosAngle = Mathf.Cos(angle);float sinAngle = Mathf.Sin(angle);Vector3 vo = new Vector3(inner * cosAngle, height, inner * sinAngle);vertices.Add(vo);if (layer > 0 && i < Details){triangles.Add(vIndex + i);triangles.Add(vIndexAddOne + i);triangles.Add(lastIndex + i);triangles.Add(lastIndex + i);triangles.Add(vIndexAddOne + i);triangles.Add(lastIndexAddOne + i);}float u = ((float)i) / ((float)Details) * 0.5f + 0.5f;Vector2 uv = new Vector2(u, v);uvs.Add(uv);}}
}
//创建内底
private void CreateInnerBottom(float deltaAngle, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs)
{int index = vertices.Count;float inner = Radius - Thickness;for (int i = 0; i < Details; ++i){float angle = (float)i * deltaAngle;float cosAngle = Mathf.Cos(angle);float sinAngle = Mathf.Sin(angle);Vector3 v = new Vector3(inner * cosAngle, LayerHeight, inner * sinAngle);vertices.Add(v);triangles.Add(index + Details);triangles.Add((i >= Details - 1) ? index : index + i + 1 );triangles.Add(index + i);Vector2 u = new Vector2(0.75f + 0.25f * cosAngle, 0.25f + 0.25f * sinAngle);uvs.Add(u);}vertices.Add(new Vector3(0, LayerHeight, 0));uvs.Add(new Vector2(0.75f, 0.25f));
}

创建好之后的效果图:
整体效果:
整体效果图
底部效果:
底部效果图
顶部效果:
顶部效果图

二、动态调整顶点

模型生成之后,接下来就是动态调整顶点位置了,流程是这样的:
1、使用射线检测来判断目标的位置。
2、判断方向。由于触控到模型的左边和触控到模型的右边,对模型顶点的方向调整是相反的,比如,点模型右边并且往右边拖动,就是要加粗目标,同样,点模型左边并且往左边拖动,也是要增粗,所以左边和右边正好相反。因此需要一个可靠的方法去判断目标点是“左边”还是“右边”。
3、判断目标点和顶点的距离,并且根据“影响的力度”对附近的顶点进行位移。
4、重新平滑一下法线。

具体代码如下:
因为我工作的电脑没有触摸屏,所以写了一个鼠标调整和触控调整兼容的方案:

private void Update()
{// 如果检测到触控用触控,否则用鼠标if( Input.touchCount == 1 ){Touch touch = Input.GetTouch(0);if (touch.phase == TouchPhase.Began){if (Physics.Raycast(Camera.main.ScreenPointToRay(touch.position), out RaycastHit hit, 1000f, PotteryLayerMask)){targetWorldPos = hit.point;isShaping = true;}elseisShaping = false;}else if ( isShaping && ( touch.phase == TouchPhase.Moved)){ShapeIt(touch.deltaPosition);}else if (touch.phase == TouchPhase.Ended)isShaping = false;}else{if (Input.GetMouseButtonDown(0)){lastScreenPos = Input.mousePosition;if (Physics.Raycast(Camera.main.ScreenPointToRay(lastScreenPos), out RaycastHit hit, 1000f, PotteryLayerMask)){targetWorldPos = hit.point;isShaping = true;}elseisShaping = false;}else if (isShaping && Input.GetMouseButton(0)){Vector3 currPos = Input.mousePosition;ShapeIt(currPos - lastScreenPos);lastScreenPos = currPos;}else if (Input.GetMouseButtonUp(0))isShaping = false;}//调试时使用//ShowNormals();
}
// 调整位置
private void ShapeIt( Vector3 deltaPos )
{bool bHorizontal = false;bool bVertical = false;float dirRate = 0, scale = 0;if (deltaPos.x > 0.01f){dirRate = IsInRight() ? 1f : -1f;bHorizontal = true;}else if (deltaPos.x < -0.01f){dirRate = IsInRight() ? -1f : 1f;bHorizontal = true;}if (deltaPos.y > 0.02f){scale = 0.001f;bVertical = true;}else if( deltaPos.y < -0.02f){scale = -0.001f;bVertical = true;}if (bHorizontal){Vector3 targetPos = transform.InverseTransformPoint(targetWorldPos);Vector3[] vertices = theMesh.vertices;int maxVerticesIndex = vertices.Length - 1;float Range = InfluenceLayer * LayerHeight;for (int i = 1; i < maxVerticesIndex; ++i){float max, min;if (i < SplitIndex){max = SqrMaxOuterRadius;min = SqrMinOuterRadius;}else{max = SqrMaxInnerRadius;min = SqrMinInnerRadius;}float dis = Mathf.Abs(targetPos.y - vertices[i].y);if (dis < Range){Vector3 dir = vertices[i];dir.y = 0;if ((dirRate > 0 && dir.sqrMagnitude < max) || (dirRate < 0 && dir.sqrMagnitude > min))vertices[i] += dir.normalized * dirRate * TouchPower * (1f - dis / Range);}}theMesh.vertices = vertices;theMesh.RecalculateBounds();theMesh.RecalculateNormals();SmoothNormals();}if( bVertical){Vector3 sc = transform.localScale;sc.y = Mathf.Clamp(sc.y + scale, 0.5f, 1.5f);transform.localScale = sc;}
}

上面提到如何判断点击点在模型的左边还是右边,这里用到了一个小技巧,方法是,将世界坐标下的目标点和模型位置同时变换到摄像机局部坐标下,然后就可以进行简单的判断它们x轴的大小了,这个方法很巧妙吧!!苦思冥想想出来的。。

private bool IsInRight()
{Transform camera = Camera.main.transform;Vector3 target = camera.InverseTransformPoint(targetWorldPos);Vector3 pottery = camera.InverseTransformPoint(transform.position);return target.x > pottery.x;
}

调整完顶点的位置,还没有结束,由于模型是有“接缝”的(并非所有的顶点都是共享,这是因为如果一个环形的结构顶点全部共享的话,就没有办法进行UV展开,所以要切断“环”,因此存在接缝)。
由于除了接缝之外的顶点全部是共享顶点,unity可以帮我们实现法线平滑,所以,我们只需要对接缝处的顶点进行法线平均即可。非常简单高效。

private void SmoothNormals()
{Vector3[] normals = theMesh.normals;int step = Details + 1;int start = step;int end = start + (LayerCount + 3) * step;for (int i = start; i < end; i += step){int index1 = i;int index2 = i + Details;Vector3 normal = ((normals[index1] + normals[index2]) / 2f).normalized;normals[index1] = normal;normals[index2] = normal;}theMesh.normals = normals;
}

至此,整个流程就完成了。

三、封装和使用

使用截图
详解如下:

  • Details:细节数量,指柱面水平的细节数量。
  • LayerCount:层数,至柱面纵向的细节数量。
  • LayerHeight:层高,每一层的尺寸
  • Radius:外径尺寸
  • Thickness:壁厚度
  • MinRadius:进行制作时可缩小到的最小半径
  • MaxRadius:进行制作时可缩小到的最大半径
  • InfluenceLayer:每次操作最大影响的层数
  • TouchPower:每次操作的“力度”,值越大,操作越灵敏。
  • Potteing:制作时使用的材质。
  • PotteryLayerMask:物体层掩码(射线检测用)

四、UV相关

程序生成模型时,对UV进行了展开,展开后的UV如下:
模型位置示意图
UV示意图
由于内壁和内底应该都为白色,所以进行了UV重合。

制作完的效果图

整体效果:
整体效果
底部效果
底部效果

源码链接

点此下载第一版源码

升级版源码

在上述功能的基础上,优化了触控算法和动态调整算法,增加了持久化功能,支持将陶艺数据保存为json文件,或从json文件中加载。以方便从服务器上传或下载。

点此下载升级版源码

注:

UV映射BUG修复专文,请参见:
https://blog.csdn.net/sdhexu/article/details/127799482

这篇关于Unity陶艺之路的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/367772

相关文章

Unity Post Process Unity后处理学习日志

Unity Post Process Unity后处理学习日志 在现代游戏开发中,后处理(Post Processing)技术已经成为提升游戏画面质量的关键工具。Unity的后处理栈(Post Processing Stack)是一个强大的插件,它允许开发者为游戏场景添加各种视觉效果,如景深、色彩校正、辉光、模糊等。这些效果不仅能够增强游戏的视觉吸引力,还能帮助传达特定的情感和氛围。 文档

Unity协程搭配队列开发Tips弹窗模块

概述 在Unity游戏开发过程中,提示系统是提升用户体验的重要组成部分。一个设计良好的提示窗口不仅能及时传达信息给玩家,还应当做到不干扰游戏流程。本文将探讨如何使用Unity的协程(Coroutine)配合队列(Queue)数据结构来构建一个高效且可扩展的Tips弹窗模块。 技术模块介绍 1. Unity协程(Coroutines) 协程是Unity中的一种特殊函数类型,允许异步操作的实现

Unity 资源 之 Super Confetti FX:点亮项目的璀璨粒子之光

Unity 资源 之 Super Confetti FX:点亮项目的璀璨粒子之光 一,前言二,资源包内容三,免费获取资源包 一,前言 在创意的世界里,每一个细节都能决定一个项目的独特魅力。今天,要向大家介绍一款令人惊艳的粒子效果包 ——Super Confetti FX。 二,资源包内容 💥充满活力与动态,是 Super Confetti FX 最显著的标签。它宛如一位

Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(4)

本文仅作笔记学习和分享,不用做任何商业用途 本文包括但不限于unity官方手册,unity唐老狮等教程知识,如有不足还请斧正​​ Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(3)-CSDN博客  这节就是真正的存储数据了   理清一下思路: 1.存储路径并检查 //2进制文件类存储private static string Data_Binary_Pa

Unity Adressables 使用说明(一)概述

使用 Adressables 组织管理 Asset Addressables 包基于 Unity 的 AssetBundles 系统,并提供了一个用户界面来管理您的 AssetBundles。当您使一个资源可寻址(Addressable)时,您可以使用该资源的地址从任何地方加载它。无论资源是在本地应用程序中可用还是存储在远程内容分发网络上,Addressable 系统都会定位并返回该资源。 您

Unity Adressables 使用说明(六)加载(Load) Addressable Assets

【概述】Load Addressable Assets Addressables类提供了加载 Addressable assets 的方法。你可以一次加载一个资源或批量加载资源。为了识别要加载的资源,你需要向加载方法传递一个键或键列表。键可以是以下对象之一: Address:包含你分配给资源的地址的字符串。Label:包含分配给一个或多个资源的标签的字符串。AssetReference Obj

在Unity环境中使用UTF-8编码

为什么要讨论这个问题         为了避免乱码和更好的跨平台         我刚开始开发时是使用VS开发,Unity自身默认使用UTF-8 without BOM格式,但是在Unity中创建一个脚本,使用VS打开,VS自身默认使用GB2312(它应该是对应了你电脑的window版本默认选取了国标编码,或者是因为一些其他的原因)读取脚本,默认是看不到在VS中的编码格式,下面我介绍一种简单快

Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(3)

本文仅作笔记学习和分享,不用做任何商业用途 本文包括但不限于unity官方手册,unity唐老狮等教程知识,如有不足还请斧正​​ Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(2) (*****生成数据结构类的方式特别有趣****)-CSDN博客 做完了数据结构类,该做一个存储类了,也就是生成一个字典类(只是声明)  实现和上一节的数据结构类的方式大同小异,所

【Unity小技巧】URP管线遮挡高亮效果

前言 在URP渲染管线环境下实现物体遮挡高亮显示效果,效果如下: Unity URP遮挡高亮 实现步骤 创建层级,为需要显示高亮效果的物体添加层级,比如Player 创建一个材质球,也就是高亮效果显示的材质球找到Universal Renderer Data Assets 4.在Assets上添加两个Render Objects组件 第一个做如下三处设置 指定遮挡层级指

【Unity面经】实习篇:面试官常问的一百个面试题

👨‍💻个人主页:@元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 专栏交流🧧🟥Unity100个实战基础✨🎁🟦 Unity100个精华一记✨🎁🟩 Unity50个demo案例教程✨🎁🟨 Unity100个精华细节BUG✨🎁🟨 Unity100个面试题✨🎁 文章