CSharpGL(35)用ViewPort实现类似3DMax那样的把一个场景渲染到4个视口
CSharpGL(35)用ViewPort实现类似3DMax那样的把一个场景渲染到4个视口
开始
像下面这样的四个视口的功能是很常用的,所以我花了几天时间在CSharpGL中集成了这个功能。
在CSharpGL中的多视口效果如下。效果图是粗糙了些,但是已经实现了拖拽图元时4个视口同步更新的功能,算是一个3D模型编辑器的雏形了。
原理
ViewPort
多视口的任务,是在不同的区域用不同的摄像机渲染同一个场景。这个“区域”我们称其为 ViewPort 。(实际上 ViewPort 是强化版的 glViewport() ,它附带了摄像机等其他成员)
为了渲染多个视口,就应该有一个 ViewPort 列表,保存所有的视口。这就是 Scene 里新增的RootViewPort属性。
public class Scene : IDisposable
{
/// <summary>
/// Root object of all viewports to be rendered in the scene.
/// </summary>
[Category(strScene)]
[Description("Root object of all viewports to be rendered in the scene.")]
[Editor(typeof(PropertyGridEditor), typeof(UITypeEditor))]
public ViewPort RootViewPort { get; private set; }
// other stuff …
}
为了让视口也能像UIRenderer那样使用ILayout接口的树型布局功能,我们也让ViewPort实现ILayout接口。
public partial class ViewPort : ILayout<ViewPort>
{
private const string viewport = "View Port"; /// <summary>
///
/// </summary>
[Category(viewport)]
[Description("camera of the view port.")]
[Editor(typeof(PropertyGridEditor), typeof(UITypeEditor))]
public ICamera Camera { get; private set; } /// <summary>
/// background color.
/// </summary>
[Category(viewport)]
[Description("background color.")]
public Color ClearColor { get; set; } /// <summary>
/// Rectangle area of this view port.
/// </summary>
[Category(viewport)]
[Description("Rectangle area of this view port.")]
public Rectangle Rect { get { return new Rectangle(this.location, this.size); } } public ViewPort(ICamera camera, AnchorStyles anchor, Padding margin, Size size)
{
this.Children = new ChildList<ViewPort>(this); this.Camera = camera;
this.Anchor = anchor;
this.Margin = margin;
this.Size = size;
}
}
有了这样的设计,CSharpGL在渲染上述效果图时就有了5个视口。如下图所示,其中根结点上的ViewPort.Visible属性为false,表示这个ViewPort不会参与渲染,即不会显示到最终的窗口上。而此根结点下属的4个子结点,各自代表一个ViewPort,他们分别以Top\Front\Left\Perspecitve的角度渲染了一次整个场景,并将渲染结果放置到自己的范围内。
树型结构的ViewPort,其布局就和UIRenderer、Winform控件的布局方式是一样的。你可以像安排控件一样安排ViewPort的Location和Size。因此ViewPort是支持重叠、支持任意多个的。
渲染
有多少个ViewPort,就要渲染多少次。同时,ViewPort修改了glViewport()的值,这个情况也要反映到每个Renderer的渲染过程。
public partial class Scene
{
private object synObj = new object(); // Render this scene.
public void Render(RenderModes renderMode,
bool autoClear = true,
GeometryType pickingGeometryType = GeometryType.Point)
{
lock (this.synObj)
{
// update view port's location and size.
this.rootViewPort.Layout();
// render scene in every view port.
this.RenderViewPort(this.rootViewPort, this.Canvas.ClientRectangle, renderMode, autoClear, pickingGeometryType);
}
} // Render scene in every view port.
private void RenderViewPort(ViewPort viewPort, Rectangle clientRectangle, RenderModes renderMode, bool autoClear, GeometryType pickingGeometryType)
{
if (viewPort.Enabled)
{
// render in this view port.
if (viewPort.Visiable)
{
viewPort.On();// limit rendering area.
// render scene in this view port.
this.Render(viewPort, clientRectangle, renderMode, autoClear, pickingGeometryType);
viewPort.Off();// cancel limitation.
} // render children viewport.
foreach (ViewPort item in viewPort.Children)
{
this.RenderViewPort(item, clientRectangle, renderMode, autoClear, pickingGeometryType);
}
}
}
}
坐标系
再次强调一个问题,Winform的坐标系,是以左上角为(0, 0)原点的。OpenGL的窗口坐标系,是以左下角为(0, 0)原点的。
那么一个良好的习惯就是,通过Winform获取的鼠标坐标,应该第一时间转换为OpenGL下的坐标,然后再参与OpenGL的后续计算。等OpenGL部分的计算完毕时,应立即转换回Winform下的坐标。
保持这个好习惯,再遇到鼠标坐标时就不会有便秘的感觉了。
拾取
为了适应新出现的ViewPort功能,原有的Picking功能也要调整了。
之前没有ViewPort树的时候,其本质上是只有一个覆盖整个窗口的'ViewPort'。现在,新出现的ViewPort可能只覆盖窗口的一部分,那么拾取时也要修改为只在这部分内进行。
只在一个ViewPort内拾取
现在有了多个ViewPort。很显然,即使ViewPort之间有重叠,也只应在一个ViewPort内执行Picking操作。因为鼠标不会同时出现在2个地方。即使鼠标位于重叠的部分,也只应在最先(后序优先搜索顺序)接触到的ViewPort上执行Picking操作。
注意,这里先用 int y = clientRectangle.Height - mousePosition.Y - ; 得到了OpenGL坐标系下的鼠标位置,然后才开始OpenGL方面的计算。
public partial class Scene
{
/// <summary>
/// Get geometry at specified <paramref name="mousePosition"/> with specified <paramref name="pickingGeometryType"/>.
/// <para>Returns null when <paramref name="mousePosition"/> is out of this scene's area or there's no active(visible and enabled) viewport.</para>
/// </summary>
/// <param name="mousePosition">mouse position in Windows coordinate system.(Left Up is (0, 0))</param>
/// <param name="pickingGeometryType">target's geometry type.</param>
/// <returns></returns>
public List<Tuple<Point, PickedGeometry>> Pick(Point mousePosition, GeometryType pickingGeometryType)
{
Rectangle clientRectangle = this.Canvas.ClientRectangle;
// if mouse is out of window's area, nothing picked.
if (mousePosition.X < || clientRectangle.Width <= mousePosition.X || mousePosition.Y < || clientRectangle.Height <= mousePosition.Y) { return null; } int x = mousePosition.X;
int y = clientRectangle.Height - mousePosition.Y - ;
// now (x, y) is in OpenGL's window cooridnate system.
Point position = new Point(x, y);
List<Tuple<Point, PickedGeometry>> allPickedGeometrys = null;
var pickingRect = new Rectangle(x, y, , );
foreach (ViewPort viewPort in this.rootViewPort.DFSEnumerateRecursively())
{
if (viewPort.Visiable && viewPort.Enabled && viewPort.Contains(position))
{
allPickedGeometrys = ColorCodedPicking(viewPort, pickingRect, clientRectangle, pickingGeometryType); break;
}
} return allPickedGeometrys;
}
}
Picking的过程
Picking的步骤比较长,分支情况也超级多。这里只大体认识一下即可。
首先,如果depth buffer在鼠标所在的像素点上的深度为1(最深),就说明鼠标没有点中任何东西,因此直接返回即可。
然后,我们在给定的 ViewPort 范围内,用color-coded方式渲染一遍整个场景。
然后,用 glReadPixels() 获取鼠标所在位置的颜色值。
最后,由于这个颜色值是与图元的编号一一对应的,我们就可以通过这个颜色值辨认出它到底是属于哪个Renderer里的哪个图元。
/// <summary>
/// Pick primitives in specified <paramref name="viewPort"/>.
/// </summary>
/// <param name="viewPort"></param>
/// <param name="pickingRect">rect in OpenGL's window coordinate system.(Left Down is (0, 0)), size).</param>
/// <param name="clientRectangle">whole canvas' rectangle.</param>
/// <param name="pickingGeometryType"></param>
/// <returns></returns>
private List<Tuple<Point, PickedGeometry>> ColorCodedPicking(ViewPort viewPort, Rectangle pickingRect, Rectangle clientRectangle, GeometryType pickingGeometryType)
{
var result = new List<Tuple<Point, PickedGeometry>>(); // if depth buffer is valid in specified rect, then maybe something is picked.
if (DepthBufferValid(pickingRect))
{
lock (this.synObj)
{
var arg = new RenderEventArgs(RenderModes.ColorCodedPicking, clientRectangle, viewPort, pickingGeometryType);
// Render all PickableRenderers for color-coded picking.
List<IColorCodedPicking> pickableRendererList = Render4Picking(arg);
// Read pixels in specified rect and get the VertexIds they represent.
List<Tuple<Point, uint>> stageVertexIdList = ReadPixels(pickingRect);
// Get all picked geometrys.
foreach (Tuple<Point, uint> tuple in stageVertexIdList)
{
int x = tuple.Item1.X;
int y = tuple.Item1.Y; uint stageVertexId = tuple.Item2;
PickedGeometry pickedGeometry = GetPickGeometry(arg,
x, y, stageVertexId, pickableRendererList);
if (pickedGeometry != null)
{
result.Add(new Tuple<Point, PickedGeometry>(new Point(x, y), pickedGeometry));
}
}
}
} return result;
}
ColorCodedPicking in view port.
这其中包含了太多的细节,关键详情可参看这6篇介绍(这里,这里,这里,这里,这里,还有这里)
自定义布局方式
虽然ViewPort实现了ILayout接口,但是这难以完成按比例布局的功能。(即:当窗口Size改变时,Top\Front\Left\Perspective始终保持各占窗口1/4大小)
这时可以通过自定义布局的方式来实现这个功能。
具体方法就是自定义 ViewPort.BeforeLayout 和 ViewPort.AfterLayout 事件。
例如,对于Top,我们想让它始终保持在窗口的左上角,且占窗口1/4大小。
private void Form_Load(object sender, EventArgs e)
{
// other stuff ...
// ‘top’ view port
var camera = new Camera(
new vec3(, , ), new vec3(, , ), new vec3(, , ),
CameraType.Perspecitive, this.glCanvas1.Width, this.glCanvas1.Height);
ViewPort viewPort = new ViewPort(camera, AnchorStyles.None, new Padding(), new Size());
viewPort.BeforeLayout += viewPort_BeforeLayout;
viewPort.AfterLayout += topViewPort_AfterLayout;
this.scene.RootViewPort.Children.Add(viewPort);
// other stuff ...
} private void viewPort_BeforeLayout(object sender, System.ComponentModel.CancelEventArgs e)
{
// cancel ILayout's layout action for this view port.
e.Cancel = true;
} private void topViewPort_AfterLayout(object sender, EventArgs e)
{
var viewPort = sender as ViewPort;
ViewPort parent = viewPort.Parent;
viewPort.Location = new Point( + , parent.Size.Height / + );
viewPort.Size = new Size(parent.Size.Width / - , parent.Size.Height / - );
}
如果你查看一下实现了布局机制的 ILayoutHelper 的代码,会发现 e.Cancel = true; 这句话取消了 ILayout 对此 ViewPort 的布局操作。(我们要自定义布局操作,因此ILayout原有的布局操作就没有必要实施了。)
public static void Layout<T>(this ILayout<T> node) where T : ILayout<T>
{
ILayout<T> parent = node.Parent;
if (parent != null)
{
bool cancelTreeLayout = false; var layoutEvent = node.Self as ILayoutEvent;
if (layoutEvent != null)
{ cancelTreeLayout = layoutEvent.DoBeforeLayout(); } if (!cancelTreeLayout)
{ NonRootNodeLayout(node, parent); } if (layoutEvent != null)
{ layoutEvent.DoAfterLayout(); }
} foreach (T item in node.Children)
{
item.Layout();
} if (parent != null)
{
node.ParentLastSize = parent.Size;
}
}
总结
ViewPort在Scene里是一个树型结构,支持ILayout布局和Before/AfterLayout自定义布局。有一个Visible的ViewPort,场景就要渲染一次。
CSharpGL(35)用ViewPort实现类似3DMax那样的把一个场景渲染到4个视口的更多相关文章
- VC++环境下单文档SDI与OpenGL多视图分割窗口的实现-类似3DMAX的主界面
本文主要讲述如何在VC++环境下实现单文档SDI与OpenGL多视图分割窗口,最终的界面类似3DMAX的主界面.首先给出我实现的效果图: 整个实现过程网络上有很多零散的博文,请各位自行搜索,在基于对话 ...
- CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL +BIT祝威+悄悄在此留下版了个权的信息说: 开始 本文用step by step的方式,讲述如何使 ...
- 如何使weblogic11g类似weblogic923一样统一使用一个boot.properties文件
如何使weblogic11g类似weblogic923一样 统一使用一个boot.properties文件 1.在weblogic域下创建文件boot.properties输入用户密码例如:usern ...
- 使用three.js加载3dmax资源,以及实现场景中的阴影效果
使用three.js可以方便的让我们在网页中做出各种不同的3D效果.如果希望2D绘图内容,建议使用canvas来进行.但很多小伙伴不清楚到底如何为我们绘制和导入的图形添加阴影效果,更是不清楚到底如何导 ...
- BIT祝威博客汇总(Blog Index)
+BIT祝威+悄悄在此留下版了个权的信息说: 关于硬件(Hardware) <穿越计算机的迷雾>笔记 继电器是如何成为CPU的(1) 继电器是如何成为CPU的(2) 关于操作系统(Oper ...
- 模拟Paxos算法及其简单学习总结
一.导读 Paxos算法的流程本身不算很难,但是其推导过程和证明比较难懂.在Paxos Made Simple[1]中虽然也用了尽量简化的流程来解释该算法,但其实还是比较抽象,而且有一些细节问题没有交 ...
- CSharpGL(2)设计和使用场景元素及常用接口
CSharpGL(2)设计和使用场景元素及常用接口 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入 ...
- CSharpGL(1)从最简单的例子开始使用CSharpGL
CSharpGL(1)从最简单的例子开始使用CSharpGL 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo ...
- CSharpGL(53)漫反射辐照度
CSharpGL(53)漫反射辐照度 本系列将通过翻译(https://learnopengl.com)这个网站上关于PBR的内容来学习PBR(Physically Based Rendering). ...
随机推荐
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 传播正能量——做一个快乐的程序员
引子 今天在博客园看到施瓦小辛格的文章我们搞开发的为什么会感觉到累,顿时有感而发.自己本来不擅长写文章,更不擅长写这种非技术性的文章,但是在思绪喷薄之际,还是止不住有很多话要说.针对从客观上说&quo ...
- 多线程的通信和同步(Java并发编程的艺术--笔记)
1. 线程间的通信机制 线程之间通信机制有两种: 共享内存.消息传递. 2. Java并发 Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式执行,通信的过程对于程序员来说是完全透 ...
- .NET平台开源项目速览(14)最快的对象映射组件Tiny Mapper
好久没有写文章,工作甚忙,但每日还是关注.NET领域的开源项目.五一休息,放松了一下之后,今天就给大家介绍一个轻量级的对象映射工具Tiny Mapper:号称是.NET平台最快的对象映射组件.那就一起 ...
- .NET Core的日志[5]:利用TraceSource写日志
从微软推出第一个版本的.NET Framework的时候,就在“System.Diagnostics”命名空间中提供了Debug和Trace两个类帮助我们完成针对调试和跟踪信息的日志记录.在.NET ...
- Java进击C#——前言
本章简言 记得三年前笔者来到现在的公司的时候,公司人口不出十个人.那个时候笔者刚从日本回来,想在福州.厦门.青岛找一个合适自己发展的机会.最后我的一个福州的朋友打电话希望我能过去帮他,跟他一起创业.这 ...
- C# 用SoapUI调试WCF服务接口(WCF中包含用户名密码的验证)
问题描述: 一般调试wcf程序可以直接建一个单元测试,直接调接口. 但是,这次,我还要测试在接口内的代码中看接收到的用户名密码是否正确,所以,单一的直接调用接口方法行不通, 然后就想办法通过soapU ...
- RabbitMQ + PHP (三)案例演示
今天用一个简单的案例来实现 RabbitMQ + PHP 这个消息队列的运行机制. 主要分为两个部分: 第一:发送者(publisher) 第二:消费者(consumer) (一)生产者 (创建一个r ...
- 树莓派 基于Web的温度计
前言:家里的树莓派吃灰很久,于是拿出来做个室内温度展示也不错. 板子是model b型. 使用Python开发,web框架是flask,温度传感器是ds18b20 1 硬件连接 ds18b20的vcc ...
- git 命令总结
1 删除分支 git push origin :branch name(Task_******) //删除远程分支 git branch -D branch name(Task_******) ...