经过【基础1~6】的介绍,相信大家对Grasshopper电池的开发的框架已经又有更深的了解了, 甚至已经在实现各种奇奇怪怪的 逻辑了。无论是在电池上修改原有功能还是增加新的功能,都是通过继承自GH_Component
类,再在其基础上要么override原来的默认实现,要么就是继承新的接口。
在这里对前面的内容小小地总结一下:一个自定义GH电池必须实现的overrides有
RegisterInputParams
// 用于注册输入参数RegisterOutputParams
// 用于注册输出参数SolveInstance
// 用于真正实现电池对输入参数的处理及提供输出参数的数据ComponentGuid
// 用于获得电池类的全局唯一标识id- 构造函数,必须实现
Name
、NickName
、Category
以及SubCategory
属性的赋值
那么,除了这些一个自定义GH电池必须要实现的自定义构造之外,我们自然而然地会去想还有什么方法可以override,这些方法能够帮助我们实现什么功能呢?
我们这次就来看看一对可以重写的函数:
- DrawViewportWires
- DrawViewportMeshes
它们是负责将GH电池中的数据展示在Rhino主界面的Viewport视口中的方法。在渲染帧时,GH文档会负责调用每个电池的这两个方法,来完成对电池内容的展示。比如我们在GH中组成的点、线、面会在选中时以绿色(未选中时以红色)渲染至Rhino视口中,这其实就归功于这两个方法。
不过,很多时候我们想要实现自己的显示逻辑,比如在某些生成的几何图形/视口上展示文字、提供额外几何图形预览,又或者是单纯地不想显示某些几何图形,这些都需要重写上述两个方法。
渲染的逻辑
在真正开始动手重写方法前,最重要的还是需要知道Grasshopper预览的底层原理。毕竟是要动手重改默认的实现,在不清楚原实现的前提下就贸然动手,这无异于放着地图不要一头扎进森林,很容易就迷失了。
下面是有关于Grasshopper渲染的几个基本要素:
- 渲染的请求是由 Rhino 的渲染通道发出的
- Rhino中视口画面的 每一帧都会发出一次渲染请求
- Grasshopper中几何数据的请求是由
GH_Document
统一管理和缓存需要渲染的数据 GH_Document
的缓存仅仅是缓存哪些电池需要渲染,对于具体渲染的内容不缓存GH_Document
会对每个需要渲染的电池跑一个大循环,调用上述提到的方法 (DrawViewportxxxx) 来完成渲染
渲染是一个对执行效率要求 特别高 的过程,可以这么简单地理解:我们平时使用的显示器一般都是60Hz刷新率,那么就是1秒60帧,所以留给每一帧渲染的时间仅有不到17ms的时间。于是,我们需要尽可能不要在渲染的过程中执行任何耗时的操作(例如一个大循环+各种复杂数据处理等),渲染过程最好只是数据的读取。
再重申一遍,不要在渲染方法里写任何耗时操作
在了解渲染的最基本的几个之后,DrawViewportWires
与 DrawViewportMeshes
又分别是干什么的呢?
Wires & Meshes
我们都见过Grasshopper右上角的几个按钮,其中有两个就是用来切换预览模式的(Shaded模式和WireFrame模式)
-
Shaded 模式
-
WireFrame 模式
可见,如果电池输出的预览包含面信息时,这两个模式渲染的内容是不一样的。在WireFrame模式仅仅会有轮廓线信息,而Shaded则是二者都有。在方法调用层面的情况如下:
- 如果渲染模式是 WireFrame ,则仅有
DrawViewportWires
被调用 - 如果渲染模式是 Shaded,则
DrawViewportWires
和DrawViewportMeshes
都会被调用 - 如果二者都被调用(即在 Shaded 渲染模式下),
DrawViewportWires
会先被调用
因此,要实现自定义预览,我们可以依据我们在对应选项中所展示的内容,把要预览的图形在相应的方法中实现即可 —— 预览的线条放入DrawViewportWires
,预览的面以及其他复杂的图形放入DrawViewportMeshes
。如此以来,对于显卡能力稍差的用户也能通过关闭Shaded渲染来获得较好的交互体验。
不过,仅仅实现这两个自定义方法,某些时候 是无法成功地在Rhino中显示预览图形的。这里提到的 某些时候 大部分时候是特指我们的电池没有输入/输出参数,或者所有参数为非图形数据的时候。终极原因就是GH_Component
中还自带了两个属性:
- IsPreviewCapable
- ClippingBox
IsPreviewCapable
前面提到了GH_Document
会对所有电池是否需要渲染进行一个缓存,这个IsPreviewCapable
属性就是用来判断电池是否会进入缓存列表中的。这个属性的默认实现是对电池的出口的所有参数进行遍历,如果发现其中有可以预览的内容时,就会返回一个 true 值来使得电池进入预览缓存。
这个属性的默认实现(在GH_Component
类中的实现)大概是这样的
public virtual bool IsPreviewCapable
{
get
{
foreach (var item in Params) // 对电池的每个参数遍历
{
if (item is IGH_PreviewObject obj
&&
obj.IsPreviewCapable)
// 如果电池的参数能够被预览
return true;
}
// 所有参数都不能被预览
return false;
}
}
如果我们的电池没有输入/输出参数,或者参数中并没有GH默认实现的预览几何数据类型(又是熟悉的GH_
开头的系列几何数据类型,例如GH_Line
等),则电池压根就不会进入预览缓存,自然就不会被渲染到Rhino视口中了。
所以,如果我们的电池需要有预览输出,但又没有任何输出参数/输出参数都不是GH_系列几何数据类型,那么我们就需要重写IsPreviewCapable
属性,让它永远返回 true 即可。
public override bool IsPreviewCapable => true;
ClippingBox
这个属性的意义在于通知渲染器 渲染的范围 有多大,对面渲染的意义重大(Mesh渲染)。
这个属性值是一个BoundingBox
结构体,其实就是一个三维的长方体盒子,可以使用最左下角与最右上角两个点构建。
一般而言,我们需要对需要渲染的内容的每一个渲染范围进行合并操作,比如说,这个属性的默认实现就是对所有输入/输出的参数都获取其 ClippingBox
属性并取并集:
public virtual BoundingBox ClippingBox
{
get
{
// 初始化
BoundingBox bdb = BoundingBox.Empty;
// 对输入/输出参数进行完全遍历
foreach (var item in Params)
{
if (item is IGH_PreviewObject obj
&&
// 该参数不隐藏
!obj.Hidden
&&
// 该参数支持预览
obj.IsPreviewCapable)
{
// 合并预览范围
bdb.Union(obj.ClippingBox);
}
}
return bdb;
}
}
所以,我们在使用时,如果有自己的一系列内容需要预览,则需要重写这部分的获取逻辑,否则就会在面的渲染上出现渲染不完整的情况。下面就是在默认实现的前提下再并入自己额外需要渲染的范围的一个简单例子
// 电池类中存储了额外的需要渲染的内容
List<IGH_PreviewObject> addtionalPreviewObjects;
public override BoundingBox ClippingBox
{
get
{
// 通过默认实现获得输入/输出参数的渲染范围
var bdb = base.ClippingBox;
// 对每个额外所需渲染的内容的范围取并集
foreach (var item in addtionalPreviewObjects)
bdb.Union(item.ClippingBox);
// 返回最终的渲染范围
return bdb;
}
}
下面的两个例子分别对应渲染范围正确及渲染范围不正确时,Rhino视口中的最终渲染效果。(一个球体,左图渲染范围不正确,右图正确)
实现渲染
上面说了一大堆,现在终于要说回正题了 —— 到底如何实现渲染。
经过上面关于渲染的要点的梳理,在电池中实现渲染的几个前置条件也变得相对清晰了,下面就直接列出来要如何实现渲染:
- 保证电池的
IsPreviewCapable
属性返回值为true - 保证
ClippingBox
属性返回正确的渲染范围 - 实现
DrawViewportWires
和DrawViewportMeshes
逻辑
其中,步骤1、2在前文已经详细阐述了,下面就来看看步骤3如何实现吧。由于步骤3中的两个方法十分的相似,后文就用DrawViewport
来简称这两个方法了。
重写 DrawViewport
其实重写DrawViewport
这两个方法与重写其它方法挺相似的,都是通过它给定的输入进行一些操作即可。当我们直接在Visual Studio中输入override时,聪明的IDE已经在提示我们有哪些方法可以重写了,此时选择想要重写的方法,按下Tab键就可以快速实现代码的插入了。
其中有一句
base.DrawViewportWires(args);
这句是用来完成默认实现的,而默认实现是“将输出参数能够被渲染的部分都渲染出来”。此时:
- 如果需求是“屏蔽掉电池原本的预览”,那么把这行删掉/注释掉;
- 如果还想保留原来电池对输出参数的渲染功能,则这行还是需要留下。
接下来就是实现自己额外的渲染逻辑了:
- 通过 args.
Display
属性获取到Rhino的渲染管道 - 使用Rhino渲染管道实现自定义渲染
args.Display
属性中有一系列 “Draw” 开头的函数,包括但并不限于
- DrawArc
- DrawArrow
- DrawLine
- DrawLineArrow
- … … … …
使用这一系列函数完成自己的自定义渲染即可!比如,下面的代码就可以在原点画一个蓝色的正方形的点:
public override void DrawViewportWires(IGH_PreviewArgs args)
{
base.DrawViewportWires(args);
// 在原点处画一个正方形的点
args.Display.DrawPoint(Point3d.Origin, Rhino.Display.PointStyle.Square, 3, Color.DarkBlue);
}
在这里,重要的事情说三遍,再次重申一下 执行效率的重要性。
Rhino中每一帧的渲染都是会调用DrawViewport
方法的,如果在这个方法里面使用耗时的操作,就会导致渲染帧数的剧烈下降。比如我们在这里插入一个Thread.Sleep(100)
来模拟耗时操作。
public override void DrawViewportWires(IGH_PreviewArgs args)
{
base.DrawViewportWires(args);
// 在原点处画一个正方形的点
args.Display.DrawPoint(Point3d.Origin, Rhino.Display.PointStyle.Square, 3, Color.DarkBlue);
Thread.Sleep(100);
}
这行必然会导致Rhino在渲染时掉到10帧以下(1秒是1000ms,1000ms / 100ms = 10帧,再加上其他预处理,每帧的渲染时间必然会超过100ms,因此渲染的帧率就在10以下)。为了做这测试我还特意下了个帧率监测软件,请注意图中左下角的渲染帧数。
果然仅有 9帧/秒。当我们把Thread.Sleep(100)
注释掉之后
直接回升至60帧/秒,十分丝滑。
因为gif录制问题,在图中可能看不出来,后文有源码,请各位读者自行尝试。
可见渲染的执行效率的影响力。
于是,顺着这个思路,如果输出参数的数据量巨大,我们也可以有选择性地屏蔽掉GH默认的渲染实现,来提高渲染效率,提供更好的体验。
自定义渲染的内容就暂时介绍到这里了,其中许多关于渲染的内容还未展开,比如视口渲染与三维模型渲染的区别等,欢迎读者们自行探索。
本文最后帧率对比的源码附在最后。
using Grasshopper.Kernel;
using Rhino.Display;
using Rhino.Geometry;
using System;
using System.Drawing;
using System.Threading;
namespace DigitalCrab.Grasshopper
{
public class PREVIEW : GH_Component
{
public override Guid ComponentGuid => throw new NotImplementedException("请自行申请GUID并替换");
public PREVIEW()
: base("PreviewFPS", "FPS",
"",
"Params", "DigitalCrab") {}
protected override void RegisterInputParams(GH_InputParamManager pManager)
{}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{}
protected override void SolveInstance(IGH_DataAccess DA)
{}
// 自定义预览相关
public override bool IsPreviewCapable => true;
public override BoundingBox ClippingBox => new BoundingBox(-1, -1, -1, 1, 1, 1);
public override void DrawViewportWires(IGH_PreviewArgs args)
{
base.DrawViewportWires(args);
args.Display.DrawPoint(Point3d.Origin, PointStyle.Square, 3, Color.DarkBlue);
Thread.Sleep(100); // 注释本行提高帧率
}
}
}