第3章第1节 两个固定任务之间的切…

本文介绍了如何通过备份和恢复寄存器组实现两个任务间的切换,并详细解释了上下文切换的过程及其实现代码。

源码请在https://github.com/ifreecoding/MbedRtos.git下载

第1节 两个固定任务之间的切换

程序的执行只与指令和数据相关,指令是不可修改的,编译后就确定了,能改变的只有数据,但指令需要对数据进行判断,走不同的指令分支,因此,如果我们需要控制程序的执行过程,不但需要编写出指令,还需要提供可方便使用的数据,操作系统任务切换的过程就是指令备份、恢复数据的过程。

通过前面的介绍,我们知道程序当前指令执行的结果只与R0~R15、CPSR这17个寄存器有关,只要我们能控制这17个寄存器,那么我们就可以控制程序的执行流程,这是实现任务切换的基础。

从C语言的角度来看,任务就是函数,只不过是在操作系统里,一个任务可以切换到其它任务,其实也就是一个函数可以切换到其它函数。当切换发生时,将正在执行的函数1的R0~R15、CPSR这17个寄存器临时保存起来,然后将希望执行的函数2的上次保存的数值恢复到R0~R15、CPSR这17个寄存器,这样芯片就从函数1切换到函数2运行了。当希望从函数2切换到函数1时,再将函数2的17个寄存器保存起来,恢复函数1的17个寄存器,芯片就又继续运行函数1了。这样便在函数1运行的中间插入了函数2,这就是任务切换,也就是所谓的“上下文切换”,函数1或函数2所在的最上层父函数调用的一系列函数就组成了任务,任务是从最上层父函数开始运行的。

图 12  任务切换过程

这种切换也可以在多个任务之间进行,至于什么时候切换,怎么控制切换,这就是操作系统要做的事情了。

下面我们将遵循着这一设计思路来编写一个最简单的切换过程——2个函数之间不停的互相切换,来验证任务切换过程中寄存器备份、恢复原理的正确性。

为了能看出任务切换的效果,我们设计2个函数TEST_TestTask1和TEST_TestTask2,这两个函数都是死循环,反复执行“打印消息—>延迟”的过程,我们可以通过打印信息来确认是哪个函数在执行,伪码如下:

TEST_TestTask1:

TEST_TestTask2:

while(1)

{

    打印“Task1 is running!”;

    延迟时间1秒;

}

while(1)

{

    打印“Task2 is running!”;

    延迟时间2秒;

}

如果没有函数切换功能,那么这样的函数只要一开始执行,它们就会一直死循环执行下去,不会给其它函数执行的机会,我们就只能看到只有一个函数在循环打印消息。如果能够按照上面是所讲述的切换原理发生函数切换,那么我们就应该能看到的是这2个函数是在循环交替打印。

现在我们需要一个函数,它具有备份、恢复这17个寄存器的功能,在TEST_TestTask1和TEST_TestTask2需要切换时就调用它,完成上下文切换。我们将这个函数命名为WLX_TaskSwitch,我们将WLX_TaskSwitch函数加入到TEST_TestTask1和TEST_TestTask2里:

TEST_TestTask1:

TEST_TestTask2:

while(1)

{

    打印“Task1 is running!”;

    延迟时间1秒;

    WLX_TaskSwitch();

}

while(1)

{

    打印“Task2 is running!”;

    延迟时间2秒;

    WLX_TaskSwitch();

}

我们将WLX_TaskSwitch函数设计为一个C函数,它仅对一些全局变量赋值,这些全局变量用来指明切换前函数的相关信息和切换后函数的相关信息。至于寄存器组备份、恢复的具体过程,由于是涉及到操作寄存器,因此只能使用汇编语言编写,将这个过程封装到由汇编语言编写的WLX_ContextSwitch函数里面来实现。

我们将使用C语言和汇编语言编写操作系统。C语言作为高级语言具有较好的可移植性,并且控制硬件方便,在嵌入式领域有极广泛的应用,但无法直接控制芯片的寄存器。汇编语言是与芯片内部硬件息息相关的,可控制寄存器,但编码困难,可移植性差。因此,本手册本着尽可能使用C语言的原则,在C语言无法实现或实现成本太大的情况下才使用汇编语言。

现在我们先来看看WLX_TaskSwitch函数,最左侧的5位数字是代码在源代码文件里的行号。Wanlix和Mindows的全部代码都可以从http://blog.sina.com.cn/ifreecoding网站免费下载,也可在网站内部的论坛上讨论。

00060  void WLX_TaskSwitch(void)

00061  {

00062      if(1 == guiCurTask)

00063      {

00064          

00065          gpuiCurTaskSpAddr = &guiTask1CurSp;

00066  

00067          

00068          guiNextTaskSp = guiTask2CurSp;

00069  

00070          

00071          guiCurTask = 2;

00072      }

00073      else //if(2 == guiCurTask)

00074      {

00075          gpuiCurTaskSpAddr = &guiTask2CurSp;

00076  

00077          guiNextTaskSp = guiTask1CurSp;

00078  

00079          guiCurTask = 1;

00080      }

00081  

00082      

00083      WLX_ContextSwitch();

00084  }

在这个函数里,我们用到了guiCurTask、guiTask1CurSp、guiTask2CurSp、gpuiCurTaskSpAddr、guiNextTaskSp这5个全局变量。guiCurTask用来指示当前运行的任务,在1和2之间不断变化。guiTask1CurSp保存的是TEST_TestTask1函数的寄存器组存储的内存地址,uiTask2CurSp保存的是TEST_TestTask2函数的寄存器组存储的内存地址,寄存器组备份、恢复时用的就是这两个全局变量指向的内存空间。gpuiCurTaskSpAddr用来存放guiTask1CurSp或guiTask2CurSp的地址,需要备份寄存器组的任务将指向它的寄存器组内存空间的变量的地址放入gpuiCurTaskSpAddr全局变量。guiNextTaskSp存放的是guiTask1CurSp或guiTask2CurSp,需要恢复寄存器组的任务将它的寄存器组内存空间地址放入guiNextTaskSp全局变量。这样,MDS_ContextSwitch函数在寄存器组备份、恢复时就可以通过gpuiCurTaskSpAddr和guiNextTaskSp分别找到备份和恢复寄存器组的内存空间了。

下面详细解释一下WLX_TaskSwitch函数:

00062行,对任务进行判断,如果当前运行的是任务1则进入此分支。

00065行,运行到此行,说明当前运行的是任务1,需要备份任务1的寄存器组数据,将任务1的全局变量guiTask1CurSp的地址存入全局变量gpuiCurTaskSpAddr中,准备供MDS_ContextSwitch函数使用。

00068行,需要还原任务2的寄存器组数据,将任务2的全局变量guiTask2CurSp保存到全局变量guiNextTaskSp中,准备供WLX_ContextSwitch函数使用。

00071行,准备从任务1切换到任务2,将保存当前运行任务ID的全局变量guiCurTask更新为将要运行的任务2。

00073~00080行与00062~00072行功能类似,不通的是从任务2切换到任务1的过程。

00083行,任务切换前的准备工作已经完成,调用WLX_ContextSwitch函数,开始寄存器组备份、恢复。

在介绍WLX_ContextSwitch函数之前,我们先设计一下保存寄存器组的内存结构,如图13所示:

图 13  寄存器组在内存中的结构

寄存器组中每个写着寄存器名字的位置用来保存对应的寄存器,可以保存R0~R14和CPSR寄存器,但没有为R15进行备份,这是因为Wanlix的切换过程是由函数主动调用WLX_ContextSwitch函数实现的,是写死在代码里的,在编译的时候编译器就会安排代码,在任务切换前将R15自动保存在R14中,这样我们只需要备份R14就足够了。

    这个寄存器组的位置存放在函数的栈中,当备份时,就从当前函数的SP栈指针指向的地址向栈增长的方向依次将寄存器存入,并更新SP栈指针,使之指向寄存器组中的“CPSR”,这也是一个压栈的过程,只不过是借用了函数的栈空间。当恢复时,从SP栈指针指向的寄存器组中依次恢复寄存器,并将SP栈指针恢复到函数切换前的所在位置,完成任务上下文切换。这时已经恢复的寄存器组所在的内存数据变为无效数据,当前函数运行时可能会压栈覆盖掉此空间的数据。

图 14  寄存器组在栈中的位置

    在这里还需要说明一下系统栈和任务栈的概念。软件在刚启动时都是运行在没有操作系统的环境下,这时候所有函数都会使用同一个栈(ARM7中中断模式除外),这个栈就叫做系统栈。启动操作系统后,程序就会以任务为功能单元运行,在建立任务时需要为每个任务分配一个栈供任务运行时使用,这个栈就称之为任务栈。软件进入操作系统后就不使用系统栈了,完全使用任务栈。由于操作系统不会重新返回到非操作系统状态下,因此系统栈中保存的数据也没有用处了,用户可以将系统栈的内存空间拿来另作它用。


 

图 15  系统栈和任务栈

有了前面的铺垫,我们来看看操作系统内核最核心的函数WLX_ContextSwitch:

00012      .func WLX_ContextSwitch

00013  WLX_ContextSwitch:

00014  

00015      @保存当前任务的堆栈信息

00016      STMDB  R13, {R0-R14}

00017      SUB    R13, R13, #0x3C

00018      MRS    R0, CPSR

00019      STMDB  R13!, {R0}

00020  

00021      @保存当前任务的指针值

00022      LDR    R0, =gpuiCurTaskSpAddr

00023      LDR    R1, [R0]

00024      CMP    R1, #0

00025      BEQ    GETNEXTTASKSP

00026      STR    R13, [R1]

00027  

00028  GETNEXTTASKSP:

00029      @获取将要运行任务的堆栈信息并运行新任务

00030      LDR    R0, =guiNextTaskSp

00031      LDR    R13, [R0]

00032      LDMIA  R13!, {R0}

00033      MSR    CPSR, R0

00034      LDMIA  R13, {R0-R14}

00035      BX     R14

00036  

00037      .endfunc

00012行,定义WLX_ContextSwitch函数。

00013行,这是一条汇编伪指令,只是一个标号,代表WLX_ContextSwitch函数的起始地址,并不生成可执行代码。

00015行,在GNU环境的汇编语言里,“@”符号代表注释符,编译时其后的所有字符都被注释掉,不生成任何代码,其存在只为程序提供说明性帮助。

00016行,这是该函数的第一条可执行语句,它将当前任务的R0-R14寄存器保存到寄存器组中,寄存器组的地址由SP寄存器指定。

00017行,更新数据入栈后的SP栈指针。

00018行,将当前任务的CPSR寄存器保存到R0中。

00019行,将R0寄存器存入寄存器组,也就是把CPSR寄存器的内容存入寄存器组,并更新SP寄存器。00016~00019行所使用的汇编指令并不会改变CPSR寄存器的内容,因此00019行这条指令保存的R0就是当前任务进入WLX_ContextSwitch函数前的CPSR寄存器的数值。

00022行,获取全局变量gpuiCurTaskSpAddr的地址。

00023行,获取全局变量gpuiCurTaskSpAddr的内容,也就是存放当前任务的寄存器组的全局变量guiTaskXCurSp(当前任务为1则guiTaskXCurSp为guiTask1CurSp,当前任务为2则则guiTaskXCurSp为guiTask2CurSp)。

00024行,将gpuiCurTaskSpAddr全局变量的内容与0做比较。在非操作系统状态下全局变量gpuiCurTaskSpAddr为0,无需保存寄存器组数据。

00025行,如果gpuiCurTaskSpAddr全局变量为0,则跳转到GETNEXTTASKSP标志所在地址,准备进入操作系统状态,但还没有任务在运行,因此,不需要保存非操作系统状态下的SP栈指针。如果不为0则代表已经进入操作系统状态,不执行本行,执行00026行。

00026行,将当前任务的SP栈指针存入当前任务的guiTaskXCurSp全局变量中,下次运行时可借此找到任务的栈指针,并根据栈指针从寄存器组中恢复出寄存器的数值。

00030行,获取全局变量guiNextTaskSp的地址。

00031行,获取全局变量guiNextTaskSp的内容,也就是存放将要运行任务的寄存器组的地址,将寄存器组的地址存入SP中。

00032行,根据SP从寄存器组中恢复将要运行任务的CPSR寄存器,恢复到R0中。

00033行,将R0保存到CPSR中,也就是恢复了将要运行任务的CPSR寄存器。

00034行,恢复R0-R14寄存器,其中包含了SP寄存器,因此不再需要额外更新SP寄存器。

00035行,跳转到将要运行的任务。至此,已经完成了2个任务的寄存器出入栈工作,芯片当前工作的寄存器已经从切换前运行的任务全部换成了切换后将要运行的任务,寄存器数据已经全部处理完了,只要能将PC指针正确跳到将要运行任务上次切出去那一时刻的位置就可以了。此时LR寄存器中保存的就是将要运行任务的上次切出去的地址,跳转到LR,完成2个任务切换的最后一步。

上面实现了任务的切换过程,但是还有一些事情需要解决,那就是测试函数TEST_TestTask1和 TEST_TestTask2第一次运行时,栈中的寄存器组是空的,无法进行寄存器组恢复,也就无法切换了。因此,我们需要一个初始化函数,来为第一次运行的任务初始化它的寄存器组栈空间。这个任务初始化函数是WLX_TaskInit,它只需要在本任务的栈内为寄存器组赋初值就可以了,至于每个寄存器应该赋什么初值,我们可以分析一下。

由于任务是第一次运行,所有一切都是空的,函数不会从R0~R12寄存器读数据使用,因此可以将这些寄存器全部置0。SP是栈指针,所以需要将任务的栈顶赋给SP。LR是返回地址,需要通过跳转到LR去第一次执行这个函数,而函数名就是函数的第一条指令所在的地址,函数名是指针,因此,将函数名存入存入LR中。

具体实现过程来看下面来看代码:

00020  void WLX_TaskInit(U8 ucTask, VFUNC vfFuncPointer, U32* puiTaskStack)

00021  {

00022      U32* puiSp;

00023  

00024      

00025      puiSp = puiTaskStack;               

00026  

00027      *(--puiSp) = (U32)vfFuncPointer;    

00028      *(--puiSp) = (U32)puiTaskStack;     

00029      *(--puiSp) = 0;                     

00030      *(--puiSp) = 0;                     

00031      *(--puiSp) = 0;                     

00032      *(--puiSp) = 0;                     

00033      *(--puiSp) = 0;                     

00034      *(--puiSp) = 0;                     

00035      *(--puiSp) = 0;                     

00036      *(--puiSp) = 0;                     

00037      *(--puiSp) = 0;                     

00038      *(--puiSp) = 0;                     

00039      *(--puiSp) = 0;                     

00040      *(--puiSp) = 0;                     

00041      *(--puiSp) = 0;                     

00042      *(--puiSp) = MODE_USR;              

00043  

00044      

00045      if(1 == ucTask)

00046      {

00047          guiTask1CurSp = (U32)puiSp;

00048      }

00049      else //if(2 == ucTask)

00050      {

00051          guiTask2CurSp = (U32)puiSp;

00052      }

00053  }

00020行,函数定义,ucTask入口参数确定需要初始化的函数;vfFuncPointer入口参数是需要初始化任务的函数;puiTaskStack入口参数是这个任务所使用的任务栈指针,需要是栈顶满栈指针。

00025行,将puiSp变量指向任务栈栈顶。

00027行,将函数指针存入寄存器组的LR位置。当该任务第一次运行,恢复寄存器时,该函数指针就会被恢复到LR寄存器中,当程序跳转到LR时也就开始运行该函数了。

00028行,将任务栈栈顶地址存入寄存器组的SP位置。当该任务第一次运行时,栈内初始化的数据全部取出后,任务栈已经空了,此时SP应该指向栈顶,这时候函数才开始运行。将任务栈栈顶地址存入SP中,当该任务第一次运行,恢复寄存器时,任务栈栈顶地址就会被恢复到SP寄存器中,该任务就会从SP所指的地址开始存放栈数据,也就到达了控制任务栈的目的。

00029~00041行,任务刚创建时R0~R12寄存器中数据为无效值,因此此处全部填0。

00042行,将USR模式存入寄存器组的CPSR位置。这样当该任务第一次运行时,USR模式就会被恢复到CPSR寄存器中,任务就会从USR模式开始启动。MODE_USR是一个宏定义,其值为0x10,可以参考图5,代表USR模式。函数第一次运行时CPSR里面NZCV等各种状态均为0,因此此处CPSR寄存器中的状态位均被初始化为0。

00045~00052行,将每个任务的栈信息保存到它对应的全局变量中,供任务切换时使用。

创建任务所使用的函数都是通过WLX_TaskInit函数以这种隐式的方式开始运行的,并没有直接调用。在创建任务时就确定了任务所使用的根函数以及栈等其它信息,后续随着我们对操作系统的不断完善,还会有更多的信息被加入到任务中来,这也是任务不同于函数的地方,任务比函数拥有更多的信息,这样,操作系统才可以利用这些信息更方便的管理任务调度。

最后需要使用WLX_TaskStart函数从非操作系统状态开始进入到操作系统状态。WLX_TaskStart函数很简单,就是对上面介绍过的全局变量做一些初始化,然后调用任务切换函数WLX_TaskSwitch,WLX_TaskSwitch函数再调用WLX_ContextSwitch函数,将任务初始化函数WLX_TaskInit初始化的参数恢复到寄存器中开始运行,开始了第一个任务的运行,这样就进入到了操作系统状态。

WLX_TaskStart函数很简单,不再详细解释,代码如下:

00091  void WLX_TaskStart(void)

00092  {

00093      

00094      gpuiCurTaskSpAddr = (U32*)NULL;

00095  

00096      

00097      guiNextTaskSp = guiTask1CurSp;

00098  

00099      

00100      guiCurTask = 1;

00101  

00102      WLX_ContextSwitch();

00103  }

现在我们已经完成任务切换所需要的全部代码,在main函数里首先初始化硬件,然后调用WLX_TaskInit函数对2个任务进行初始化,最后调用WLX_TaskStart函数启动任务调度,这2个任务就开始交替执行了,交替向串口打印数据。 

00014  S32 main(void)

00015  {

00016      

00017      DEV_HardwareInit();

00018  

00019      

00020      WLX_TaskInit(1, TEST_TestTask1, TEST_GetTaskInitSp(1));

00021      WLX_TaskInit(2, TEST_TestTask2, TEST_GetTaskInitSp(2));

00022  

00023      

00024      WLX_TaskStart();

00025  

00026      return 0;

00027  }

测试函数TEST_TestTask1和 TEST_TestTask2运行时函数调用关系如下:

—>TEST_TestTask1

  —>WLX_TaskSwitch

    —>WLX_ContextSwitch

      —> TEST_TestTask2

        —>WLX_TaskSwitch

          —>WLX_ContextSwitch

            —>TEST_TestTask1

              —>……

我们来看看最终的效果:

图 16  两个任务交替执行

通过图16我们可以看到这两个任务交替的运行,在代码里我们并没有直接运行TEST_TestTask1和TEST_TestTask2函数,而是采用操作系统创建任务、切换任务的原理运行这两个函数的。从串口工具的输出来看,已经完全实现了我们的设计!

读者还可以观看串口输出的视频,请登陆ifreecoding_新浪博客网站下载,该视频动态的记录了两个任务在串口工具上输出的过程,可以看到TEST_TestTask1任务执行1秒后切换到TEST_TestTask2任务,TEST_TestTask2任务执行2秒后切换到TEST_TestTask1任务,如此循环。

还有一点需要说明,某些函数具有返回值,但我并没有完全判断这些返回值,当不影响软件功能时我一般是采用viod屏蔽了函数返回值,以突出本手册介绍的重点。但我建议,如果你是在做一个项目的话,最好能判断函数的返回值,以增强系统的健壮性。

//1.mesh构建 using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer), typeof(MeshCollider))] public class Potteryprototype : MonoBehaviour { &nbsp;&nbsp;&nbsp; MeshFilter meshFilter; &nbsp;&nbsp;&nbsp; MeshRenderer meshRenderer; &nbsp;&nbsp;&nbsp; MeshCollider meshCollider; &nbsp;&nbsp;&nbsp; Mesh mesh; &nbsp; &nbsp;&nbsp;&nbsp; public int details = 40; &nbsp;&nbsp;&nbsp; public int layer = 20; &nbsp;&nbsp;&nbsp; public float Height = 0.1f; &nbsp; &nbsp;&nbsp;&nbsp; public float OuterRadius = 1.0f; &nbsp;&nbsp;&nbsp; public float InnerRadius = 0.9f; &nbsp; &nbsp;&nbsp;&nbsp; List<Vector3> vertices; &nbsp;&nbsp;&nbsp; List<Vector2> UV; &nbsp;&nbsp;&nbsp; List<int> triangles; &nbsp; &nbsp;&nbsp;&nbsp; float EachAngle ; &nbsp;&nbsp;&nbsp; int SideCount; &nbsp; &nbsp;&nbsp;&nbsp; public MouseControl mouse; &nbsp; &nbsp;&nbsp;&nbsp; void Start() &nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshFilter = GetComponent<MeshFilter>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshCollider = GetComponent<MeshCollider>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshRenderer = GetComponent<MeshRenderer>(); &nbsp;&nbsp;&nbsp; } &nbsp; &nbsp;&nbsp;&nbsp; [ContextMenu("GeneratePottery")] &nbsp;&nbsp;&nbsp; void GeneratePrototype() &nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; vertices = new List<Vector3>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles = new List<int>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; UV = new List<Vector2>(); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; EachAngle = Mathf.PI * 2 / details; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (int i = 0; i < layer; i++) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; GenerateCircle(i); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Capping(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh = new Mesh(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.vertices = vertices.ToArray(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.triangles = triangles.ToArray(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.uv = UV.ToArray(); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.RecalculateBounds(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.RecalculateTangents(); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshFilter.mesh = mesh; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.RecalculateNormals(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshCollider.sharedMesh = mesh; &nbsp;&nbsp;&nbsp; } &nbsp; &nbsp;&nbsp;&nbsp; void GenerateCircle(int _layer) &nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //外顶点与内顶点分开存储,方便变化操作时的计算 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; List<Vector3> vertices_outside = new List<Vector3>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; List<Vector3> vertices_inside = new List<Vector3>(); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; List<Vector2> UV_outside = new List<Vector2>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; List<Vector2> UV_inside = new List<Vector2>(); &nbsp; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //外侧和内侧顶点计算 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //注意这里让每一圈的首尾重合了,也就是开始和结尾的顶点坐标一致 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //目的是计算UV坐标时不会出现空缺 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (float i = 0; i <= Mathf.PI * 2+EachAngle; i += EachAngle) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 v1 = new Vector3(OuterRadius * Mathf.Sin(i),&nbsp; _layer * Height, OuterRadius * Mathf.Cos(i)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 v2 = new Vector3(OuterRadius * Mathf.Sin(i),&nbsp; (_layer +1)* Height, OuterRadius * Mathf.Cos(i)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 v3 = new Vector3(InnerRadius * Mathf.Sin(i),&nbsp; _layer * Height, InnerRadius * Mathf.Cos(i)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 v4 = new Vector3(InnerRadius * Mathf.Sin(i),&nbsp; (_layer+1) * Height, InnerRadius * Mathf.Cos(i)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; vertices_outside.Add(v1); vertices_outside.Add(v2); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; vertices_inside.Add(v3); vertices_inside.Add(v4); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector2 uv1 = new Vector2(i / Mathf.PI*2, _layer*1.0f / layer * 1.0f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector2 uv2 = new Vector2(i / Mathf.PI*2, (_layer + 1)*1.0f / layer * 1.0f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector2 uv3 = new Vector2(i / Mathf.PI*2, _layer*1.0f / layer * 1.0f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector2 uv4 = new Vector2(i / Mathf.PI*2, (_layer + 1) *1.0f/ layer * 1.0f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; UV_outside.Add(uv1); UV_outside.Add(uv2); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; UV_inside.Add(uv3); UV_inside.Add(uv4); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; vertices.AddRange(vertices_outside); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; vertices.AddRange(vertices_inside); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; UV.AddRange(UV_outside); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; UV.AddRange(UV_inside); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; SideCount = vertices_outside.Count; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int j = vertices_outside.Count * _layer * 2; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int n = vertices_outside.Count; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (int i = j; i < j + vertices_outside.Count - 2; i += 2) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(i); triangles.Add(i + 2); triangles.Add(i + 1); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(i + 2); triangles.Add(i + 3); triangles.Add(i + 1); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(i + n); triangles.Add(i + n + 1); triangles.Add(i + n + 2); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(i + n + 2); triangles.Add(i + n + 1); triangles.Add(i + n + 3); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp; //封顶,底面由于看不见就不用管了 &nbsp;&nbsp;&nbsp; void Capping() &nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (float i = 0; i <= Mathf.PI * 2+EachAngle; i += EachAngle) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 outer = new Vector3(OuterRadius * Mathf.Sin(i),layer * Height, OuterRadius * Mathf.Cos(i)); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 inner= new Vector3(InnerRadius * Mathf.Sin(i), layer * Height, InnerRadius * Mathf.Cos(i)); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vertices.Add(outer);vertices.Add(inner); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector2 uv1 = new Vector2(i / Mathf.PI * 2,0); Vector2 uv2 = new Vector2(i / Mathf.PI * 2, 1); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; UV.Add(uv1); UV.Add(uv2); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int j = SideCount * layer * 2; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (int i=j;i<vertices.Count-2;i+=2) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(i);triangles.Add(i + 3);triangles.Add(i + 1); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(i);triangles.Add(i + 2);triangles.Add(i + 3); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(vertices.Count - 2);triangles.Add(j + 1);triangles.Add(vertices.Count - 1); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; triangles.Add(vertices.Count - 2);triangles.Add(j);triangles.Add(j + 1); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; } } &nbsp; //2.动态改变形状 //这个函数放在Update()里调用 &nbsp;&nbsp;&nbsp; void GetMouseControlTransform() &nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //从屏幕鼠标位置发射一条射线到模型上,获取这个坐标 &nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; RaycastHit info; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (Physics.Raycast(ray.origin, ray.direction, out info)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //在Unity中无法直接修改MeshFilter中Mesh的信息,需要新建一个Mesh修改其引用关系 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Mesh mesh = meshFilter.mesh; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3[] _vertices = mesh.vertices; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (int i = 0; i < _vertices.Length; i++) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //x,z平面变换 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //顶点移动与Y值的关系限制在5倍单层高度 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //这里可以自行修改,限制高度越大,曲线越平滑 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;if (Mathf.Abs(info.point.y - transform.TransformPoint(_vertices[i]).y) < (5 * Height)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //计算顶点移动方向的向量 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Vector3 v_xz = (transform.TransformPoint(_vertices[i]) - new Vector3(transform.position.x, transform.TransformPoint(_vertices[i]).y, transform.position.z)); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //外顶点与内顶点移动时相对距离应该保持不变 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //因为我们知道顶点数组内的顺序关系,所以可以通过计算总顶点数除以每层单侧顶点数的商的奇偶关系来判断是外顶点还是内顶点 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int n = i / SideCount; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bool side = n % 2 == 0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //判断顶面顶点内外关系 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bool caps = (i - (SideCount * layer * 2)) % 2 == 0; &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //限制每个顶点最大和最小的移动距离 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; float max; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; float min; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (i < SideCount * layer * 2) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; max = side ? 2f * OuterRadius : 2f * OuterRadius - (OuterRadius - InnerRadius); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; min = side ? 0.5f * OuterRadius : 0.5f * OuterRadius - (OuterRadius - InnerRadius); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; max = caps ? 2f * OuterRadius : 2f * OuterRadius - (OuterRadius - InnerRadius); ; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; min = caps ? 0.5f * OuterRadius : 0.5f * OuterRadius - (OuterRadius - InnerRadius); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //计算当前顶点到鼠标Y值之间的距离,再用余弦函数算出实际位移距离 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; float dif = Mathf.Abs(info.point.y - transform.TransformPoint(_vertices[i]).y); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (Input.GetKey(KeyCode.RightArrow)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; float outer = max - v_xz.magnitude; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _vertices[i] += v_xz.normalized * Mathf.Min(0.01f * Mathf.Cos(((dif / 5 * Height) * Mathf.PI) / 2), outer); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else if (Input.GetKey(KeyCode.LeftArrow)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; float inner = v_xz.magnitude - min; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; _vertices[i] -= v_xz.normalized * Mathf.Min(0.01f * Mathf.Cos(((dif / 5 * Height) * Mathf.PI) / 2), inner); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //Y轴变换 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; float scale_y = transform.localScale.y; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (Input.GetKey(KeyCode.UpArrow)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; scale_y = Mathf.Min(transform.localScale.y + 0.000001f, 2.0f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else if (Input.GetKey(KeyCode.DownArrow)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; scale_y = Mathf.Max(transform.localScale.y - 0.000001f, 0.3f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; transform.localScale = new Vector3(transform.localScale.x, scale_y, transform.localScale.z); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.vertices = _vertices; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.RecalculateBounds(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; mesh.RecalculateNormals(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshFilter.mesh = mesh; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; meshCollider.sharedMesh = mesh; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp; } } &nbsp; //计算时就把顶点坐标系转换为自身坐标系,求得向量后再转换为世界坐标系 &nbsp;&nbsp;&nbsp; Vector3 v_xz = transform.TransformDirection(transform.InverseTransformPoint(_vertices[i]) - transform.InverseTransformPoint(new Vector3(0, _vertices[i].y, 0))); &nbsp; //3.法线平均化 IEnumerator Print_Normals() &nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (int i = 0; i < meshFilter.mesh.vertices.Length; i++) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (i % 2 == 0) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Debug.DrawRay(transform.TransformPoint(meshFilter.mesh.vertices[i]), transform.TransformDirection(meshFilter.mesh.normals[i] * 0.3f), Color.green, 1000f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Debug.DrawRay(transform.TransformPoint(meshFilter.mesh.vertices[i]), transform.TransformDirection(meshFilter.mesh.normals[i] * 0.3f), Color.blue, 1000f); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; yield return new WaitForSeconds(Time.deltaTime); &nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp;&nbsp; } &nbsp; //回到项目上来。这段法线计算的代码就不放上来了,大致就是根据顶点在数组中的下标去判断位置是否相同,然后把该顶点的法线相加即可。大家自己构建Mesh时的顶点顺序可能会不太一样。 以上代码是否有问题,请帮我完善
最新发布
08-30
<think> 我们正在处理一个Unity项目,需要完善Mesh构建、动态改变形状和法线平均化的代码。 根据引用[1]和引用[2]: - 创建Mesh需要设置顶点、三角形、法线等。 - 法线信息是一个Vector3数组,每个顶点对应一个法线向量。 - 如果希望网格看起来平滑,需要对相邻面的法线进行平均(即平滑法线)。 - 引用[2]提到可以使用mesh.RecalculateNormals()自动计算法线,但它是根据相邻三角面计算平均法线,从而实现平滑效果。 然而,用户提到了“动态改变形状”和“法线平均化”,因此我们需要: 1. 动态更新Mesh的顶点(改变形状)。 2. 在顶点改变后,重新计算法线。我们可以使用RecalculateNormals(),但有时可能需要自定义法线计算(例如,实现特定的平均化方式)。 引用[2]中提到了一个偷懒的方法:mesh.RecalculateNormals(),它会自动计算每个顶点的法线(基于共享该顶点的所有三角面的法线平均值,并归一化)。 但如果我们想要自己计算法线(比如为了更精确的控制),则需要: - 首先计算每个三角面的法线(通过叉乘得到)。 - 然后对于每个顶点,收集所有共享该顶点的三角面的法线,求平均(并归一化)作为该顶点的法线。 考虑到动态改变形状,我们可能需要频繁更新法线,因此使用Unity内置的RecalculateNormals()可能是高效且方便的选择。但如果需要自定义(例如忽略某些面),则需要手动计算。 根据要求,我们提供两种方案: 方案1:使用Unity内置的RecalculateNormals()(简单,适用于一般平滑效果)。 方案2:手动计算平均法线(更灵活,但代码较多)。 我们先构建一个基本的Mesh构建类,包含: - 顶点数组(Vector3[] vertices) - 三角形数组(int[] triangles) - 法线数组(Vector3[] normals) - 初始化时构建网格并设置MeshFilter和MeshRenderer。 然后提供动态改变顶点的方法,并在改变后更新法线。 步骤: 1. 创建Mesh对象并设置顶点和三角形。 2. 计算法线(使用内置方法或自定义方法)。 3. 将网格应用于MeshFilter。 注意:动态改变形状时,我们可能会修改顶点数组,然后重新计算法线。 引用[3]提到法线对于渲染、照明等非常重要,所以正确设置法线很关键。 下面是一个示例类,包含: - 初始化网格(例如生成一个平面) - 修改顶点的方法 - 重新计算法线(可选择内置或自定义) 根据引用[2],我们也可以选择手动计算法线。这里我们提供两种方式,并通过一个标志位选择。 注意:手动计算法线需要遍历三角形,计算每个面的法线,然后对每个顶点的所有相邻面的法线求平均。 由于用户要求动态改变形状,我们假设顶点位置会变化,但拓扑结构(三角形连接)不变?或者也可能改变?这里我们先假设三角形索引不变,只有顶点位置变化。 如果拓扑结构也动态改变,那么三角形数组也需要更新,但法线计算同理。 代码结构: ```csharp using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class DynamicMesh : MonoBehaviour { private Mesh mesh; private Vector3[] vertices; private int[] triangles; private Vector3[] normals; // 是否使用Unity内置的法线计算(否则使用自定义计算) public bool useUnityRecalculateNormals = true; void Start() { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Dynamic Mesh"; // 初始化网格数据(例如创建一个平面) GenerateInitialMesh(); UpdateMesh(); } // 生成初始网格(示例:一个四边形平面) private void GenerateInitialMesh() { // 4个顶点 vertices = new Vector3[4]; vertices[0] = new Vector3(0, 0, 0); vertices[1] = new Vector3(1, 0, 0); vertices[2] = new Vector3(0, 1, 0); vertices[3] = new Vector3(1, 1, 0); // 两个三角形(顺时针或逆时针,注意Unity是顺时针为正面) triangles = new int[] { 0, 2, 1, 1, 2, 3 }; // 法线数组初始化为零,后面会计算 normals = new Vector3[4]; } // 更新网格(在修改顶点后调用) public void UpdateMesh() { mesh.Clear(); mesh.vertices = vertices; mesh.triangles = triangles; // 计算法线 if (useUnityRecalculateNormals) { mesh.RecalculateNormals(); // 从mesh中取回计算后的法线(如果我们后续需要访问) normals = mesh.normals; } else { normals = CalculateNormals(); mesh.normals = normals; } // 如果需要,也可以重新计算线等 } // 自定义法线计算:计算每个顶点的法线(通过相邻面的平均) private Vector3[] CalculateNormals() { // 初始化法线数组(每个顶点一个法线,初始为Vector3.zero) Vector3[] vertexNormals = new Vector3[vertices.Length]; // 遍历每个三角形(每3个索引一个三角形) for (int i = 0; i < triangles.Length; i += 3) { int index0 = triangles[i]; int index1 = triangles[i + 1]; int index2 = triangles[i + 2]; Vector3 v0 = vertices[index0]; Vector3 v1 = vertices[index1]; Vector3 v2 = vertices[index2]; // 计算三角形面的法线(叉乘) Vector3 normal = Vector3.Cross(v1 - v0, v2 - v0).normalized; // 将这个面的法线加到三个顶点的法线上 vertexNormals[index0] += normal; vertexNormals[index1] += normal; vertexNormals[index2] += normal; } // 归一化每个顶点的法线(得到平均方向) for (int i = 0; i < vertexNormals.Length; i++) { vertexNormals[i] = vertexNormals[i].normalized; } return vertexNormals; } // 示例:动态改变顶点位置(例如根据噪声或物理模拟) public void ModifyVertices() { // 这里只是一个示例:随机移动顶点(实际应用中可能是更复杂的形变) for (int i = 0; i < vertices.Length; i++) { vertices[i] += Random.insideUnitSphere * 0.1f; } // 更新网格 UpdateMesh(); } // 在Inspector中提供一个按钮来测试修改顶点 [ContextMenu("Modify Vertices")] public void ModifyVerticesContextMenu() { ModifyVertices(); } } ``` 说明: - 在Start中,我们创建初始网格(一个平面四边形),然后调用UpdateMesh来设置网格并计算法线。 - 我们提供了两种计算法线的方式:使用Unity内置的RecalculateNormals(默认)或自定义的CalculateNormals方法(遍历三角形,计算每个面的法线并累加到顶点,然后归一化)。 - 动态修改顶点后,调用UpdateMesh更新网格和法线。 注意: - 自定义法线计算中,我们假设网格是连续的(即共享顶点)。如果网格中有不连续的顶点(例如硬边),那么需要特殊处理(比如复制顶点),但这里我们只考虑平滑网格。 - 内置的RecalculateNormals方法也会考虑顶点共享的情况,所以对于平滑网格,使用内置方法即可。 用户可以根据需求选择使用内置方法还是自定义方法。如果希望实现硬边(不平滑),可能需要不同的顶点法线(例如,在硬边处,同一个位置有多个顶点,每个顶点属于不同的面,法线不同)。 关于动态改变形状:这里只是简单示例(随机移动顶点),实际应用中可能是根据物理模拟、函数变形等。 请根据实际项目需求调整初始网格的生成和顶点修改逻辑。 参考引用: [^1]: 引用1说明需要提供法线数组,并提到平滑法线需要平均相邻面的法线。 [^2]: 引用2提到可以使用mesh.RecalculateNormals()自动计算法线,并解释了其原理(计算与顶点相连的三角面法线的平均值并归一化)。 [^3]: 引用3强调了法线在渲染和光照中的重要性。 完善点: - 我们实现了动态改变顶点后更新法线(内置方法或自定义方法)。 - 我们提供了自定义法线计算的代码,以便用户需要更精细控制时使用。 如果用户需要硬边(非平滑)的效果,则需要复制顶点(使每个三角形使用独立的顶点),然后为这些顶点分别设置法线(即三角形面的法线)。但这不是当前问题的重点。 因此,以上代码应该能够满足用户对动态改变形状和法线平均化的需求。 §§相关问题§§ 1. 在Unity中,如何为具有硬边的网格计算法线? 2. 动态修改网格顶点时,如何优化性能? 3. 除了法线,动态网格还需要更新哪些属性以保证正确渲染? </think> 以下是针对 Unity 项目中 Mesh 构建、动态变形和法线平均化的代码检查与完善方案,整合了引用中的关键信息: ### 核心代码实现(含注释说明) ```csharp using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class DynamicMesh : MonoBehaviour { private Mesh mesh; private Vector3[] vertices; private Vector3[] normals; void Start() { // 初始化网格组件 GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Dynamic Mesh"; // 1. 基础网格构建(示例:平面) GenerateBaseMesh(); // 2. 动态更新顶点位置(示例:正弦波动) InvokeRepeating(nameof(UpdateShape), 0.1f, 0.1f); } // 生成基础网格(含UV和法线初始化) private void GenerateBaseMesh() { // 顶点设置(4个顶点构成平面) vertices = new Vector3[4] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(1, 1, 0) }; // 三角形索引(两个三角面) int[] triangles = new int[6] { 0, 2, 1, // 第一个三角形 1, 2, 3 // 第二个三角形 }; // UV坐标(必须设置) Vector2[] uv = new Vector2[4] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 1), new Vector2(1, 1) }; mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uv; // 3. 法线计算(平滑平均) CalculateSmoothNormals(); // 使用自定义平滑法线 // mesh.RecalculateNormals(); // 或使用Unity内置方法[^2] } //=== 关键完善点1:平滑法线计算 === private void CalculateSmoothNormals() { // 初始化法线数组(与顶点一一对应)[^1] normals = new Vector3[vertices.Length]; // 步骤1:计算每个三角面的法线 Vector3[] faceNormals = new Vector3[mesh.triangles.Length / 3]; for (int i = 0; i < mesh.triangles.Length; i += 3) { int idx0 = mesh.triangles[i]; int idx1 = mesh.triangles[i + 1]; int idx2 = mesh.triangles[i + 2]; Vector3 v1 = vertices[idx1] - vertices[idx0]; Vector3 v2 = vertices[idx2] - vertices[idx0]; faceNormals[i / 3] = Vector3.Cross(v1, v2).normalized; } // 步骤2:顶点法线平均化[^1] for (int i = 0; i < vertices.Length; i++) { Vector3 normalSum = Vector3.zero; int count = 0; // 遍历所有三角形,找到共享当前顶点的面 for (int j = 0; j < mesh.triangles.Length; j++) { if (mesh.triangles[j] == i) { int faceIndex = j / 3; normalSum += faceNormals[faceIndex]; count++; } } // 计算平均值并单位化[^3] normals[i] = (count > 0) ? (normalSum / count).normalized : Vector3.up; } mesh.normals = normals; // 应用法线 } //=== 关键完善点2:动态改变形状 === private void UpdateShape() { // 动态修改顶点位置(示例:Y轴正弦波动) for (int i = 0; i < vertices.Length; i++) { float wave = Mathf.Sin(Time.time + i * 0.5f); vertices[i].y = wave * 0.2f; // 振幅控制 } mesh.vertices = vertices; // 更新顶点 mesh.RecalculateBounds(); // 重计算边界 // 动态更新法线(必须执行!) CalculateSmoothNormals(); // 若使用内置方法:mesh.RecalculateNormals()[^2] } } ``` ### 关键完善说明 1. **法线平均化优化** - 手动实现相邻面法线加权平均(`CalculateSmoothNormals`方法) - 替代方案:直接调用 `mesh.RecalculateNormals()`(Unity 内置的面法线平均算法)[^2] - **注意**:两种方法都要求顶点位置更新后**必须重新计算法线**,否则光照错误 2. **动态变形要点** - 修改 `vertices` 数组后需重新赋值给 `mesh.vertices` - 必须调用 `RecalculateBounds()` 更新碰撞检测范围 - UV 坐标在初始化后无需修改(除非做纹理动画) 3. **性能优化** ```csharp // 在UpdateShape()开头添加(减少GC) mesh.MarkDynamic(); // 声明为动态网格[^2] // 避免每帧创建新数组 vertices = mesh.vertices; // 直接获取引用 ``` 4. **探针与光照兼容** ```csharp // 确保MeshRenderer正确响应光照 GetComponent<MeshRenderer>().lightProbeUsage = LightProbeUsage.BlendProbes; GetComponent<MeshRenderer>().reflectionProbeUsage = ReflectionProbeUsage.BlendProbes; ``` ### 验证要点 1. **法线方向验证** - 在Scene视图开启 **Face Culling**,确认法线朝向一致 - 使用Debug.DrawRay绘制顶点法线辅助检查 2. **平滑度测试** - 对比 `mesh.RecalculateNormals()` 和自定义算法的平滑效果 - 硬边处理需分割顶点(非平均法线) > 引用说明:[^1] 手动计算法线需顶点与法线一一对应;[^2] `RecalculateNormals()` 使用面法线平均值;[^3] 单位化法线保证光照计算正确。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值