Camera 上面挂载了UICamera Script,这个脚本的主要目的是”contains NGUI’s event system”,至于消息是如何传递的这里先暂时不管,以后会深入学习。
Anchor 上面挂载了UIAnchor Script,这个脚本的主要目的是”apply a half-pixel offset on Windows machines, for pixel-perfect results.”(把UI限定在窗口特定位置,当窗口变化的时候会依然处于相对窗口的位置)
Panel 上面挂载了UIPanel,这个脚本的主要目的是”container that will collect all UI widgets under it and will combine them into as few draw calls as possible.”(这里相当于UI的管理容器,负责以最少的draw call去管理UI)
[FF0000]NGUI's [FFFFFF]lables can have [00FF00]embedded [0000FF]colors.
Button
当我们添加了Button后,如何响应事件了? “anything with a collider on it will receive all of the events.”
还记得之前说UICamera负责发送所有事件吗?所有在Camera(绑定了UICamera)下的UI都会接受到Event(前提是设置了event发送到UI层)。 让我们来看下UICamera的一些public可控变量: 让我们关注一些比较重要的点: Event Receive Mask – 控制了接受events的layer层 Debug – debug模式下可以让我们看到哪一个game objec t接收到了event Range Distance – 控制raycast的有效范围 Scroll Axis Name – 设定横向控制来源 Vertical Axis Name – 设定纵向控制来源 ……
接下来让我们看看UICamera都负责发送哪些事件?(具体可以参见UICamera源码) OnHover (isOver) is sent when the mouse hovers over a collider or moves away. OnPress (isDown) is sent when a mouse button gets pressed on the collider. OnSelect (selected) is sent when a mouse button is first pressed on a game object. Repeated presses on the same object won’t result in a new OnSelect. OnClick () is sent with the same conditions as OnSelect, with the added check to see if the mouse has not moved much. UICamera.currentTouchID tells you which button was clicked. OnDoubleClick () is sent when the click happens twice within a fourth of a second. UICamera.currentTouchID tells you which button was clicked. OnDragStart () is sent to a game object under the touch just before the OnDrag() notifications begin. OnDrag (delta) is sent to an object that’s being dragged. OnDragOver (draggedObject) is sent to a game object when another object is dragged over its area. OnDragOut (draggedObject) is sent to a game object when another object is dragged out of its area. OnDragEnd () is sent to a dragged object when the drag event finishes. OnInput (text) is sent when typing (after selecting a collider by clicking on it). OnTooltip (show) is sent when the mouse hovers over a collider for some time without moving. OnScroll (float delta) is sent out when the mouse scroll wheel is moved. OnKey (KeyCode key) is sent when keyboard or controller input is used.
那么知道了发送哪些事件,那么我们如何通过添加的控件去响应事件了?
控件要处于Camera(含UICamera脚本)之下
Layer处于UICamera的Event Receiver Mask
接受事件的控件必须包含Collider
自定义响应方法
定义对应事件响应的方法并挂载到需要响应的Game Object上
1 2 3 4 5 6 7 8 9 10 11
voidOnPress(bool isPressed) { if(isPressed) { Debug.Log("Exit button is pressed!"); } else { Debug.Log("Exit button is unpressed!"); } }
///<summary> /// Event Hook class lets you easily add remote event listener functions to an object. /// Example usage: UIEventListener.Get(gameObject).onClick += MyClickFunction; ///</summary>
public VoidDelegate onSubmit; public VoidDelegate onClick; public VoidDelegate onDoubleClick; public BoolDelegate onHover; public BoolDelegate onPress; public BoolDelegate onSelect; public FloatDelegate onScroll; public VectorDelegate onDrag; public ObjectDelegate onDrop; public StringDelegate onInput; public KeyCodeDelegate onKey;
// Use this for initialization voidStart () { GameObject exitbutton = GameObject.Find("UI Root (2D)/Camera/Anchor/Panel/Exit"); // 这里的回调方法要和delegate签名一致 UIEventListener.Get(exitbutton).onPress = RemoteExitButtonClick; } // Update is called once per frame voidUpdate () { }
voidRemoteExitButtonClick(GameObject go, bool state) { if (state) { Debug.Log("RemoteExitButtonClick button is pressed!"); } else { Debug.Log("RemoteExitButtonClick button is unpressed!"); } } }
Font Maker里的字体是由什么制作而来的了? “All fonts are created using the free BMFont program, or the more advanced Glyph Designer.”(NGUI使用的Font是由BMFont program或Glyph Designer制作的)
最后点击存储为Bitmap导出字符集(Option -> Save Bitmap Font As) 这里就会生成对应的字符集纹理图片和Font Descriptor文件。 Note: 但我发现由于包含了大量中文字符,字符纹理比较大,包括中文和英文就有10M了,如果文字比较多比如做本地化,静态字体(我们这里提前生成字符集纹理就是静态字体)不适合。
NGUI支持动态字体的生成: 让我们先看看什么是动态字体? “When you set the Characters drop-down in the Import Settings to Dynamic, Unity will not pre-generate a texture with all font characters. Instead, it will use the FreeType font rendering engine to create the texture on the fly.”(https://docs.unity3d.com/Manual/class-Font.html) 可以看出动态字体不需提前生成字符集纹理,而是通过导入的ttf动态的在游戏里生成对应字体纹理。这样做的好处就是减少了像静态字体这样的纹理内存开销,我们只需指定使用哪些ttf并确保用户包含了该ttf就能正确显示所有语言。 个人认为动态字体是本地化的一个解决方案。 动态字体直接从ttf文件生成。 Note: 值得注意的是动态字体下写的那个警告”Please note that dynamic fonts can’t be made a part of an atlas, and they will always be draw in a speparate draw call. You will need to adjust transform position’s Z rather than depth.” 使用动态字体后会增加一个draw call,而且动态字体的深度不再是有depth来控制而是z值。
protectedvirtualvoidStart () { UIOrthoCamera oc = GetComponentInChildren<UIOrthoCamera>(); if (oc != null) { Debug.LogWarning("UIRoot should not be active at the same time as UIOrthoCamera. Disabling UIOrthoCamera.", oc); Camera cam = oc.gameObject.GetComponent<Camera>(); oc.enabled = false; if (cam != null) cam.orthographicSize = 1f; } else Update(); }
publicint activeHeight { get { int height = Mathf.Max(2, Screen.height);
// Base on width instead of height manualHeight = Screen.height * UIRootExtend.ManualWidth / Screen.width; if (scalingStyle == Scaling.FixedSize) return manualHeight;
#if UNITY_IPHONE || UNITY_ANDROID if (scalingStyle == Scaling.FixedSizeOnMobiles) return manualHeight; #endif if (height < minimumHeight) return minimumHeight; if (height > maximumHeight) return maximumHeight; return height; } }
但上述在低分辨率变到高分辨率的时候会因为以宽为基准导致高度的自适应超出范围,见下图: 这种情况只能通过拉伸背景来铺满屏幕,但会失去原始比例(UIStretch可以实现基于屏幕或容器的自适应,不建议使用,后续NGUI都放弃了)。 我们肯定不希望我们的主要的UI都被拉伸变形,所以这里需要提到的是UIAnchor, 让我们在回忆下UIAnchor的作用: “apply a half-pixel offset on Windows machines, for pixel-perfect results.”(把UI限定在窗口特定位置,当窗口变化的时候会依然处于相对窗口的位置) 待续……
关于UI制作尺寸的选择以及UI自适应相关设置: 前面的学习结合Unity Unit Pixel Per Unit的学习,可以看出,UI自适应和Pixel显示我們需要考虑的点有以下几个: UI需要考虑的点:
Canvas Render Mode(Canvas) – Screen Space Camera(一般来说不是作为3D UI摄像机的话,都会选择Screen Space Camera去自己指定一個UI Camera)
UI Scale Mode(Canvas Scaler) – Scale With Screen Size(一般来说为了根据不同设备分辨率进行自适应,我們都会选择这种模式(跟物理size无关))
简单看一下官网提到的Features(这里只Copy一部分): Speed and efficiency: Not only very fast, but also very efficient: everything is cached and reused to avoid useless GC allocations. Shortcuts: Shortcut extensions that directly extend common objects Extremely accurate: Time is calculated in a very precise way. This means that 1000 loops of 1 second each will play exactly as long as a single loop of 1000 seconds. Animate everything (almost) DOTween can animate every numeric value and also some non-numeric ones. It can even animate strings, with support for rich-text. Full control Play, Pause, Rewind, Restart, Complete, Goto and tons of other useful methods to control your tweens. Grouping Combine tweens into Sequences to create complex animations (which don’t need to be in a, uh, sequence: they can also overlap each other). Blendable tweens Some tweens can blend between each other in realtime, thanks to powerful DOBlendable shortcuts. Paths Animate stuff along both linear and curved paths, with additional options for the orientation of your traveling agents. Change values and duration while playing Change a tween’s start/end values or duration at any moment, even while playing. Yield for coroutines Various “WaitFor…” methods to use inside coroutines, that allow you to wait for a tween to be completed, killed, started, or for it to reach a given position or loops. Plugins DOTween is built with an extensible architecture in mind, which allows you to create your own tween plugins as separate files. Extras Extra virtual methods to do stuff like calling a function after a given delay. ……. 总的来说DOTween可以满足我们做动画的任何要求且高效快速方便使用。
深入学习DOTween之前,让我们先了解下DOTween里的命名方式和一些关键术语: 以下学习源于DOTween官网 Nomenclature: Tweener – A tween that takes control of a value and animates it.(Tweener是控制动画细节参数和播放的) Sequence – A special tween that, instead of taking control of a value, takes control of other tweens and animates them as a group.(Sequence可以理解成负责对tweens进行动画而非对属性比如position做动画) Tween – A generic word that indicates both a Tweener and a Sequence. Nested tween – A tween contained inside a Sequence.
Prefixes: DO – Prefix for all tween shortcuts (operations that can be started directly from a known object, like a transform or a material). Also the prefix of the main DOTween class.
Set – Prefix for all settings that can be chained to a tween (except for From, since it’s applied as a setting but is not really a setting).(tween的setting函数)
The generic way DOTween.TO(getter, setter, to float duration) Changes the given property from its current value to the given one. getter A delegate that returns the value of the property to tween. Can be written as a lambda like this: ()=> myValue where myValue is the name of the property to tween. setter A delegate that sets the value of the property to tween. Can be written as a lambda like this: x=> myValue = x where myValue is the name of the property to tween. to The end value to reach. duration The duration of the tween.
1 2 3 4
// Tween a Vector3 called myVector to 3,4,8 in 1 second DOTween.To(()=> myVector, x=> myVector = x, new Vector3(3,4,8), 1); // Tween a float called myFloat to 52 in 1 second DOTween.To(()=> myFloat, x=> myFloat = x, 52, 1);
static DOTween.Punch(getter, setter, Vector3 direction, float duration, int vibrato, float elasticity) static DOTween.Shake(getter, setter, float duration, float/Vector3 strength, int vibrato, float randomness, bool ignoreZAxis) static DOTween.ToAlpha(getter, setter, float to, float duration) static DOTween.ToArray(getter, setter, float to, float duration) static DOTween.ToAxis(getter, setter, float to, float duration, AxisConstraint axis)
貌似是会对Tween做特殊的限制,比如DOTween.Shake延axis轴变化vector3。
接下来让我们看看如何使用Sequence: The sequenced tweens don’t have to be one after each other. You can overlap tweens with the Insert method.(sequence可包含其他sequence,可以通过Insert把tween插入到特定时间执行) A tween (Sequence or Tweener) can be nested only inside a single other Sequence, meaning you can’t reuse the same tween in multiple Sequences. (同一个tween不能被多个sequence使用) 创建一个Sequence:
1
DOTween.Sequence();
我们可以通过添加tweens,callback到sequence里去做一些特别的动画。 Note: “ all these methods need to be applied before the Sequence starts (usually the next frame after you create it, unless it’s paused), or they won’t have any effect.”(添加tweens,callback这些都必须在Sequence开始之前,可以理解为同一帧里)
Via Static methods and filters DOTween class给我们提供了大量的静态方法去控制tween动画。 比如我们想暂停某个tween动画或暂停所有tween:
1 2
DOTween.Pause(tweenname); DOTween.PauseAll();
Directly from the tween 我们也可以通过保存的tween动画引用去调用控制相关的动画
1
myTween.Pause();
From a shortcut-enhanced reference 直接通过扩展方法去停止对应的物体上的tween(这里要注意的是,所有控制tween的扩展方法都带DO前缀)
1
transfrom.DOPause();
Note: “IMPORTANT: remember that to use these methods on a tween after it has ended, you have to disable its autoKill behaviour, otherwise a tween is automatically killed at completion.”(对tween设置的时候一定要disable automatically kill,否则在tween完成的时候tween会被销毁) 同时DOTween还给我们提供了很多静态方法去获取tween的信息(比如Delay(),Duration()等)
// 需要等到tween结束 IEnumerator SomeCoroutine() { Tween myTween = transform.DOMoveX(45, 1); yieldreturn myTween.WaitForCompletion(); // This log will happen after the tween has completed Debug.Log("Tween completed!"); }
voidDelete(T v) { if (mHeadNode == NULL) { return; }
SingleLinkNode<T> *tempnode = mHeadNode;
SingleLinkNode<T> *prenode = NULL;
bool find = false;
while (tempnode != NULL) { //remove first v element if (tempnode->mElement == v) { //when remove first node, move forward mHeadNode if (tempnode == mHeadNode) { mHeadNode = tempnode->Next; } else { prenode->Next = tempnode->Next; } delete tempnode; mLength--; find = true; break; } //Record pre node for later operation prenode = tempnode; tempnode = tempnode->Next; }
if (find) { cout << "Delete " << v << " successfully!" << endl; } else { cout << "Delete " << v << " failed! Not find it in list!" << endl; } }
intFind(T v) { int position = 0; SingleLinkNode<T> *tempnode = mHeadNode; while (tempnode != NULL) { position++; if (tempnode->mElement == v) { return position; } else { tempnode = tempnode->Next; } } return-1; }
SingleLinkNode<T>* GetNodeAt(int position) { if (position < 1 || position > mLength) { cout << position << " Out of range of link list!" << endl; cout << "Current length of link list = " << mLength << endl; cout << "Insert failed!" << endl; returnNULL; } else { //链表是非连续的存储方式,所以需要通过循环访问到特定位置 SingleLinkNode<T> *tempnode = mHeadNode;
if (position == 1) { return mHeadNode; } else { for (int i = 1; i < position; i++) { tempnode = tempnode->Next; } return tempnode; } } }
//when insert first element, tail node equals to head node if (mLength == 0) { tempnode->Pre = NULL; tempnode->Next = NULL; mHeadNode = tempnode; mTailNode = mHeadNode; } else { mHeadNode->Pre = tempnode; tempnode->Next = mHeadNode; mHeadNode = tempnode; }
mLength++; }
voidInsertAtPosition(int position, T v) { if (position < 1 || position > mLength + 1) { cout << position << " Out of range of link list!" << endl; cout << "Current length of link list = " << mLength << endl; cout << "Insert failed!" << endl; } //when insert element as first element. elseif (position == 1) { FrontInsert(v); } //when insert at final position //when position is larger than length of link list, //we insert it at the end of the link list elseif (position == mLength + 1) { TailInsert(v); } //insert element between head and last element else { DoubleLinkNode<T> *tempnode = newDoubleLinkNode<T>();
DoubleLinkNode<T>* GetNodeAt(int position) { if (position < 1 || position > mLength) { cout << position << " Out of range of link list!" << endl; cout << "Current length of link list = " << mLength << endl; cout << "Insert failed!" << endl; returnNULL; } else { //链表是非连续的存储方式,所以需要通过循环访问到特定位置 DoubleLinkNode<T> *tempnode = mHeadNode;
if (position == 1) { return mHeadNode; } elseif (position == mLength) { return mTailNode; } else { for (int i = 1; i < position; i++) { tempnode = tempnode->Next; } return tempnode; } } }
boolIsEmpty() { return mLength; }
voidDelete(T v) { if (mHeadNode == NULL) { return; }
DoubleLinkNode<T> *tempnode = mHeadNode;
DoubleLinkNode<T> *prenode = NULL;
bool find = false;
while (tempnode != NULL) { //remove first v element if (tempnode->mElement == v) { //when remove first node, move forward mHeadNode if (tempnode == mHeadNode) { if (mLength <= 1) { mHeadNode = NULL; mTailNode = NULL; } else { mHeadNode = tempnode->Next; mHeadNode->Pre = NULL; } } elseif (tempnode == mTailNode) { if (mLength <= 1) { mHeadNode = NULL; mTailNode = NULL; } else { mTailNode = prenode; mTailNode->Next = NULL; } } else { prenode->Next = tempnode->Next; tempnode->Next->Pre = prenode; } delete tempnode; mLength--; find = true; break; } //Record pre node for later operation prenode = tempnode; tempnode = tempnode->Next; }
if (find) { cout << "Delete " << v << " successfully!" << endl; } else { cout << "Delete " << v << " failed! Not find it in list!" << endl; } }
intFind(T v) { int position = 0; DoubleLinkNode<T> *tempnode = mHeadNode; while (tempnode != NULL) { position++; if (tempnode->mElement == v) { return position; } else { tempnode = tempnode->Next; } } return-1; }
int _tmain(int argc, _TCHAR* argv[]) { int n; cout<<"Please Enter the Fibonacci's n:"<<endl; cin>>n; cout<<"Fibonacci("<<n<<") = "<<Fibonacci(n)<<endl;
形式化描述: 给定c > 0,w(i) > 0, v(i) > 0, i <= i <=n,要求找出一个n元0-1向量(x1,x2,x3…..,xn), x(i) ∈ {0, 1}, 1 <= i <= n,使得∑(i= 1 - n)(w(i) x x(i)) <= c,而且∑(i= 1 - n)(v(i) x x(i))达到最大。 转换成式子如下: max(∑( 1 <= i <= n)(v(i) x x(i))) { ∑(i= 1 - n)(w(i) x x(i)) <= c { x(i) ∈ {0, 1}, 1 <= i <= n
分析: 最优子结构性质: 设(y1,y2……yn)是所给0-1背包问题的一个最优解,则(y2,y3…..yn)是下面相应子问题的一个最优解: max(∑(2 <= i <= n)(v(i) x x(i))) { ∑(2 <= i <= n)(w(i) x x(i)) <= c - w(1) x y(1) { x(i) ∈ {0, 1}, 2 <= i <= n
递归关系: 0-1背包问题的子问题: max(∑(i <= k <= n)(v(k) x x(k))) { ∑(i <= k <= n)(w(k) x x(k)) <= j { x(k) ∈ {0, 1}, i <= k <= n m(i,j)是背包容量为j,可选物品为i,i+1…..n时0-1背包问题的最优解。 x(i)表示背包i的选择∈ {0, 1} 根据最优子结构性质,可以建立就按m(i,j)的递归式如下: { max{m((i+1,j), m(i+1, j - w(i)) + v(i))} j >= w(i) m(i,j) = { { m(i+1,j) 0 <= j <= w(i)
voidKnapsack(float *v, int *w, int c, int n, float **m) { int jmax = fmin(w[n] - 1,c); //根据最优子结构性质,m[n - 1][j]的最优解是m[n][j]最优解的子集, //我们先构建出m[n][j]时的数据,然后利用m[n][j]的数据去构建m[i][c] 1 <= i < n for (int j = 0; j <= jmax; j++) { m[n][j] = 0; }
//Finally, we can get m[1][c] from c[2][c] m[1][c] = m[2][c]; if (c >= w[1]) { m[1][c] = fmax(m[2][c], m[2][c - w[1]] + v[1]); } }
voidTraceback(float **m, int *w, int c, int n, int *x) { for (int i = 1; i < n; i++) { if (m[i][c] == m[i + 1][c]) { x[i] = 0; } else { x[i] = 1; c -= w[i]; cout << "Put " << i << " into the bag!" << endl; } } //check for last one x[n] = m[n][c] ? 1 : 0; if (x[n] == 1) { cout << "Put " << n << " into the bag!" << endl; } }
int _tmain(int argc, _TCHAR* argv[]) { //0-1背包问题 //因为我们从v[1]开始访问当做第一个背包的价值, //所以这里加一个0在最前面方便从1开始索引 float v[] = { 0, 1, 5, 2, 3, 6, 8, 3 }; int w[] = { 0, 4, 3, 6, 7, 2, 3, 1 }; int c = 20; //减掉我们额外增加的第一个 int n = GetArrayLength(v) - 1; float **m = newfloat*[GetArrayLength(v)]; //为了从1开始索引 int *x = newint[n + 1]; for (int i = 0; i < GetArrayLength(v); i++) { //m[i][j] 1 <= j <= n用于表示背包容量为j,可选物品为i,i+1....n是0-1背包问题的最优解 //为了m[i][c]来访问背包重量为c的时候的最优解,这里需要额外增加数组长度1 m[i] = newfloat[c + 1]; } Knapsack(v, w, c, n, m); Traceback(m, w, c, n, x); for (int i = 0; i < GetArrayLength(v); i++) { delete m[i]; }
接下来以0-1背包问题为例来学习理解回溯法: 假设0-1背包n = 3,w = [16, 15, 15] v = [45, 25, 25] c= 30 解空间为: {(0,0,0), (0,1,0), (0,0,1), (1,0,0), (0,1,1), (1,0,1), (1,1,0), (1,1,1)} 因为是以深度优先搜索,所以是延A -> B -> D -> H -> D -> I -> D -> B -> E -> J -> K的顺序来搜索,直到回溯完所有的解空间节点或找到答案为止。
问题: 上述回溯法把所有的解空间都搜索了一遍,时间和空间复杂度大。
优化: 回溯法通常采用两种策略避免无效搜索,提高回溯法的搜索效率:
用约束函数在扩展结点处剪去不满足约束条件的子树。
用限界函数剪去得不到最优解的子树。(两个函数统称为剪枝函数)
在0-1背包里左子树表示装载当前背包,右子树表示不装载当前背包。 所以在针对剪枝函数的优化上,我们可以在判断只有在右子树中有可能包含最优解时才进入。设r是当前剩余物品价值总和;cp是当前价值;bestp是当前最优价值。当cp + r <= bestp时,可剪去右子树。
template<class Forwarditerator> voidshift_left(Forwarditerator beg, Forwarditerator end) { //temporary variable for first element typedeftypename std::iterator_traits<Forwarditerator>::value_type value_type;
if(beg != end) { //save value of first element value_type temp(*beg);
//convenience function to create the MyInsertIterator template<class Container> inline MyInsertIterator<Container> MyInsert(Container& c) { returnMyInsertIterator<Container>(c); }
template<class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function fn) { while (first!=last) { fn (*first); ++first; } return fn; // or, since C++11: return move(fn); }
在进一步了解我们导入Assets的时候,会发生些什么之前,先让我们来学习了解一些相关的概念。 首先,什么是Asset? Asset – “An Asset is a file on disk, stored in the Assets folder of a Unity Project. e.g. texture files, material files and FBX files……”(Asset代表的所有存储在Assets目录下的文件资源)
遇到Unity不支持的格式,Unity需要通过import process导入资源文件(e.g. PNG, JPG) “The import process converts source Assets into formats suitable for the target platform selected in the Unity Editor.”(Import process主要是为了将不支持的资源格式转换到Unity对应平台设置的对应格式)
Unity不可能每一次都去重新Import这些资源,那么Unity是如何将import结果存储在哪里了? 为了避免重复的import导入,” the results of Asset importing are cached in the Library folder.”(Asset导入的结果被缓存在了library folder – 我想这也就是为什么每次删掉Library文件会导致一些asset重新导入的原因)
“the results of the import process are stored in a folder named for the first two digits of the Asset’s File GUID. This folder is stored inside the Library/metadata/ folder. The individual Objects are serialized into a single binary file that has a name identical to the Asset’s File GUID.”(可以看出import的结果存储在Library/metadata/文件夹下,并且把File GUID的前两位bit作为文件夹名,以File GUID作为文件名字) 以前面Projectile.png导入为例: 因为Projectile.png.meta的File ID为8764571b0416f90488390d0114c49afd 所以导入的结果就存储在Library\metadata\87\文件夹下,文件名为8764571b0416f90488390d0114c49afd(由于是二进制文件,无法查看具体内容) Note: Non-Native asset type需要通过asset importer去导入Unity。(自动调用,也可以通过AssetImporter API去调用)
先来看看什么是AssetDatabase: “AssetDatabase is an API which allows you to access the assets contained in your project. Among other things, it provides methods to find and load assets and also to create, delete and modify them. The Unity Editor uses the AssetDatabase internally to keep track of asset files and maintain the linkage between assets and objects that reference them.”(可以看出,AssetDatabase在Unity对于Asset管理上起了关键性作用,AssetDatabase里存储了Asset相关的很多信息(e.g. Asset Depedency, Asset Path…..))
using UnityEngine; using System.Collections; #if UNITY_EDITOR using UnityEditor; #endif
publicclassCreateCubeAssetMenu { //Only works under editor #if UNITY_EDITOR [MenuItem("AssetDatabase/CreateCubeAsset")] staticvoidCreateCudeAsset() { //load Projectile.png as texture Texture2D texture = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/Sprites/Projectile.png", typeof(Texture2D));
//create material and cube assets string matassetname; string cubename; string materialfoldername = "Materials"; string cubefoldername = "Prefabs";
Color[] colors = { Color.red, Color.blue, Color.green }; for (int i = 0; i < colors.Length; i++) { //Create material first Material mat = new Material(Shader.Find("Transparent/Diffuse")); mat.mainTexture = texture; mat.color = colors[i];
//create a new cube and set material for it GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube); MeshRenderer meshrender = obj.GetComponent<MeshRenderer>(); if (meshrender != null) { meshrender.material = mat; } else { Debug.Log("meshrender == null!"); break; }
matassetname = mat.color.ToString() + "Material.mat"; cubename = mat.color.ToString() + "Cube.prefab"; //create material folder //check whether material folder exist if (AssetDatabase.Contains(mat)) { Debug.Log("Material asset has been created before!"); } else { if (!AssetDatabase.IsValidFolder("Assets/" + materialfoldername)) { AssetDatabase.CreateFolder("Assets", materialfoldername); }
if (!AssetDatabase.IsValidFolder("Assets/" + cubefoldername)) { AssetDatabase.CreateFolder("Assets", cubefoldername); }
在进一步了解还应该存储哪些信息之前,这里需要理解一个概念UnityEngine.Object 那么Object在Unity里是什么概念了? UnityEngine.Object – “Object with a capitalized ‘O’, is a set of serialized data collectively describing a specific instance of a resource. This can be any type of resource which the Unity Engine uses, such as a mesh, a sprite, and AudioClip …..”(Object是指描述了Asset上使用的所有resources的序列化数据。比如制作一个2D Bullet Prefab,上面会挂载Transform,Sprite Renderer,Box Collider 2D,Rigidbody 2D, Bullet Script,Animator等Object,这里我们需要分别标识和记录这些Object的信息)
前面我们提到了Asset是通过Unique ID(File GUID)来标识,那么这里的Object用什么来标识了?并且又存储在哪里了? 答案是使用Local ID来标识并存储在Asset文件里(需要设置Asset Serialization到Force Text才能查看(默认是Mixed(Text和Binary))) Local ID – identifies each Object within an Asset file because an Asset file may contain multiple Objects.
那么Asset和Object之间是什么样的关系了? “There is a one-to-many relationship between Assets and Objects: that is, any given Asset file contains one or more Objects.”(Asset file可以包含一个或多个Objects)
那么为什么要采用File GUID和Local ID了? “The File GUID provides an abstraction of a file’s specific location. As long as a specific File GUID can be associated with a specific file, that file’s location on disk becomes irrelevant. The file can be freely moved without having to update all Objects referring to the file.”(通过指定唯一个File GUID和Local ID,使文件的位置变的无关紧要,我们可以随意的移动文件位置而无需更新所有的Object reference信息)
那么Instance ID是如何运作的了? “At startup, the Instance ID cache is initialized with data for all Objects that are built-in to the project (i.e. referenced in Scenes), as well as all Objects contained in the Resources folder. Additional entries are added to the cache when new assets are imported at runtime(3) and when Objects are loaded from AssetBundles. Instance ID entries are only removed from the cache when they become stale. This happens when an AssetBundle providing access to a specific File GUID and Local ID is unloaded.”(当游戏启动的时候,Instance ID的cache开始初始化所有在项目场景里引用到的Object。额外的Instance ID Cache只有在通过运行时导入或则AssetBundle动态加载Object的时候添加。Instance ID只有在Instance ID标识的Object被Unloaded的时候才会被removed from cache)
那么什么时候Object才会被Unloaded了?Object的Instance ID与AssetBundle之间又是如何关联起来的了? “When the unloading of an AssetBundle causes an Instance ID to become stale, the mapping between the Instance ID and its File GUID and Local ID is deleted to conserve memory. If the AssetBundle is re-loaded, a new Instance ID will be created for each Object loaded from the re-loaded AssetBundle.”(当AssetBundle(后面会详细讲到)被unload后,通过AssetBundle加载的Object的Instance ID会被删除以节约内存。当AssetBundle再次加载进来后,当AssetBundel里的Obejct被再次加载时,会为该Object生成新的Instance ID到cache里)
ScriptableObject “Provides a convenient system for developers to define their own data types.”(用于定义自定义类型数据)
MonoBehaviour “Provides a wrapper that lins to a MonoScript. A MonoScript is an internal data type that Unity uses to hold a reference to a specific scripting class within a specific assembly and namespace. The MonoScript does not contain any actual executable code.”
Monoscripts: “a MonoBehaviour has a reference to a MonoScript, and MonoScripts simply contain the information needed to locate a specific script class. Neither type of Object contains the executable code of script class.” “A MonoScript contains three strings: an assembly name, a class name, and a namespace.”(正如前面MonoBehavior提到的,MonoScript包含了定位script class需要的信息。 e.g. assembly name, class name, namspace……)
说了这么多Asset和Object相关的知识和概念,下面提一个与之相关却又常常遇到的问题。 Unity Store可以下载很多Asset Package资源,那么这里的问题就是,Asset Package是个什么概念?为什么通过导入Asset Package我们就能导入别人做好的Asset资源?如何制作自己的Asset Package? Asset Package概念: “Packages are collections of files and data from Unity projects, or elements of projects, which are compressed and stored in one file, similar to Zip files.”(可以看出Asset Package只是相当于Unity对于一系列Assets的打包,单记录了Assets原始的目录结构和Asset信息,好比压缩包)
Use of the Resources folder makes fine-grained memory management more difficult.(使用Resources folder会使内存管理更困难)
Improper use of Resources folders will increase application startup time and the length of builds.(不合理的使用Resources folders会使程序启动和编译时间变长) As the number of Resources folders increases, management of the Assets within those folders becomes very difficult.(随着Resources folders数量的增加,Assets管理越来越困难)
The Resources system degrades a project’s ability to deliver custom content to specific platforms and eliminates the possibility of incremental content upgrades.(Resources System降低了项目对于各平台和资源的动态更新能力,因为Resources目录下的资源无论如何都会被打包到游戏程序里) AssetBundle Variants are Unity’s primary tool for adjusting content on a per-device basis.(AssetBundle是Unity针对设备动态更新的主要工具)
Resources is an excellent system for during rapid prototyping and experimentation because it is simple and easy to use. However, when a project moves into full production, it is strongly recommended to eliminate uses of the Resources folder.(快速开发,但到了真正发布还是应该减少Resources Folder的使用)
The Resources folder is also useful in trivial cases, when all of the following conditions are met(当下列情况都满足的时候,Resource folder比较有用):
The content stored in the Resources folder is not memory-intense
The content is generally required throughout a project’s lifetime(该资源在项目生命周期里都需要)
The content rarely requires patching(很少需要改动patch)
The content does not vary across platforms or devices.(在各个平台设备都一致) 比如一些第三方配置文件等asset。
那么接下来让我们了解下Resources是如何被保存到Unity里的: Serialization of resources: “The Assets and Objects in all folders named “Resources” are combined into a single serialized file when a project is built.”(当项目编译的时候,所有放到Resources目录下的Assets和Object最终会被序列化到一个单独的文件,根据前面的测试应该是resources.assets)
“This file also contains metadata and indexing information, similar to an AssetBundle. This indexing information includes a serialized lookup tree that is used to resolve a given Object’s name into its appropriate File GUID and Local ID. It is also used to locate the Object at a specific byte offset in the serialized file’s body.”(Resource会去维护一个映射表,用于查询特定Object
“As the lookup data structure is (on most platforms) a balanced search tree(1), its construction time grows at an O(N log(N)) rate.”(Lookup是通过平衡二叉树来查找,所以时间复杂度为N x Log(N))
“This operation is unskippable and occurs at application startup time while the initial non-interactive splash screen is displayed.”(在程序启动的时候会去初始化index info(Lookup data)的时候,Resources里assets数量过多的话会导致花费大量时间)
A header The header is generated by Unity when the AssetBundle is built.(header是在Unity编译AssetBundle的时候生成)主要包含下列内容:
The AssetBundle’s identifier
Whether the AssetBundle is compressed or uncompressed
A manifest(“The manifest is a lookup table keyed by an Object’s name. Each entry provides a byte index that indicates where a given Object can be found within the AssetBundle’s data segment.”(manifest把Object的名字作为key,用于查询特定object是否存在于AssetBundle的数据字段里)) (manifest里通过std::multimap实现,不同平台multimap的实现有些许差别,Windows和OSX采用red-black tree,所以在构造manifest的时候,时间复杂度是N x Log(N))
A data segment. “Contains the raw data generated by serializing the Assets in the AssetBundle.”(data segment包含了序列化Assets的原始数据) data segment最后还会通过LZMA algorithm压缩。 “Prior to Unity 5.3, Objects could not be compressed individually inside an AssetBundle. “(Unity 5.3之前Object不支持被单独压缩到AssetBundle里,所以在去访问一个被包含在压缩了的AssetBundle里的Object时,Unity需要去解压整个AssetBundle) “Unity 5.3 added a LZ4 compression option. AssetBundles built with the LZ4 compression option will compress individual Objects within the AssetBundle, allowing Unity to store compressed AssetBundles on disk.”(Unity 5.3加入了LZ4压缩选项,支持单独的Object压缩到AssetBundle里,这样一来就可以通过单独解压特定的Object来实现访问该Object)
AssetBundle能包含哪些Assets? Models,Materials,textures and secenes.AssetBundle can not contain scripts.
AssetBundle.LoadFromFile “A highly-efficient API intended for loading uncompressed AssetBundle from local storage, such as a hard disk or an SD card.”(可以高效的加载本地未压缩的AssetBundle,也支持加载LZ4压缩的AssetBundle,但不支持LZMA压缩的AssetBundle) Mobile和Editor表现不一样,详情参见
WWW.LoadFromCacheOrDownload “A useful API for loading Objects both from remote servers and from local storage.”(主要用于加载远程服务器端和本地的Object) 使用建议: “Due to the memory overhead of caching an AssetBundle’s bytes in the WWW object, it is recommended that all developers using WWW.LoadFromCacheOrDownload ensure that their AssetBundles remain small”(尽量保证AssetBundle很小,避免内存消耗过大) “Each call to this API will spawn a new worker thread. Be careful of creating an excessive number of threads when calling this API multiple times.”(避免同时调用多次,导致大量的Thread执行,确保同一时间很少的thread执行)
UnityWebRequest’s DonwloadHandleAssetBundle(on Unity 5.3 or newer) “UnityWebRequest allows developers to specify exactly how Unity should handle downloaded data and allows developers to eliminate unnecessary memory usage.”(UnityWebRequest支持更细致的AssetBundle加载的内存使用,可以通过配置UnityWebRequest达到使用最少内存的目的得到我们想要加载的Obejct) “Note: Unlike WWW, the UnityWebRequest system has an internal pool of worker threads and an internal job system to ensure that developers cannot start an excessive number of simultaneous downloads. The size of the thread pool is not currently configurable.”(UnituWebRequest system内部有自身的线程管理,避免同一时间大量的线程同时加载)
Loading Assets from AssetBundles: Synchronous API:
LoadAsset
LoadAllAssets
LoadAssetWithSubAsset
Asynchronous API:
LoadAssetAsync
LoadAllAssetsAsync
LoadAssetWithSubAssetAsync
使用建议: “LoadAllAssets should be used when loading multiple independent UnityEngine.Objects.”(当需要加载大量独立的Objects的时候,使用LoadAllAssets。当需要加载的Object数量很多,又少于AssetBundle里的2/3的时候,我们可以采用制作多个小的AssetBundle,然后再通过LoadAllAssets加载) “LoadAssetWithSubAssets should be used when loading a composite Asset which contains multiple embedded Objects. If the Objects that need to be loaded all come from the same Asset, but are stored in an AssetBundle with many other unrelated Objects.”(当加载由多个obejct构成的Object的时候,建议使用LoadAssetWithSubAssets。当加载的Objects都来之同一个Asset,但存储的AssetBundle里包含很多其他无关的Obejcts时,采用LoadAssetWithSubAssets) “For any other case, use LoadAsset or LoadAssetAsync.”(其他情况都是用LoadAsset和LoadAssetAsync)
Low-Level Loading details: “UnityEngine.Object loading is performed off the main thread: an Object’s data is read from storage on a worker thread. Anything which does not touch thread-sensitive parts of the Unity system (scripting, graphics) will be converted on the worker thread.”(Object的加载是在main thread上,而object data的数据读取是在worker thread。所有线程不敏感的数据都是在worker thread进行。)
加载AssetBundle里的Object需要注意些什么? “An Object is assigned a valid Instance ID when its AssetBundle is loaded, the order in which AssetBundles are loaded is not important. Instead, it is important to load all AssetBundles that contain dependencies of an Object before loading the Object itself. Unity will not attempt to automatically load any child AssetBundles when a parent AssetBundle is loaded.”(当AssetBundle被加载的时候,Object会被assigned一个valide instance ID,因为这个instance ID是唯一的,所以AssetBundle的加载顺序并不重要,重要的是我们要确保所有Object依赖的Objects都被加载(Unity不会自动加载所有Child AssetBundles当Parent AssetBundle被加载的时候)) 下面以Material A引用Texture B为例。Material A被Packaged到了AssetBundle1,而Texture B被packaged到了AssetBundle2。 所以我们要使用Material A,我们不仅要加载AssetBundle1,还得确保在此之前我们加载了AssetBundle2里的Texture B
如何查询AssetBundle里的Dependecy信息? Depending on the runtime environmen:
Editor AssetDatabase API(Query AssetBundle dependencies) AssetImporter API(Access and change AssetBundle assignments and dependencies)
Runtime AssetBundleManifest API(load the dependency information of AssetBundle) AssetBundleManifest.GetAllDependencies AssetBundleManifest.GetDirectDependencies Note: “Both of these APIs allocate arrays of strings. Use them sparingly, and preferably not during performance-sensitive portions of an application’s lifetime.”(因为上述API会分配大量的string字符串,所以要尽量少用并且避开性能敏感的时期)
if (manifest == null) { Debug.Log(string.Format("Failed to load {0}.manifest!", bundlename)); yieldbreak; }
//try to instantiate backgroundimage assetbundle //but need to load dependencies assetbundle first Debug.Log("Instantiate BackgroundImage.prefab");
var backgroundbundlename = "backgroundimage"; var backgroundimagerequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, backgroundbundlename));
yieldreturn backgroundimagerequest;
if (backgroundimagerequest == null) { Debug.Log(string.Format("Load {0} falied!", "backgroundimagerequest")); }
var backgrounddependencies = manifest.GetAllDependencies(backgroundbundlename); if (backgrounddependencies.Length == 0) { Debug.Log("dependencies.length == 0"); }
//Load dependencies assetbundle first foreach (string dependency in backgrounddependencies) { Debug.Log("Dependency : " + dependency); var dependencyabrequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, dependency)); yieldreturn dependencyabrequest; AssetBundle dependencyab = dependencyabrequest.assetBundle; if (dependencyab == null) { Debug.Log(string.Format("Load {0} failed!", dependency)); yieldbreak; } }
//Once load all dependencies assetbundle, we can instantiate the gameobject in assetbundle var backgroundprefabrequest = backgroundimageassetbundle.LoadAssetAsync("BackgroundImage.prefab"); yieldreturn backgroundprefabrequest; if(backgroundprefabrequest == null) { Debug.Log(string.Format("Load {0} faled!", "BackgroundImage.prefab")); }
//After complete using AssetBundle, always remember unload it, //otherwise you can not load it again due to it has exists in memory assetbundle.Unload(false); } }
上面仅仅是以本地读取作为事例,了解了如何去访问AssetBundle以及manifest里的信息以及如何实例化AssetBundle里的资源。 那么如何判断AssetBundle是否需要更新了? 这里我们可以利用Built-in caching去实现版本更新判断。 “AssetBundle caching system that can be used to cache AssetBundles downloaded via the WWW.LoadFromCacheOrDownload or UnityWebRequest APIs.”(AssetBundle caching system可以帮助我们缓存加载了的AssetBundle而无需每次都重新加载) 而AssetBundle caching system是根据AssetBundle的version number来决定时是否下载新的AssetBundle。(AssetBundleManifest API支持通过MD5算法去计算出AssetBundle的version number,这样一来每次AssetBundle有变化都会得到一个新的Hash version number)
“AssetBundles in the caching system are identified only by their file names, and not by the full URL from which they are downloaded.”(AssetBundles在Caching system里是通过文件名来标识的跟URL无关,所以无论AssetBundle放在服务器哪里都没有关系)
Cach相关的API控制:
Caching.expirationDelay “The minimum number of seconds that must elapse before an AssetBundle is automatically deleted. If an AssetBundle is not accessed during this time, it will be deleted automatically.”(AssetBundle可允许被删除的未使用时间,只有未被使用的时间达到了才能才被删除)
Caching.maximumAvailableDiskSpace “The amount of space on local storage that the cache may use before it begins deleting AssetBundles that have been used less recently than the expirationDelay. It is counted in bytes.”(Caching内存使用上限)
Note: “As of Unity 5.3, control over the built-in Unity cache is very rough. It is not possible to remove specific AssetBundles from the cache. They will only be removed due to expiration, excess disk space usage, or a call to Caching.CleanCache. (Caching.CleanCache will delete all AssetBundles currently in the cache.) “(Caching System还不完善,所以还不允许删除特定AssetBundle,而只能通过Caching.CleanCache去删除所有的AssetBundle)
Cache Priming: Steps:
Store the initial or base version of each AssetBundle in /Assets/StreamingAssets/
Loading AssetBundles from Application.streamingAssetsPath the first time the application is run
Call WWW.LoadFromCacheOrDownload or UnityWebRequest normally.
那么在使用AssetBundle的过程中,我们应该遵循后续讲到的内容(大部分翻译至官网,翻译不太对的地方欢迎指出): Managing Loaded Assets: “If an AssetBundle is unloaded improperly, it can cause Object duplication in memory. Improperly unloading AssetBundles can also result in undesirable behavior in certain circumstances.”(不合理的释放Object会导致在内存中重复创建Object。同时也可能导致非预期的问题(比如texture丢失))
对于AssetBundle里Assets管理,这里需要强调的一个API是AssetBundle.Unload(bool); Unloads all assets in the bundle. Unload frees all the memory associated with the objects inside the bundle. 当传递true的时候所有从AssetBundle里实例化的Object都会被unload。传递false则只释放AssetBundle资源。
那么这里释放的AssetBundle资源是哪些了? 还记得之前提到的AssetBundle的组成吗(header & data segment)
Eliminate all references to an unwanted Object, both in the scene and in code. After this is done, call Resources.UnloadUnusedAssets.(取消所有引用,并调用Resources.UnloadUnusedAssets)
Load a scene non-additively. This will destroy all Objects in the current scene and invoke Resources.UnloadUnusedAssets automatically.(切换Scene,触发Resources.UnloadUnusedAssets)
“Another problem can arise if Unity must reload an Object from its AssetBundle after the AssetBundle has been unloaded. In this case, the reload will fail and the Object will appear in the Unity Editor’s hierarchy as a (Missing) Object.”(当AssetBundle被释放后,如果再从该AssetBundle里加载Object会加载失败,出现missing object)
Distribution: Two basic ways to distribute a project’s AssetBundles to clients:
Installing them simultaneously with the project
Downloading them after installation. 使用哪一种方式,主要取决于平台需求。 “Mobile projects usually opt for post-install downloads to reduce initial install size and remain below over-the-air download size limits. Console and PC projects generally ship AssetBundles with their initial install.”(手机上为了减少安装程序大小,通常选择post-installation。而PC不担心硬盘不够,所以通常选择initial install)
Shipped with Project: “To reduce project build times and permit simpler iterative development. If these AssetBundles do not need to be updated separately from the application itself, then the AssetBundles can be included with the application by storing the AssetBundles in Streaming Assets.”(和程序一起更新的AssetBundle可以放在streaming assets伴随程序一起打包发布)
Streaming Assets: “The easiest way to include any type of content within a Unity application at install time is to build the content into the /Assets/StreamingAssets/ folder, prior to building the project. Anything contained in the StreamingAssets folder at build time will be copied into the final application. This folder can be used to store any type of content within the final application, not just AssetBundles.”(可以看出Streaming Assets被存放在/Assets/StreamingAssets/目录下,最终会被打包到应用程序里)
Note: “Android Developers: On Android, Application.streamingAssetsPath will point to a compressed .jar file, even if the AssetBundles are compressed. In this case, WWW.LoadFromCacheOrDownload must be used to load each AssetBundle.”(在Android上,streamingAssetsPath指向的是压缩后的.jar文件,所以我们需要采用LoadFromCacheOrDonwload去解压读取(5.3及以后可以采用UnityWebRequest’s DonwloadHandleAssetBundle))
“Streaming Assets is not a writable location on some platforms. If a project’s AssetBundles need to be updated after installation, either use WWW.LoadFromCacheOrDownload or write a custom downloader. “(因为Streaming Assets在一些平台上是一个不可写的位置,所以我们如果还需要更新该AssetBundle,我们而已通过WWW.LoadFromCacheOrDonwload或则自己编写custom downloader)
Donwloaded post-install: 手机上出于程序安装大小考虑多采用这个方案。 同时AssetBundle通过WWW.LoadFromCacheOrDownload or UnityWebRequest的更新可以快速方便的更新一些经常变化的资源。 更多内容参见
Asset Assignment Strategies: The key decision is how to group Objects into AssetBundles. The primary strategies are:
Split frequently-updated Objects into different AssetBundles than Objects that usually remain unchanged
Group together Objects that are likely to be loaded simultaneously
Patching with AssetBundles: “Patching AssetBundles is as simple as downloading a new AssetBundle and replacing the existing one.”(Patching AssetBundles用于实现动态替换一些资源很方便)
实战学习使用AssetBundle Manager: 首先粗略的了解下AssetBundle Manager提供的一些API: Initialize() – Initializes the AssetBundle manifest object.(初始化AssetBundle Manifest Object) LoadAssetAsync() – Loads a given asset from a given AssetBundle and handles all the dependencies.(加载特定Asset,并负责处理器所有的dependencies) LoadLevelAsync() – Loads a given scene from a given AssetBundle and handles all the dependencies.(加载特定scene,并负责处理所有的dependencies) LoadDependencies() – Loads all the dependent AssetBundles for a given AssetBundle.(加载AssetBundle所依赖的所有dependencies) BaseDownloadingURL – Sets the base downloading url which is used for automatic downloading dependencies.(设置dependencies下载url) SimulateAssetBundleInEditor – Sets Simulation Mode in the Editor.(设置editor的模拟模式) Variants – Sets the active variant.(设置激活的variants) RemapVariantName() – Resolves the correct AssetBundle according to the active variant.
那么Object什么情况下才会被加载到游戏里了? An Object will be loaded on-demand the first time its Instance ID is dereferenced if two criteria are true:(当Instance ID被间接引用同时满足以下两个条件的时候,Object会被加载)
The Instance ID references an Object that is not currently loaded(Instance ID引用的Object还没加载)
The Instance ID has a valid File GUID and Local ID registered in the cache(Instance ID拥有的File GUID和Local ID已经存在于cache里)
什么情况下,Object会被unloaded了? Objects are unloaded in three specific scenarios(Object被Unloaded的三种情况):
Objects are automatically unloaded when unused Asset cleanup occurs.(比如Application.LoadLevel() Rersources.UnloadUnusedAssets()调用的时候,Object会被自动unloaded)
Objects sourced from the Resources folder can be explicitly unloaded by invoking the Resource.UnloadAsset API.(主动调用Resource API去unload resoures下的object)
Objects source from Asset Bundles are automatically and immediately unloaded when invoking the AssetBundle.Unload(true) API.(这样会导致AssetBundle里的Objects InstanceID的引用无效) 具体Resource API如何影响Object的的生命周期,还需进一步学习,参考文档AssetBundle
知道了Resources的生命周期和如何被映射缓存的,那么如何才能以高效的方式存储resources了? Loading Large Hierarchies(当我们制作一个复杂的Resources时): “When serializing hierarchies of Unity GameObjects (such as when serializing prefabs), it is important to remember that the entire hierarchy will be fully serialized.”(当序列化Unity GameObject的时候,所有存在于hierarchy下的GameObject都会被一一序列化。)
When creating any GameObject hierarchy, CPU time is spent in several different ways:
Time to read the source data (from storage, from another GameObject, etc.)
Time to set up the parent-child relationships between the new Transforms
Time to instantiate the new GameObjects and Components
Time to awaken the new GameObjects and Components 我们的关注点放到第一点上,数据的读写方式对后面三点影响不大,第一点跟数据的读写方式和数据的大小紧密相关。 “On all current platforms, it is considerably faster to read data from elsewhere in memory rather than loading it from a storage device. “(所有平台上,从内存中读取都比从存储设备去读取快,当然不同的平台的读取速度会有一些差别)
ETC1主要是用于RGB的24-bit数据压缩(Note:不包含Alpha通道,在OpenGL ES 2.0就要求支持,所以基本是所有Android机型通用。参考:GL_OES_compressed_ETC1_RGB8_texture。ETC1想要配合使用Alpha信息需要拆成两张图,一张RGB,一张A。)
ETC2在兼容ETC1的基础上支持了Alpha通道的压缩(Note:ECT2至少要OpenGL ES 3.0 参考:ETC2 Compression)
///<summary> /// 获取指定纹理压缩格式的Bits Per Pixel(多少bit每像素) ///</summary> ///<param name="textureformat"></param> ///<returns></return>s publicstaticintGetTextureFormatBitsPerPixel(TextureFormat textureformat) { switch(textureformat) { case TextureFormat.ETC_RGB4: return4; case TextureFormat.ETC2_RGB: case TextureFormat.ETC2_RGBA8: return8; case TextureFormat.PVRTC_RGB4: case TextureFormat.PVRTC_RGBA4: return4; case TextureFormat.ARGB4444: case TextureFormat.RGBA4444: case TextureFormat.RGB565: return16; case TextureFormat.RGB24: return24; case TextureFormat.RGBA32: case TextureFormat.ARGB32: return32; default: Debug.LogErrorFormat("没有被包含的纹理压缩格式:{0},无法返回对应BitsPerPixel信息,请自己添加。", textureformat); return0; } } }
输出结果:
从上面我们可以看出以下几个结论:
纹理的大小主要取决于纹理压缩格式和自身宽高,纹理压缩格式所占的Bits Per Pixel越大,纹理内存占用越大。
XCode project is generated by Unity with all the required libraries, precompiled .NET code and serialized assets.(打包所有需要的库和资源生成XCode项目) 第一步是通过Unity的Building Settings设定IOS平台点击Build生成
XCode project is built by XCode and deployed and run on the actual device.(编译Xcode项目安装到设备上) 第二部必须在Mac上执行(或者黑苹果),因为需要用到IOS SDK和XCode编译器
Cloud Build
关于Cloud Build让我们直接看看官网的介绍吧: What is Unity Cloud Build? A service that automates the build pipeline for Unity games.(自动化编译打包服务 – Build Machine)
Why Should I Use Cloud Build? By using Cloud Build automation services, you will - Save Time. Builds are compiled and distributed automatically, minimizing manual work and intervention. Games with multiple platforms can be consolidated into a single build process.
Improve Quality. Games are built continuously as changes are detected (“Continuous Integration”), enabling detection of issues as they are introduced to the project. - Distribute Faster. Cloud-based infrastructure compiles builds; in parallel if for multi platform projects. Completed builds are available to download by anyone on the team through Cloud Build’s website.(快捷方便,自动化检测变化进行编译打包。每个人都可以自己去下载安装对应的版本)
How does Unity Cloud Build work? Unity Cloud Build monitors your source control repository (e.g. Git, Subversion, Mercurial, Perforce). When a change is detected, a build is automatically generated by Cloud Build. When the build is completed, you and your team are notified via email. Your build is hosted by Unity for you and your team mates. If the build is not successful, you’re also notified and provided with logs to begin troubleshooting.(通过检测Git等工具上传变化,自动触发编译打包流程,完成或出错的时候有邮件提醒和log)
What do I need to use Unity Cloud Build? 使用Unity Cloud Build我们必须先选择一个版本管理器 Git, Subversion, Mercurial, Perforce 这里我使用Git。
// Update is called once per frame voidUpdate () { float y = Mathf.Repeat(Time.time * mScrollSpeed, 1); Vector2 offset = new Vector2(0.0f, y); if(mBackgroundMeshRender != null) { mBackgroundMeshRender.material.mainTextureOffset = offset; } else { Debug.Log("This script only works with Gameobject that contains MeshRenderer and Material."); } }
// Use this for initialization voidStart () { InputControllerManager.mInputControllerManager.RightButtonClickDelegate(MoveRight); InputControllerManager.mInputControllerManager.LeftButtonClickDelegate(MoveLeft); InputControllerManager.mInputControllerManager.UpButtonClickDelegate(MoveUp); InputControllerManager.mInputControllerManager.UpButtonOnPressDelegat(KeepMoveUp); InputControllerManager.mInputControllerManager.DownButtonClickDelegate(MoveDown); InputControllerManager.mInputControllerManager.DownButtonOnPressDelegat(KeepMoveDown); InputControllerManager.mInputControllerManager.ControlButtonClickDelegate(Jump);
StartCoroutine(MoveUpCoroutine()); StartCoroutine(MoveDownCoroutine()); } // Update is called once per frame voidUpdate () { }
privatevoidMoveRight(GameObject go) { if (mIsTweenComplete == true && mIsCrash == false) { Vector2 start = new Vector2(transform.position.x, transform.position.y); mTargetPosition = transform.position + mHorizontalOffset; Vector2 end = new Vector2(mTargetPosition.x, mTargetPosition.y); RaycastHit2D hit = Physics2D.Linecast(start, end, mBolockingLayer); if(hit.transform == null) { if (mPlayerCarAnimator != null) { mPlayerCarAnimator.SetTrigger("IsTurningRight"); } mIsTweenComplete = false; transform.DOMove(mTargetPosition, mMoveTweenTime).OnComplete(OnTweenComplete); } } }
privatevoidMoveLeft(GameObject go) { if (mIsTweenComplete == true && mIsCrash == false) { Vector2 start = new Vector2(transform.position.x, transform.position.y); mTargetPosition = transform.position - mHorizontalOffset; Vector2 end = new Vector2(mTargetPosition.x, mTargetPosition.y); RaycastHit2D hit = Physics2D.Linecast(start, end, mBolockingLayer); if (hit.transform == null) { if (mPlayerCarAnimator != null) { mPlayerCarAnimator.SetTrigger("IsTurningLeft"); } mIsTweenComplete = false; mTargetPosition = transform.position - mHorizontalOffset; transform.DOMove(mTargetPosition, mMoveTweenTime).OnComplete(OnTweenComplete); } } }
voidStart() { mEnemyCarTwoDimensionList = new List<List<GameObject>>();
for (int i = 0; i < mEnemyCar.Length; i++) { List<GameObject> enemycarlist = new List<GameObject>(mAmountForEachEnemyCar); for(int j = 0; j < mAmountForEachEnemyCar; j++) { GameObject enemycarobj = Instantiate(mEnemyCar[i]) as GameObject; enemycarobj.SetActive(false); enemycarlist.Add(enemycarobj); } mEnemyCarTwoDimensionList.Add(enemycarlist); } }
public GameObject GetEnemyCarObject(int carindex) { for (int i = 0; i < mEnemyCarTwoDimensionList[carindex].Count; i++) { if (!mEnemyCarTwoDimensionList[carindex][i].activeInHierarchy) { mEnemyCarTwoDimensionList[carindex][i].SetActive(true); return mEnemyCarTwoDimensionList[carindex][i]; } }
if (mWillGrow) { GameObject enemycar = Instantiate(mEnemyCar[carindex]) as GameObject; mEnemyCarTwoDimensionList[carindex].Add(enemycar); return enemycar; }
最终游戏截图(大部分游戏UI素材选至COC): 出于是学习使用旧版的NGUI,所以上述UI并没有实际的账号注册检查,只实现了简单的输入文字要求和账号对错(账号暂时是硬代码写的,PC上是通过读取Excel(使用的是ExcelReader,参考ExcelRead website),真机对应功能还没做)检查。 这一个主要是尝试Scroll Bar 这三个主要是尝试UIBUtton,UIPopupList,UIISlider,UIDraggable Panel的使用和TweenPosition制作简单UI动画(包含了对背景音乐和音量的设置功能,存储采取PlayerPrefs写入程序文件)。 最后这一个是本章2D赛车躲避的最终真机(Ipad mini 1)游戏截图,主要实现了赛车跳跃,前后左右移动,随着赛车数量躲避增加游戏速度增加,限制了移动范围。
编译打包XCode项目出问题 Failed to Copy File / Directory from ‘**\Unity\Editor\Data\Tools/MapFileParser/MapFileParser’ to ‘Temp/StagingArea\Trampoline\MapFileParser’. 这是Unity 5.1版本的一个bug。 解决方案 修改.\Unity\Editor\Data\Tools\MapFileParser\MapFileParser.exe到MapFileParser然后打包XCode项目,然后在XCode项目里把MapFileParser改回MapFileParser.exe 或者使用新版本Unity
编译打包XCode项目时库找不到的问题 ArgumentException: The Assembly System.Configuration is referenced by System.Data. But the dll is not allowed to be included or could not be found. 解决方案 Change it from .NET sub 2.0 to .NET
指定Android NDK的时候报错”Unable to detect NDK version, please pick a different folder” 解决方案: 需要下载特定版本的NDK 10r Edit -> Preference -> External Tool -> NDK download 或者 自己去下载后指定目录
XCode编译项目报错:MapParser.sh: bin/sh^M: bad interpreter: no such file or directory 解决方案 这是不同系统编码格式引起的:在windows系统中编辑的.sh文件可能有不可见字符,所以在Linux系统下执行会报以上异常信息。 使用dos2unix工具转换字符编码后放到Xcode项目里。(为了避免XCode生成每次都出这个问题,我们替换字符编码为Unix位于Unity引擎里的Editor->Data->PlaybackEngines->iOSSupport->Trampoline->MapFileParser.sh)
Unity Cloud Build error:”2015-12-10 17:21:27.407 xcodebuild[7363:75765] Failed to locate a valid instance of CoreSimulatorService in the bootstrap. Adding it now. Could not find service “com.apple.CoreSimulator.CoreSimulatorService” in domain for uid: 502 2015-12-10 17:21:27.431 xcodebuild[7363:75765] launchctl print returned an error code: 28928” 解决方案 从上面链接发现这是Xcode 7.2的一个bug,我们需要用Xcode 7.3,所以我们只需要把Unity Cloud Build设置到Xcode 7.3即可。
Unity Icloud Build打包出来的.ipa文件很大 一. 关闭Bitcode 关闭Bitcode Bitcode好像是Apple提交App后用于帮助优化的数据,IOS上是optional的,但watchOS and tvOS apps是必须的。
COC(Clash of Clans) is a freemium mobile MMO strategy video game developed and published by Supercell.The game was released for iOS platforms on 2 August, 2012,[1] and on Google Play for Android on 7 October, 2013 (from wiki)
The answer is 2D game,actually we should call 2.5D. 第一眼看到COC里面的所有动画人物给人的感觉都是3D的,但后来知道了Isometric Tileset Engine的概念。
Isometric Tileset Engine
What is Isometric Tileset Engine?
(斜视角游戏的地图渲染) (Isometric Tiles Introduction) 结合上述文章,我们可以知道,Isometric Tileset Engine主要是通过美术制作出Isometric Projection(we angle our camera along two axes (swing the camera 45 degrees to one side, then 30 degrees down))的2D图片来实现游戏的3D效果(2.5D)。
What does game with isometric projection look like?
典型的Isometric Projection游戏有: Age of Empires Diablo 2
UI的Render Mode主要分为三种 2.1 Screen Space – Overlay(Rendered on top of the secene) 2.2 Screen Space – Camera(有距离感的UI,受摄像机设置影响) 2.3 World Space(3D UI,有深度概念,会被3D物体遮挡)
voidStart() { mBBulletsList = new List<GameObject>(); mSBulletsList = new List<GameObject>();
for (int i = 0; i < mBBulletPoolAmount; i++) { GameObject bbulletobj = Instantiate(mBuildingBullet) as GameObject; bbulletobj.SetActive(false); mBBulletsList.Add(bbulletobj); }
for (int j = 0; j < mSBulletPoolAmount; j++) { GameObject sbulletobj = Instantiate(mSoldierBullet) as GameObject; sbulletobj.SetActive(false); mSBulletsList.Add(sbulletobj); } }
public GameObject GetBuildingBulletObject() { for (int i = 0; i < mBBulletsList.Count; i++) { if (!mBBulletsList[i].activeInHierarchy) { mBBulletsList[i].SetActive(true); return mBBulletsList[i]; } }
if (mWillGrow) { GameObject bbulletobj = Instantiate(mBuildingBullet) as GameObject; mBBulletsList.Add(bbulletobj); return bbulletobj; }
returnnull; }
public GameObject GetSoldierBulletObject() { for (int i = 0; i < mSBulletsList.Count; i++) { if (!mSBulletsList[i].activeInHierarchy) { mSBulletsList[i].SetActive(true); return mSBulletsList[i]; } }
if (mWillGrow) { GameObject sbulletobj = Instantiate(mSoldierBullet) as GameObject; mSBulletsList.Add(sbulletobj); return sbulletobj; }
[Serializable] publicclassSoldier : MonoBehaviour, GameObjectType { public SoldierState SCurrentState { set { if (mSCurrentState != null) { mSCurrentState.ExitState(); } mSCurrentState = value; mSCurrentState.EnterState(); } } [HideInInspector] private SoldierState mSCurrentState;
[HideInInspector] public SoldierAttackState mSAttackState;
[HideInInspector] public SoldierDeadState mSDeadState;
[HideInInspector] public SoldierMoveState mSMoveState;
......
publicvirtualvoidAwake() { mSAttackState = new SoldierAttackState(this);
mSMoveState = new SoldierMoveState(this);
mSDeadState = new SoldierDeadState(this);
...... }
publicvirtualvoidUpdate() { if (gameObject) { mSCurrentState.UpdateState(); } }
...... }
Decision Trees(决策树 – 用于简单的AI(Decision making)) Decision Trees主要用于AI体做决策,通过对已知数据的分析判断,根据Decision Tree抉择出最终的决定(即AI行为)。(项目里我主要使用FSM而非Decision Trees) 下图来源:《Artificial Intelligence for Game》 – Ian Millington
A Star(A Star是 Dijkstra(著名的最短路径算法)基础上通过一个启发因子来预估给定节点到目标节点的距离来使得路径节点搜索是向目标节点方向逼近不至于出现搜索大量无效节点的情况) (AI相关学习)
A Star
Searching
通过搜索,我发现网络上有现成的很完善的A Star的版本 A Star Pathfinding Project(Asset) 但通过使用后发现,里面所支持的Four,Six and Eight connections都不符合我的需求(每个点都和周围的八个点连通),所以最终放弃了A Star Pathfinding Project而决定自己实现自己的A Star Pathfinding
Create Myself A Star
A Star Preperation
结合Artificial-Inteligence-Study的学习,让我们了解下A Star里的一些基本概念和核心思想: A Star属于什么图? A Star里的图属于导航图(Navigation Graph),是基于开销的图搜索(cost-based graph searches)
Dijstra算法改进: A Star – 和Dijkstra算法的唯一区别是对搜索边界上的点的开销(GCost)的计算。因为Dijstra的搜索扩展方向是由GCost决定的,所以A Star算法通过给GCost添加一个启发因子(H)来确保搜索行进方向。 F的计算: F = G + H G是到达一个节点的累计开销, H是一个启发因子,它给出的是节点到目标节点的估计距离。
using UnityEngine; using System.Collections; using System.Collections.Generic; using System; using UnityEngine.Assertions;
publicclassSearchAStar { publicstruct PathInfo {
public List<int> PathToTarget { get { return mPathToTarget; } set { mPathToTarget = value; } } private List<int> mPathToTarget;
public List<Vector3> MovementPathToTarget { get { return mMovementPathToTarget; } set { mMovementPathToTarget = value; } } private List<Vector3> mMovementPathToTarget;
publicbool IsWallInPathToTarget { get { return mIsWallInPathToTarget; } set { mIsWallInPathToTarget = value; } } privatebool mIsWallInPathToTarget;
publicint WallInPathToTargetIndex { get { return mWallInPathToTargetIndex; } set { mWallInPathToTargetIndex = value; } } privateint mWallInPathToTargetIndex;
publicfloat CostToTarget { get { return mCostToTarget; } set { mCostToTarget = value; } } privatefloat mCostToTarget;
publicint ITarget { set { mITarget = value; } get { return mITarget; } } privateint mITarget;
publicint OriginalTarget { get { return mOriginalTarget; } set { mOriginalTarget = value; } } privateint mOriginalTarget;
publicint NodesSearched { get { return mNodesSearched; } set { mNodesSearched = value; } } privateint mNodesSearched;
publicint EdgesSearched { get { return mEdgesSearched; } set { mEdgesSearched = value; } } privateint mEdgesSearched;
//this list of edges is used to store any subtree returned from any of the graph algorithms /* public List<GraphEdge> SubTree { get { return mSubTree; } set { mSubTree = value; } } private List<GraphEdge> mSubTree; */ /* public PathInfo() { ResetPathInfo(); } */
public PathInfo DeepCopy() { PathInfo pi = (PathInfo)this.MemberwiseClone(); pi.PathToTarget = new List<int>(mPathToTarget); pi.MovementPathToTarget = new List<Vector3>(mMovementPathToTarget);
if (!mIsIgnoreWall) { //No matter the wall in path is jumpable or not, we should record it as useful information if (mGraph.Nodes[nd].IsWall /*&& !mGraph.Nodes[nd].IsJumpable*/) { mAStarPathInfo.IsWallInPathToTarget = true; mAStarPathInfo.WallInPathToTargetIndex = nd; } }
while (!mPQ.Empty()) { //Get lowest cost node from the queue int nextclosestnode = mPQ.Pop().Key;
mAStarPathInfo.NodesSearched++;
//move this node from the frontier to the spanning tree if (mSearchFrontier[nextclosestnode] != null && mSearchFrontier[nextclosestnode].IsValidEdge()) { mShortestPathTree[nextclosestnode] = mSearchFrontier[nextclosestnode]; } //If the target has been found exit if (nextclosestnode == mITarget) { return; }
//Now to test all the edges attached to this node List<GraphEdge> edgelist = mGraph.EdgesList[nextclosestnode]; GraphEdge edge; for (int i = 0; i < edgelist.Count; i++) { edge = edgelist[i]; //calculate the heuristic cost from this node to the target (H) float hcost = Heuristic_Euclid.Calculate(mGraph, mITarget, edge.To) * mHCostPercentage;
//calculate the 'real' cost to this node from the source (G) float gcost = mGCosts[nextclosestnode] + edge.Cost;
//if the node has not been added to the frontier, add it and update the G and F costs if (mSearchFrontier[edge.To] != null && !mSearchFrontier[edge.To].IsValidEdge()) { mFCosts[edge.To].Value = gcost + hcost; mGCosts[edge.To] = gcost;
mPQ.Push(mFCosts[edge.To]);
mSearchFrontier[edge.To] = edge;
mAStarPathInfo.EdgesSearched++;
if (mBDrawExplorePath) { Debug.DrawLine(mGraph.Nodes[edge.From].Position, mGraph.Nodes[edge.To].Position, Color.yellow, mExplorePathRemainTime); } }
//if this node is already on the frontier but the cost to get here //is cheaper than has been found previously, update the node //cost and frontier accordingly elseif (gcost < mGCosts[edge.To]) { mFCosts[edge.To].Value = gcost + hcost; mGCosts[edge.To] = gcost;
//Due to some node's f cost has been changed //we should reoder the priority queue to make sure we pop up the lowest fcost node first //compare the fcost will make sure we search the path in the right direction //h cost is the key to search in the right direction mPQ.ChangePriority(edge.To);
while (!mPQ.Empty()) { //Get lowest cost node from the queue int nextclosestnode = mPQ.Pop().Key;
mAStarPathInfo.NodesSearched++;
//move this node from the frontier to the spanning tree if (mSearchFrontier[nextclosestnode] != null && mSearchFrontier[nextclosestnode].IsValidEdge()) { mShortestPathTree[nextclosestnode] = mSearchFrontier[nextclosestnode]; }
//Now to test all the edges attached to this node List<GraphEdge> edgelist = mGraph.EdgesList[nextclosestnode]; GraphEdge edge; for (int i = 0; i < edgelist.Count; i++) { edge = edgelist[i]; //calculate the heuristic cost from this node to the target (H) float hcost = Heuristic_Euclid.Calculate(mGraph, mITarget, edge.To) * mHCostPercentage;
//calculate the 'real' cost to this node from the source (G) float gcost = mGCosts[nextclosestnode] + edge.Cost;
//if the node has not been added to the frontier, add it and update the G and F costs if (mSearchFrontier[edge.To] != null && !mSearchFrontier[edge.To].IsValidEdge()) { mFCosts[edge.To].Value = gcost + hcost; mGCosts[edge.To] = gcost;
mPQ.Push(mFCosts[edge.To]);
mSearchFrontier[edge.To] = edge;
mAStarPathInfo.EdgesSearched++;
if (mBDrawExplorePath) { Debug.DrawLine(mGraph.Nodes[edge.From].Position, mGraph.Nodes[edge.To].Position, Color.yellow, mExplorePathRemainTime); } }
//if this node is already on the frontier but the cost to get here //is cheaper than has been found previously, update the node //cost and frontier accordingly elseif (gcost < mGCosts[edge.To]) { mFCosts[edge.To].Value = gcost + hcost; mGCosts[edge.To] = gcost;
//Due to some node's f cost has been changed //we should reoder the priority queue to make sure we pop up the lowest fcost node first //compare the fcost will make sure we search the path in the right direction //h cost is the key to search in the right direction mPQ.ChangePriority(edge.To);
mSearchFrontier[edge.To] = edge;
mAStarPathInfo.EdgesSearched++; } } } }
//The A* search algorithm with strickdistance with wall consideration privatevoidSearch(float strickdistance, bool isignorewall) { float currentnodetotargetdistance = Mathf.Infinity;
mPQ.Clear();
mPQ.Push(mFCosts[mISource]);
//mSearchFrontier [mISource] = new GraphEdge (mISource, mISource, 0.0f); mSearchFrontier[mISource].From = mISource; mSearchFrontier[mISource].To = mISource; mSearchFrontier[mISource].Cost = 0.0f; GraphEdge edge = new GraphEdge(); int nextclosestnode = -1;
while (!mPQ.Empty()) { //Get lowest cost node from the queue nextclosestnode = mPQ.Pop().Key;
mAStarPathInfo.NodesSearched++;
//move this node from the frontier to the spanning tree if (mSearchFrontier[nextclosestnode] != null && mSearchFrontier[nextclosestnode].IsValidEdge()) { mShortestPathTree[nextclosestnode] = mSearchFrontier[nextclosestnode]; }
//Now to test all the edges attached to this node List<GraphEdge> edgelist = mGraph.EdgesList[nextclosestnode]; for (int i = 0; i < edgelist.Count; i++) { //Avoid pass refrence edge.Reset(); edge.From = edgelist[i].From; edge.To = edgelist[i].To; edge.Cost = edgelist[i].Cost; //calculate the heuristic cost from this node to the target (H) float hcost = Heuristic_Euclid.Calculate(mGraph, mITarget, edge.To) * mHCostPercentage;
//calculate the 'real' cost to this node from the source (G) float gcost = 0.0f; if (isignorewall) { gcost = mGCosts[nextclosestnode] + edge.Cost;
if (mGraph.Nodes[edge.From].IsWall) { gcost -= mGraph.Nodes[edge.From].Weight; } if (mGraph.Nodes[edge.To].IsWall) { gcost -= mGraph.Nodes[edge.To].Weight; } } else { gcost = mGCosts[nextclosestnode] + edge.Cost; if (mGraph.Nodes[edge.From].IsJumpable) { gcost -= mGraph.Nodes[edge.From].Weight; } if (mGraph.Nodes[edge.To].IsJumpable) { gcost -= mGraph.Nodes[edge.To].Weight; } }
//if the node has not been added to the frontier, add it and update the G and F costs if (mSearchFrontier[edge.To] != null && !mSearchFrontier[edge.To].IsValidEdge()) { mFCosts[edge.To].Value = gcost + hcost; mGCosts[edge.To] = gcost;
mPQ.Push(mFCosts[edge.To]);
mSearchFrontier[edge.To].ValueCopy(edge);
mAStarPathInfo.EdgesSearched++;
if (mBDrawExplorePath) { Debug.DrawLine(mGraph.Nodes[edge.From].Position, mGraph.Nodes[edge.To].Position, Color.yellow, mExplorePathRemainTime); } }
//if this node is already on the frontier but the cost to get here //is cheaper than has been found previously, update the node //cost and frontier accordingly elseif (gcost < mGCosts[edge.To]) { mFCosts[edge.To].Value = gcost + hcost; mGCosts[edge.To] = gcost;
//Due to some node's f cost has been changed //we should reoder the priority queue to make sure we pop up the lowest fcost node first //compare the fcost will make sure we search the path in the right direction //h cost is the key to search in the right direction mPQ.ChangePriority(edge.To);
盲目搜索(Uniformed Graph Searches) (在搜索一个图时不考虑相关的边的开销) a. 深度优先搜索(DFS: Depth First Search) (搜索时尽可能地深入一个图。在搜索时,当它走入死胡同时,才会回溯,以回到上一个较浅的节点,在那里继续深度搜索) 注意: 使用Stack来模拟,先进后出(FILO)的原则 DFS优化: 一些图可能非常深,深度优先便可能非常容易地就在错误的路径上陷得很深,因而延误了搜索。 限制深度的搜索(Limited Search): 限制深度优先搜索算法在开始回溯之前可以进行多少步的深度搜索 缺点: 如何设置最大搜索深度 迭代加深深度优先搜索(Iterative Deepending Depth First Search) b. 广度优先搜索(BFS:Breadth First Search) (从源节点展开以检查从它出发的边指向的每一个节点,然后再从那些刚检查过的节点继续展开) 注意: 使用Queue来模拟,先进先出(FIFO)的原则 BFS缺点: 如果搜索的图非常大而且分支数很高,那么BFS就会浪费大量的内存并且表现出很低的效率。
基于开销的图搜索(cost-based graph searchs) a. 边放松(Edge Relaxation) (从源节点到抵达目标节点的路径上的所有其他节点中搜集当前最优路径(BPFSF: Best Path Found So Far)信息。这个信息在检查新的边时得到更新。如果刚检查的边表明,如果用通过此边到达一个节点的路径取代现有的最优路径会使路程更短,那么,这条边就 被加入,而路径也相应地更新) b. 最短路径树(SPT: Short Path Tree) (从任何节点到达源节点的最短路径) c. Dijkstra算法(Dijkstra Algorithm) (教授Edsger Wybe Dijkstra著名的寻找带全图的最短路径算法) 注意: 使用一个索引的优先队列(Indexed Priority Queue)来实现。 缺点: Dijkstra算法检查了太多的边。 d. Dijkstra算法的一个改进:A算法 (Dijkstra算法通过最小化开销进行搜索。在处理搜索边界上的点时,如果估计一下他们距离目标节点的开销,并将这个信息考虑进去,那么算法的效率就可以大大提高。这个估计值被称为启发因子。) 注意: A算法和Dijkstra算法的唯一区别是对搜索边界上的点的开销的计算。被修正的到节点的开销F用来决定节点在优先队列中的位置。 F的计算: F = G + H G是到达一个节点的累计开销, H是一个启发因子,它给出的是节点到目标节点的估计距离。 通过用这种方法使用一个启发因子,被修正的开销会指引搜索逼近目标节点,而不是在各个可能的方向发散的搜索。这也使需要检查的边更少。因此搜索的加速是Disjkstra算法和A算法的最主要区别。 A算法的实现需要维护两个用来存储开销的std::vector,一个用来保存每一个节点的F开销,作为优先队列的索引,另一个是每一个节点的G开销。
《Artificial Intelligence for Game》 – Ian Millington 1. Introduction 1.1 What is AI? Artificial intelligence is about making computers able to perform the thinking tasks that humans and animals are capable of
1.1.2 Game AI Pacman [Midway Games West, Inc, 1979] – state machine Warcraft –[Blizzard Entertainment, 1994] – Path finding
AI three basic needs: 1. Move Movement refers to algorithms that turn decisions into some kind of motion 2. Decision making Decision making involves a characterworking out what to do next 3. Strategy Strategy refers to an overall approach used by a group of characters – e.g. Harf-Life Note: Not all game applications require all levels of AI
1.2.5 Agent-Based AI Agent-based AI is about producing autonomous characters that take in information from the game data, determine what actions to take based on the information, and carry out those actions
1.3 Algorithms, Data Structures, and Representations 1.3.1 Algorithms
Tactical and Strategic AI 6.1 Waypoint Tactics 6.1.1 Tactical Locations
参考书籍: 《OpenGL Programming Guide 8th Edition》 – Addison Wesley 《Fundamentals of Computer Graphics (3rd Edition)》 – Peter Shirley, Steve Marschnner 《Real-Time Rendering, Third Edition》 – Tomas Akenine-Moller, Eric Haines, Naty Hoffman
Rendering Knowledge
在进入书籍内容的学习之前,先了解一些必要的知识
What is Rendering?
“Rendering is a process that takes as its input a set of objects and produces as its output an array of pixels.” – 《Fundamentals of Computer Graphics (3rd Edition)》
既然Rendering是通过处理一系列的对象数据最终输出成一组一组的像素呈现出来,那接下来的问题就是What is the rendering process(Graphic Pipeline)?
Application e.g collision detection, global acceleration algorithms, animation, physics simulation….. (On CPU) Acceleration algorithms, such as hierarchical view frustum culling, are also implemented in this stage. – 《Real-Time Rendering, Third Edition》 可以看出Application阶段主要是做一些非渲染相关的一些计算,但部分计算也可以帮助我们减少渲染数量提高渲染效率(比如:hierachical view frustum culling)
Geometry Deal with transforms, projection. Computes what is to be draw, how it should be drawn, and where it should be drawn (On GPU) e.g. model and view transform, vertex shading, projection, clipping, and screen mapping – 《Real-Time Rendering, Third Edition》 可以看出Geometry阶段主要是负责3D到2D Screen的顶点运算和顶点剔除
Rasterizer Conversion from two-dimensional vertices in screen space – each with a z-value (depth value), and various shading information associated with each vertex – into pixels on the screen (On GPU) – 《Real-Time Rendering, Third Edition》 可以看出Rasterizer阶段主要是负责2D Screen的顶点像素运算(包括Z-Buffer test, Color computation, Alpha test, Stencil buffer等)
Accumulation Buffer – Images can be accumulated using a set of operators. E,g, motion blur……
那么什么是Shader(着色器)?Shader(着色器)和GPU硬件的关系是怎样的了? “Shaders are programmed using C-like shading languages such as HLSL, Cg and GLSL. These are compiled to a machine-independent assembly language, also called the intermediate language(IL). This assembly language is converted to the actual machine language in a separate step, usually in the drivers. This arrangement allows compatibility across different hardware implementations.” – Real-Time Rendering, Third Edition》 GPU的像素着色器单元和顶点着色器单元就对应了Shader里面的Pixel Shader和Vertex Shader。由于像素在计算机都是以RGB三种颜色构成,加上Alpha总共4个通道,所以GPU的像素着色器单元和顶点着色器单元一开始就被设计成为同时具备4次运算能力的算数逻辑运算器(ALU)
从上面可以看出Shader是machine-independent的,因为事先被编译成了与机器无关的intermediate language(This intermediate language can be seen as defining a virtual machine, which is targeted by the shading language compiler – 《Real-Time Rendering, Third Edition》), 最后才会被机器转换成机器码来运行.
可编程Shading进化史可参考 3.3 The Evolution of Programmable Shading – 《Real-Time Rendering, Third Edition》
了解了计算机架构与Shader的关系,让我们来看看Common-shader core virtual machine architecture and register layout吧
Input Type
Uniform inputs With values that remain constant throughout a draw call(but can be changed between draw calls) – accessed via read-only constant registers or constant buffers
Varying inputs Which are different for each vertex or pixel processed by the shader – accessed via varying input registers
Shader Knowledge
Shader
Shader programs can be compiled offline before program load or during run time. As with any compiler, there are options for generating different output files and for using different optimization levels. A compiled shader is stored as a string of text, which is passed to the GPU via the driver.
OpenGL is an application programming interface – “API” for short – which is merely a software library for accessing features in graphics hardware.(访问图形硬件设备功能的API)
OpenGL is a “C” language library(OpenGL是一个C语言库)
History
It was first developed at Silicon Graphics Computer Systems with Version 1,0 released in July of 1994(wiki)
Vertex Shader Process the data associated with that vertex
Tessellation Shader Tessellation uses patchs to describe an object’s shape, and allows relatively simple collections of patch geometry to be tessellated to increase the number of geometric primitives providing better-looking models (eg: LOD)
Geometry Shader Allows additional processing of individual geometric primitives, including creating new ones, before rasterization
Primitive Assembly Organizes the vertices into their associated geometric primitives in preparation for clipping and rasterization
Clipping Clip the vertex and pixels are outside of the viewport – this operation is handled automatically by OpenGL
Rasterization Fragment generation. Pixels have a home in the framebuffer, while a fragment still can be rejected and never update its associated pixel location.
Fragment Shading Use a fragment shading to determine the fragment’s final color, and potentially its depth value
Per-Fragment Operations If a fragment successfully makes it through all of the enabled tests (eg: depth testing, stencil testing), it may be written directly to the framebuffer, updating the color of its pixel, or if blending is enabled, the fragment’s color will be combined with the pixel’s current color to generate a new color that is written into the framebuffer
Note: Fragment’s visibility is determined using depth testing and stencil testing Pixel data is usually stored in texture map for use with texture mapping, which allows any texture stage to look up data values from one or more texture maps.
GLSL - OpenGL Shading Language 也称作 GLslang,是一个以C语言为基础的高阶着色语言。它是由 OpenGL ARB 所建立,提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言。
编译和执行 GLSL 着色器不是独立的应用程式;其需要使用 OpenGL API 的应用程式。C、C++、C#、Delphi 和 Java 皆支援 OpenGL API,且支援 OpenGL 着色语言。 GLSL 着色器本身只是简单的字串集,这些字串集会传送到硬件厂商的驱动程式,并从程式内部的 OpenGL API 进入点编译。着色器可从程式内部或读入纯文字档来即时建立,但必须以字串形式传送到驱动程式。
工具 GLSL 着色器可以事先建立和测试,现有以下 GLSL 开发工具: RenderMonkey - 这个软件是由 ATI 制作的,提供界面用以建立、编译和除错 GLSL 着色器,和 DirectX 着色器一样。仅能在 Windows 平台上执行。 GLSLEditorSample - 在 Mac OS X 上,它是目前唯一可用的程式,其提供着色器的建立和编译,但不能除错。它是 cocoa 应用程式,仅能在 Mac OS X 上执行。 Lumina - Lumina 是新的 GLSL 开发工具。其使用 QT 界面,可以跨平台。
The color space in OpenGL
In OpenGL, colors are represented in what’s called the RGB color space
以下是一个材质库文件的基本结构:
newmtl mymtl_1
材质颜色光照定义
纹理贴图定义
反射贴图定义
……
注释:每个材质库可含多个材质定义,每个材质都有一个材质名。用newmtl mtlName来定义一个材质。对于每个材质,可定义它的颜色光照纹理反射等描述特征。主要的定义格式如下文所示:
////////////////////////////////////////////////
材质颜色光照
1。环境反射有以下三种描述格式,三者是互斥的,不能同时使用。
Ka r g b ——用RGB颜色值来表示,g和b两参数是可选的,如果只指定了r的值,则g和b的值都等于r的值。三个参数一般取值范围为0.0~1.0,在此范围外的值则相应的增加或减少反射率;
Ka spectral file.rfl factor ——用一个rfl文件来表示。factor是一个可选参数,表示.rfl文件中值的乘数,默认为1.0;
Ka xyz x y z ——用CIEXYZ值来表示,x,y,z是CIEXYZ颜色空间的各分量值。y和z两参数是可选的,如果只指定了x的值,则y和z的值都等于r的值。三个参数一般取值范围为0~1。
2。漫反射描述的三种格式:
Kd r g b
Kd spectral file.rfl factor
Kd xyz x y z
3。镜反射描述的三种格式:
Ks r g b
Ks spectral file.rfl factor
Ks xyz x y z
4。滤光透射率描述的三种格式:
Tf r g b
Tf spectral file.rfl factor
Tf xyz x y z
5。光照模型描述格式:
illum illum_#
指定材质的光照模型。illum后面可接0~10范围内的数字参数。各个参数代表的光照模
“OpenGL Execute Model: The model for interpretation of OpenGL commands is client-server. An application (the client) issues commands, which are interpreted and processed by OpenGL (the server). The server may or may not operate on the same computer as the client. In this sense, OpenGL is network-transparent. “
“client-server 模式: OpenGL 是一种 client-server 模式,当你的应用程序调用 OpenGL 函数时, 它将告诉OpenGL client, 然后 client 将渲染命令传送给 server. 这里client 和 server可能是不同的计算机,或是同一台计算机上的不同进程。一般来说 server 是在 GPU上处理的, 而 client 是在 CPU 上处理的,这样分担了 CPU 的负担, 同时高效利用了GPU.”
但如果Client和Server没在同一个机器上,我们就需要一种一种网络传输协议框架来实现他们之间的交流: X Window System
GLUT是由Mark J. Kilgard在Silicon Graphics工作时所写,此人同时也是OpenGL Programming for the X Window System以及The Cg Tutorial: The Definitive Guide to Programmable Real-Time Graphics两书的作者。
Glew ( OpenGL Extension Wrangler Library) The OpenGL Extension Wrangler Library (GLEW) is a cross-platform C/C++ library that helps in querying and loading OpenGL extensions. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. All OpenGL extensions are exposed in a single header file, which is machine-generated from the official extension list.(Glew是一个支持跨平台的C/C++库,用于运行时鉴别OpenGL扩展所支持的版本) more info for extention tools
How to use Glu & Glew?
add glu.lib & glew.lib into additional dependencies
add the directory that includes glu.h & glew.h into include dirctory
Include GL/freeglut.h & GL/glew.h in source file Note: if you use static link, #define FREEGLUT_STATIC before you include GL/freeglut.h, otherwise it will look for freeglut.lib. #define GLEW_STATIC for Glew.
include GL/glew.h before GL/freeglut.h, otherwise, it will through “fatal error C1189: #error : gl.h included before glew.h”
V’ = V × M(m-w) × M(w-v) × M(v-p) 我们知道了M(m-w)是如何计算出来的了,接下来我们要了解M(w-v) – 世界坐标系到观察坐标系 在了解如何从世界坐标系转换到观察坐标系之前我们先来看看摄像机的定义: 位置 – (x,y,z) N – The vector from the camera to its target.(look at 朝向) V – When standing upright this is the vector from your head to the sky.(垂直于N向上的向量) U – This vector points from the camera to its “right” side”.(在N和V定了之后可以算出Camera的向右的向量)
后来看了《Mathematics for 3D Game Programming and Computer Grahpics 3rd section》的5.4.1 Depth Interpolation后明白了,光栅化的时候对于深度的运算证明了是对Z的倒数进行插值来得到Z的值的。 所以上述公式是成立的。
“Textures are composed of texels, which often contain color values.”
“Textures are bound to the OpenGL context via texture units, which are represented as binding points named GL_TEXTURE0 through GL_TEXTUREi where i is one less than the number of texture units supported by the implementation.”
The textures are accessed via sampler variables which were declared with dimensionality that matches the texture in shader
在真正接触Texutre之前,让我们理解下下列几个重要的概念:
Texture object – contains the data of the texture image itself, i.e. the texels(可以看出Texture object才是含有原始数据信息的对象)
Texture unit – texture object bind to a ‘texture unit’ whose index is passed to the shader. So the shader reaches the texture object by going through the texture unit.(我们访问texture数据信息并不是通过texture object,而是在shader里通过访问特定索引的texture unit去访问texture object里的数据)
Sampler Object – configure it with a sampling state and bind it to the texture unit. When you do that the sampler object will override any sampling state defined in the texture object.(Sampler Object一些sampling的配置信息,当用于texture object时会覆盖texture object里的原始sampler设定)
Sampler uniform – corresponding to handle of texture unit(用于在Shader里访问texture unit,texture unit和texture object绑定,也就间接的访问了texture的原始数据)
Relationship between texture object, texture unit, sampler object and sampler uniform
Create a texture object and load texel data into it glGenTextures() – gen texture object glBindTexture() – Tells OpenGL the texture object we refer to in all the following texture related calls, until a new texture object is bound. glTexImage2D() – load texel data into texture object
Associate a texture sampler with each texture map you intend to use in your shader glTexParameterf() – Texture采样方式的配置 还记得我们之前讲到的Sampler object吗?这里的配置就好比我们在sampler object里配置后再作用于特定的texture object 这里我就不说关于采样方式配置相关的内容了(采样方式会决定最终像素的计算方式),这里值得一提的是mipmap的概念。 mipmap主要是由于物体在场景中因为距离的缘故会在屏幕显示的大小有变化,如果我们在物体很远,只需要显示很小一块的时候还依然采用很大的纹理贴图,最终显示在屏幕上的纹理会很不清晰(失真)。为了解决这个问题,mipmap应运而生,通过事先生成或指定多个级别的同一纹理贴图,然后在程序运作的过程中通过计算算出应该使用哪一个等级的纹理贴图来避免大纹理小色块失真的问题。 我们可以手动通过: glTexStorage2D() && glTexSubImage2D() 去手动指定各级纹理贴图 也可以通过: glGenerateMipmap() – 自动去生成对应的mipmap纹理贴图 而程序在实际运作过程中如何去计算Mipmap Level这里就不做介绍了,详细参考《OpenGL Programming Guide 8th Edition》的Calculating the Mipmap章节 相关函数: textureLod() textureGrad()
Active texture unit and bind texture object to it glActiveTexture() – 激活特定的texture unit然后绑定特定texture object到特定texture unit上 glBindTexture() – 绑定特定的texture object到texture unit上
Retrieve the texel values through the texture sampler from your shader 首先我们在程序中指定了我们即将访问的Texture unit
Note: “The important thing to note here is that the actual index of the texture unit is used here, and not the OpenGL enum GL_TEXTURE0 (which has a different value).”
vsshader.vs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#version 330
uniform mat4 gWVP;
layout (location = 0) in vec3 Position; layout (location = 1) in vec2 TexCoord;
Use immutable texture storage for textures wherever possible – When a texture is marked as immutable, the OpenGL implementation can make certain assumptions about the validity of a texture object (尽量使用不可变的texture storage, 这样OpenGL可以确保texture的有效性)
Create and initialize the mipmap chain for textures unless you have a good reason not to – improve the image quality of your program’s rendering, but also will make more efficient use of the caches in the graphics processor (为了渲染效率,减轻GPU负担,尽可能为texture创建mipmap)
Use an integer sampler in your shader when your texture data is an unnormalized integer and you intend to use the integer values it contains directly in the shader (尽量在shader里使用integer类型的sampler)
Note: “The maximum number of texture units supported by OpenGL can be determined by retrieving the value of the GL_MAX_COMBINED_ TEXTURE_IMAGE_UNITS constant, which is guaranteed to be at least 80 as of OpenGL 4.0.”
Proxy texture – used to test the capabilities of the OpenGL implementation when certain limits are used in combination with each other.
接下来我们看一下Spot Light: Spot Light和Point Light的主要区别在于,Spot Light定义了一个可影响的范围Cone和其垂直照射的方向。 而这个Cone通过Cutoff来定义: Cutoff – “ The cutoff value represents the maximum angle between the light direction and the light to pixel vector for pixels that are under the influence of the spot light.” 见下图:
More Advanced Lighting Model: Hemisphere Lighting: The idea behind hemisphere lighting is that we model the illumination as two hemispheres. The upper hemisphere represents the sky and the lower hemisphere represents the ground
Imaged-Based Lighting: “It is often easier and much more efficient to sample the lighting in such environments and store the results in one or more environment maps”
Lighting with Spherical Harmonics: “This method reproduces accurate diffuse reflection, based on the content of a light probe image, without accessing the light probe image at runtime”
接下来我们看一个真实渲染过程中比较重要的技术 – Shadow Mapping Shadow Mapping – Uses a depth texture to determine whether a point is lit or not.
Shadow mapping is a multipass technique that uses depth textures to provide a solution to rendering shadows (核心思想是通过比较通过光源点观察保存的深度信息(depth texture)和从观察点观察的深度信息来判断哪些点是shadowed,哪些是unshadowed – 注意比较的是通过映射到2D depth texture后的信息) A key pass is to view the scene from the shadow-casting light source rather than from the final viewpoint Two passes:
First Pass Shadow map – by rendering the scene’s depth from the point of the light into a depth texture, we can obtain a map of the shadowed and unshadowed points in the scene 在第一个pass中,我按照事例代码中写了,但发现最后显示的是纯白色的图像。 后来就不断去查问题。 首先,我怀疑depth texture是不是没有生成成功?
通过上图,我发现我自己的代码有三个Texture被生成,但Demo只有两个,并且我自己写的代码Enable的并非FBO 1绑定生成的Texture而是第三个Texture,所以这是我怀疑我在加载Mesh的时候加载了第三个Texture并将其绑定在了Texture unit 0,而我碰巧激活了这一个Texture unit。
// Load a white texture in case the model does not include its own texture if (!m_Textures[i]) { m_Textures[i] = newTexture(GL_TEXTURE_2D, "../Content/white.png");
Ret = m_Textures[i]->Load(); }
...... }
voidMesh::Render() { .....
if (MaterialIndex < m_Textures.size() && m_Textures[MaterialIndex]) { m_Textures[MaterialIndex]->Bind(GL_TEXTURE0); }
//Set uniform variable value glUniformMatrix4fv(gWVPLocation, 1, GL_TRUE, (const GLfloat*)p.GetWVPTrans());
gPTank->Render();
glBindFramebuffer(GL_FRAMEBUFFER, 0); }
Second Pass Rendering the scene from the point of view of the viewer. Project the surface coordinates into the light’s reference frame and compare their depths to the depth recorded into the light’s depth texture. Fragments that are further from the light than the recorded depth value were not visible to the light, and hence in shadow
A skybox is a method of creating backgrounds to make a computer and video games level look bigger than it really is. When a skybox is used, the level is enclosed in a cuboid. (From wiki)
在OpenGL中实现Skybox是通过Cubemap。
In order to sample from the cubemap we will use a 3D texture coordinate instead of the 2D coordinate
Skydome – A skybox which uses a sphere is sometimes called a skydome.
Note: “An interesting performance tip is to always render the skybox last (after all the other models). The reason is that we know that it will always be behind the other objects in the scene.”
What is Normal Mapping? “Normal Mapping也叫做Dot3 Bump Mapping,它也是Bump Mapping的一种,区别在于Normal Mapping技术直接把Normal存到一张NormalMap里面,从NormalMap里面采回来的值就是Normal,不需要像HeightMap那样再经过额外的计算。”
Note: 在实际开发中我们并非一定要手动写代码运算,比如”Open Asset Import Library就支持flag called ‘aiProcess_CalcTangentSpace’ which does exactly that and calculates the tangent vectors for us”
Normal Map也通过工具可以生成,比如3D Max, Maya, 教程里用的GNU Image Manipulation Program (GIMP)…….
Parallax Mapping首先在一篇名为“Detailed Shape Representation with Parallax Mapping”的文章中提出。它的基本思想如下图示(本图来自Parallax Mapping with Offset Limiting: A PerPixel Approximation of Uneven Surfaces)。在图示的视线方向,如果表面是真正的凹凸不平的,如real surfacer所示,那么能看到的是B点,因此用于采样法线的正确纹理坐是TB而不是TA。 因此,我们需要对纹理坐标作偏移,为了满足实时渲染的要求,采用了取近似偏移的方法(如下图示),这种近似的算法已经可以达到比较好的效果。具体的offset计算可以参考:“Parallax Mapping with Offset Limiting: A PerPixel Approximation of Uneven Surface”,里面有详细的讲解。
Geometry shader(Optional) “The geometry shader sits logically right before primitive assembly and fragment shading.”
Receives as its input complete primitives as a collection of vertices, and these inputs are represented as array (Geometry shader接收完整图形的顶点集合,这些顶点集合在geometry shader中通过gl_in[]数组的方式访问)
gl_in的声明:
1 2 3 4 5
in gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; }gl_in[];
Geometry Features:
Producing Primitives They can have a different primitive type for their output than they do for their input. (EG: wireframe rendering, billboards and even interesting instancing effects)(Billboard效果见后面)
Culling Geometry Selective culling (geometry shader通过对特定的gl_PrimitiveIDIn进行生成特定的primitive实现selective culling) “gl_PrimitiveIDIn is a geometry language input variable that holds the number of primitives processed by the shader since the current set of rendering primitives was started.”
Geometry Amplification Produces more primitives on its output than it accepts on its input (can be used to implement fur shells or moderate tessellation – 因为可以对传入的primitive数据进行处理并生成多个primitive,所以能通过复制并改变primitive的信息数据来实现毛发等效果) Gl_MaxGeometryOutputVertices & glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES) 毛发效果(来源OpenGL红宝书第八版):
Geometry Shader Instance Only runs the geometry shader and subsequent stages (rasterization) multiple times, rather than the whole pipeline (Geometry shader instancing draw call是通过运行多次geometry和rasterization和fragment来实现的) Geometry shader instancing is enabled in the shader by specifying the invocations layout qualifier
1 2
//gl_InvocationID identifies the invocation number assigned to the geometry shader invocation. layout (triangles, invocations = 4) in; //invocations = 4 indicates that the geometry shader will be called 4 times for each input primitives
Multiple Viewport Rendering gl_ViewportIndex (output variables available in the geometry shader that can redirect rendering into different regions of the framebuffer) gl_ViewportIndex is used to specify which set of viewport parameters will be used to perform the viewport transformation by OpenGL “ (Multiple viewport concept (多个视图窗口) – 这里主要是通过gl_ViewportIndex访问多个viewport,然后在geometry shader中通过指定primitive输出到特定的viewport来实现多个视图窗口) glViewportIndexedf() or glViewportIndexedfv() – specify how window x and y coordinates are generated from clip coordinates glDepthRangeIndexed() – specify how the window z coordinate is generated 效果展示(这里展示的OpenGL红宝书第八版的例子):
Layer Rendering It is also possible to use a 2D array texture as a color attachment and render into the slices of the array using a geometry shader (传入2D的纹理数组数据当做color attachment,通过geometry shader把传入的2D纹理数组信息去渲染多个slices) A restriction exits when using layered attachments to framebuffer: (使用layered attachment到framebuffer的规则): All the attachments of that framebuffer must be layered (framebuffer的所有attachment都必须是layered) Also, all attachments of a layered framebuffer must be of the same type (所有attach到layered framebuffer的attachment必须是同样类型) gl_Layer – built in variable in geometry shader – that is used to specify the zero-based index of the layer into which rendering will be directed 可实现的效果好比: Cube-Map 添加cube_map texture为color attachment到framebuffer中 cube-map texture(2D texture)这里会被划分成六个layer的array texture 通过instanced geometry shader生成六个faces(对应六个layer),通过gl_InvocationID和gl_Layer访问六个faces并做相应的projection matrices运算实现Cube_Map Face的效果
Advanced Transform Feedback 这里首先要了解下什么是Transform Feeback? Transform feedback can be considered a stage of the OpenGL pipeline that sits after all of the vertex-processing stages and directly before primitive assembly and rasterization. Transform feedback captures vertices as they are assembled into primitives and allow some or all of their attributes to be recorded into buffer objects. (Transform feedback发生在所有顶点运算阶段之后(所以如果geometry shader打开了,transform feedback就发生在geometry shader之后,相反是在vertex shader之后),在primitive assembly和光栅化之前。Transform feedback可以保存顶点的一些属性信息用于下一次的运算。)
Why do we need transform feedback? “DirectX10 introduced a new feature known as Stream Output that is very useful for implementing particle systems. OpenGL followed in version 3.0 with the same feature and named it Transform Feedback. The idea behind this feature is that we can connect a special type of buffer (called Transform Feedback Buffer right after the GS (or the VS if the GS is absent) and send our transformed primitives to it. In addition, we can decide whether the primitives will also continue on their regular route to the rasterizer. The same buffer can be connected as a vertex buffer in the next draw and provide the vertices that were output in the previous draw as input into the next draw. This loop enables the two steps above to take place entirely on the GPU with no application involvement (other than connecting the proper buffers for each draw and setting up some state).”
了解一些相关概念: Transform Feedback Objects: “The state required to represent transform feedback is encapsulated into a transform feedback object.”(transform feedback objects主要是存储跟transform feedback相关的一些状态。比如:哪一个buffer绑定到了transform feedback buffer的binding point)
因为粒子效果用到了Billboard来展示,所以在了解Particle System之前,我们先来看看Billboard是如何通过GS实现的: Billboard - “A billboard is a quad which always faces the camera. “
Before a geometry shader may be linked, the input primitive type, output primitive type, and the maximum number of vertices that is might produce must be specified (在链接geometry shader之前,我们必须先定义geometry shader的输入输出类型)
EmitVertex() - produces a new vertex at the output of the geometry shader. Each time it is called, a vertex is appended to the end of the current strip (将新的vertex加入到primitive的队列)
EndPrimitive() - breaks the current strip and signals OpenGL that a new strip should be started the next time EmitVertex() is called (将之前所加入的vertex算作一个primitive的信息,通知OpenGL开始下一个primitive的构造)
Note:
When the geometry shader exits, the current primitive is ended implicitly (如果geometry shader结束了,那么当前还没有调用EndPrimitive()的primitive将视作结束)
When EndPrimitive() is called, any incomplete primitives will simply be discarded (当EndPrimitive()被调用的时候,数据不完全的primitive将被抛弃 -- 不调用这个方法的primitive相当于culling掉)
boolParticleSystem::InitParticleSystem(const Vector3f& pos) { Particle Particles[MAX_PARTICLES]; ZERO_MEM(Particles); //Particle System最初的那个发射点信息 Particles[0].Type = PARTICLE_TYPE_LAUCHER; Particles[0].Pos = pos; Particles[0].Vel = Vector3f(0.0f, 0.0001f, 0.0f); Particles[0].LifetimeMillis = 0.0f; //生成2个transform feedback object和两个buffer //"OpenGL enforces a general limitation that the same resource cannot be bound for both input and output in the same draw call. //This means that if we want to update the particles in a vertex buffer we actually need two transform feedback buffers and toggle between them. //On frame 0 we will update the particles in buffer A and render the particles from buffer B and on frame 1 we will update the particles in buffer B and render the particles from buffer " //从上面可以看出我们之所以生成两个Transform Feedback Object和两个buffer是因为OpenGL要求我们不能在一次draw call里把同一个resource(这里指TFB和buffer)即作为输入也作为输出 //所以我们想要通过fist pass去记录一些数据信息,然后再将其渲染到屏幕上,我们必须通过切换两个TFO和buffer来实现 //记录到A的时候用B数据渲染,记录到B的时候通过A数据来渲染 glGenTransformFeedbacks(2, m_TransformFeedback);
m_IsFirst = false; } else { //第二次以后,顶点数量是未知的,因为GS是可以生成多个顶点数据的。 //"The system automatically tracks the number of vertices for us for each buffer and later uses that number internally when the buffer is used for input. " //从上面可知,transfor feedback buffer里的顶点数量系统会自己去track //我们只需通知用哪一个TFB绑定的buffer作为数据输入即可 glDrawTransformFeedback(GL_POINTS, m_TransformFeedback[m_CurrVB]); }
接下来让我们来看看Transform Feedback的更多高级使用: Multiple Output Steams Multiple streams of vertices can be declared as outputs in the geometry shader (通过stream我们可以把一些额外需要保存的信息保存到特定的stream里便于transform feedback buffer去访问并进行进一步的处理)
Using the stream layout qualifier – this layout qualifier may be applied globally, to an interface block, or to a single output declaration
Each stream is numbered, starting from zero, max number of streams – GL_MAX_VERTEX_STREAMS
When the stream number is given at global scope, all subsequently declared geometry shader outputs become members of that stream until another output stream layout qualifier is specified See how to declaration stream:
Multiple output stream’s built in GLSL functions: EmitStreamVertex(int stream) EndStreamVertex(int stream)
glTransformFeedbackVaryings() – tell OpenGL how those streams are mapped into transform feedback buffer (告诉OpenGL各个stream是怎么映射到transform feedback buffer的)
When multiple streams are active, it is required that variables associated with a single stream are not written into the same buffer binding point as those associated with any other stream(当多个stream声明激活的时候,我们必须将每一个stream写到不同的buffer binding point里)
gl_NextBuffer is used to signal that the following output variables are to be recorded into the buffer object bound to the next transform feedback binding point (gl_NexBuffer告诉OpenGL后面的数据将绑定到下一个transform feedback buffer)
if rasterization & fragment shader are enabled, the output variables belonging to stream 0 will be used to form primitives for rasterization and will be passed into the fragment shader. Output variables belonging to other streams will not be visible in the fragment shader and if transform feedback is not active, they will be discarded (这里需要注意,一旦rasterization 和 fragment shader被开启或者transform feedback没有被开启,那么geometry shader里面指定的out变量只有属于stream 0的才会被进行处理,其他都会被抛弃)
Note: When multiple output streams are used in a geometry shader, they must all have points as the primitive type (注意,当multiple output streams被开启时,geometry shader必须指定输出类型为point,当first pass的时候geometry shader指定输出类型为point,second pass的时候geometry shader可以针对第一次transform feedback记录的point数据进行处理输出triangle等)
Primitive Queries Reason: Geometry shader can emit a variable number of vertices per invocation (因为geometry shader会扩展出很多primitive和vertices,我们在访问一些跟transform feedback buffer相关的数据的时候就不那么直接 – 这里要提一下没有geometry shader,vertex shader结合transform feeback buffer的使用是一对一的输出,而geometry shader不一样,会有一堆多的primitive,vertices的输出)
Problem: The number of vertices recorded into transform feedback buffers when a geometry shader is present may not be easy to infer
Solution: Two types of queries are available to count both the number of primitives the geometry shader generates, and the number of primitives actually written into the transform feedback buffers(通过Primitive Queries我们可以得知geometry shader的primitives,vertices生成数量和实际被写入transform feedback buffer的primitive,vertices数量)
GL_PRIMITIVES_GENERATED – query counts the number of vertices output by the geometry shader – valid at any time & GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN – query counts the number of vertices actually written into a transform feedback buffer – only valid when transform feedback is active
Due to geometry shader supports multiple transform feedback streams, primitive queries are indexed (因为geometry shader支持multiple transform feedback streams,所以primitive queries也是indexed的)
3D Picking
“The ability to match a mouse click on a window showing a 3D scene to the primitive (let’s assume a triangle) who was fortunate enough to be projected to the exact same pixel where the mouse hit is called 3D Picking.”
3D Picking实现的关键在于通过类似Shadow map的方式,把所有的primitive信息写入到一张picking texture里,当mouse点击的时候我们去查询所点击的primitive信息然后把该primitive渲染成我们想要的颜色即可。
实现步骤: First pass(picking pass) – 利用gDrawIndex, gObjectIndex, Primitive Index生成picking texture
通过类似生成Shaow map的方式,我们生成一个FRAMEBUFFER m_fbo,然后通过分别attach m_depthTexture和m_pickingTexture到GL_COLOR_ATTACHMENT0和GL_DEPTH_COMPONENT上,紧接着我们通过指定绘制到m_fbo的GL_COLOR_ATTACHMENT0(即我们attach的那个)上,这样一来当我们渲染到m_fbo的时候,attach到GL_COLOR_ATTACHMENT0的那个color texture就会得到渲染的picking texture,最后在我们我们在渲染之前需要指定m_fbo作为渲染到的FRAMEBUFFER。这样一来我们通过Picking Technique就能得到Picking texture。 这里也会生成depth texture,但我们并不会用到,指定生成depth texture的原因如下: “By combining a depth buffer in the process we guarantee that when several primitives are overlapping the same pixel we get the index of the top-most primitive (closest to the camera). “(注意我们需要结合depth buffer来保证我们生成的picking texture保存的primitive信息是离摄像机最近的)
for (uint i = 0 ; i < (int)ARRAY_SIZE_IN_ELEMENTS(gWorldPos) ; i++) { p.WorldPos(gWorldPos[i]); gPickingEffect.SetObjectIndex(i); gPickingEffect.SetWVP(p.GetWVPTrans()); gPSpider->Render(&gPickingEffect); }
gPickingTexture.DisableWriting(); }
最后gl_PrimitiveID是OpenGL build-in的变量,”This is a running index of the primitives which is automatically maintained by the system.”(代表我们绘制的primitive索引值,每一次draw都会从0开始。) 这里就引出了一个问题。我们如何得知我们渲染到picking texture里的primitive值0是指background还是被object遮挡的primitive了。 这也就是为什么我们在写入picking texture的时候,gl_PrimitiveID + 1的原因了。这样一来凡是primitive为0的都是background。
Render pass – 通过映射mouse click的pixel到picking texture,会得到鼠标点击到的gObjectIndex,gDrawIndex,gl_PrimitiveID信息,然后通过这些信息,我们把该点击的primitive通过simple color shader渲染成红色,然后再正常渲染两个spider即可。
// If the left mouse button is clicked check if it hit a triangle // and color it red if (m_leftMouseButton.IsPressed) { PickingTexture::PixelInfo Pixel = m_pickingTexture.ReadPixel(m_leftMouseButton.x, WINDOW_HEIGHT - m_leftMouseButton.y - 1); if (Pixel.PrimID != 0) { m_simpleColorEffect.Enable(); p.WorldPos(m_worldPos[(uint)Pixel.ObjectID]); m_simpleColorEffect.SetWVP(p.GetWVPTrans()); // Must compensate for the decrement in the FS! m_pMesh->Render((uint)Pixel.DrawID, (uint)Pixel.PrimID - 1); } }
// render the objects as usual m_lightingEffect.Enable(); m_lightingEffect.SetEyeWorldPos(m_pGameCamera->GetPos());
for (unsignedint i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_worldPos) ; i++) { p.WorldPos(m_worldPos[i]); m_lightingEffect.SetWVP(p.GetWVPTrans()); m_lightingEffect.SetWorldMatrix(p.GetWorldTrans()); m_pMesh->Render(NULL); } }
这里去读取picking texture里的信息的时候,要注意的一点是,鼠标获取得到的坐标信息和我们去查询texture的坐标系是不一致的,这里需要转换。 一下来源于Glut Mouse Coordinates “In “window” coordinate, the origin (0,0) is top left of the viewport.In OpenGL the origin is bottom left of the viewport. When you click glut give you the window coordinate. All you have to do is calculate this: y = height_of_viewport - y - 1.
Edit: Notice that you compare a screen coordinate (mouse click) with an object coordinate (your rectangle). This is fine if you use a perspective projection like this glOrtho(0,0,viewport_width,viewport_height). If not you need to call gluProject to map each corner of your rectangle in screen coordinate. “ 从上面可以看出,glut获取的mouse坐标系是以左上角为(0,0)点。而OpenGL viewport的(0,0)点时左下角,所以我们需要通过下列方式去转换映射点:
在得到正确的映射值后,我们将查询到的gObjectIndex,gDrawIndex,gl_PrimitiveID当做信息传入void Mesh::Render(unsigned int DrawIndex, unsigned int PrimID)去指定渲染特定mesh的特定primitive成红色。这里要注意的一点是因为mesh里的primitive索引是从0开始的,但我们之前存储的primitive index是+1的,所以这里我们需要恢复原有正确的值去指定渲染正确的primitive。
1 2
// Must compensate for the decrement in the FS! m_pMesh->Render((uint)Pixel.DrawID, (uint)Pixel.PrimID - 1);
The tessellation process doesn’t operate on OpenGL’s classic geometric primitives: points, lines, and triangles, but uses a new primitive called a patch (Tessellation shader就是针对patch来进行处理的而并非点,线,三角形)
Patch is just an ordered list of vertices (在tessellation shader里面比较重要的概念就是这个patch,patch是一系列的顶点,OpenGL规定patch的vertex数量必须至少大于等于3)。这里的Patch我们可以理解为一个包含了几何图形的所有Control Points(CP)的集合。Control Points会决定这个几何图形最终的形态。
Tessellation Control Shader(TCS) “The control shader calculates a set of numbers called Tessellation Levels (TL). The TLs determine the Tessellation level of detail - how many triangles to generate for the patch.” 可以看出TCS并不是负责顶点的细分而是负责指定细分的规则(如何细分,细分程度)。
Note: “It is executed once per CP in the output patch”
Primitive Generator (Fixed function) “OpenGL passes the output of the tessellation control shader to the primitive generator, which generates the mesh of geometric primitives and tessellation coordinates that the tessellation evaluation shader stage uses.”(PG之后会输出domain细分后的顶点和顶点纹理坐标信息,通过顶点纹理信息TES会算出对应的顶点位置信息)
Tessellation Evaluation Shader(TES) The TES is executed on all generated domain locations.Positions each of the vertices in the final mesh (TES是针对从tessellation control shader和Primitive Generator通过细分后所有patch相关的顶点来进行运算,通过各顶点的gl_TessCoord(顶点在patch里的相对坐标信息)按不同Domain的纹理坐标计算方式计算出相应的纹理坐标,位置信息和法线信息,从而实现细分多边形和修改顶点信息效果)
// attributes of the input CPs in vec3 WorldPos_CS_in[]; in vec2 TexCoord_CS_in[]; in vec3 Normal_CS_in[]; // attributes of the output CPs out vec3 WorldPos_ES_in[]; out vec2 TexCoord_ES_in[]; out vec3 Normal_ES_in[]; // 根据patch各顶点到camera的距离决定patch的细分程度 floatGetTessLevel(float Distance0, float Distance1) { float AvgDistance = (Distance0 + Distance1) / 2.0; if (AvgDistance <= 2.0) { return10.0; } elseif (AvgDistance <= 5.0) { return7.0; } else { return3.0; } } voidmain() { // Set the control points of the output patch // 记录下patch的control point的原始顶点信息,在TES中会参与就算,算出细分的顶点的位置信息 // **gl_InvocationID** is used to access the specific vertex of a patch (gl_InvocationID 用于访问传入patch里的特定顶点) // 之前我们指定patch的顶点数量是3,TCS是针对patch的顶点来执行的,所以每一个patch会执行3次TCS TexCoord_ES_in[gl_InvocationID] = TexCoord_CS_in[gl_InvocationID]; Normal_ES_in[gl_InvocationID] = Normal_CS_in[gl_InvocationID]; WorldPos_ES_in[gl_InvocationID] = WorldPos_CS_in[gl_InvocationID]; // Calculate the distance from the camera to the three control points // 算出patch各顶点到camera的距离 float EyeToVertexDistance0 = distance(gEyeWorldPos, WorldPos_ES_in[0]); float EyeToVertexDistance1 = distance(gEyeWorldPos, WorldPos_ES_in[1]); float EyeToVertexDistance2 = distance(gEyeWorldPos, WorldPos_ES_in[2]); // Calculate the tessellation levels // 根据patch各顶点到camera的距离设置细分方式和细分程度 // **gl_TessLevelInner** // Specify how the interior of the domain is subdivided and stored in a two element array named gl_TessLevelInner(指定多边形内部如何细分) // **gl_TessLevelOuter** // Control how the perimeter of the domain is subdivided, and is stored in an implicitly declared four-element array named gl_TessLevelOuter(指定多边形边界上的边被如何细分) // gl_TessLevelInner & gl_TessLevelOuter 根据Domain的类型不同会有不同的含义,参见前面提到的Domain // 我们也可以在程序里通过调用glPatchParameterfv()指定inner和outer的数值 gl_TessLevelOuter[0] = GetTessLevel(EyeToVertexDistance1, EyeToVertexDistance2); gl_TessLevelOuter[1] = GetTessLevel(EyeToVertexDistance2, EyeToVertexDistance0); gl_TessLevelOuter[2] = GetTessLevel(EyeToVertexDistance0, EyeToVertexDistance1); gl_TessLevelInner[0] = gl_TessLevelOuter[2]; }
“The Vertex Array Object (a.k.a VAO) is a special type of object that encapsulates all the data that is associated with the vertex processor. Instead of containing the actual data, it holds references to the vertex buffers, the index buffer and the layout specification of the vertex itself.”
“VAOs store all of the links between the attributes and your VBOs with raw vertex data.”
unsignedint NumVertices = 0; unsignedint NumIndices = 0; // Count the number of vertices and indices for (unsignedint i = 0 ; i < m_Entries.size() ; i++) { m_Entries[i].MaterialIndex = pScene->mMeshes[i]->mMaterialIndex; m_Entries[i].NumIndices = pScene->mMeshes[i]->mNumFaces * 3; m_Entries[i].BaseVertex = NumVertices; m_Entries[i].BaseIndex = NumIndices; NumVertices += pScene->mMeshes[i]->mNumVertices; NumIndices += m_Entries[i].NumIndices; } // Reserve space in the vectors for the vertex attributes and indices Positions.reserve(NumVertices); Normals.reserve(NumVertices); TexCoords.reserve(NumVertices); Indices.reserve(NumIndices);
// Initialize the meshes in the scene one by one for (unsignedint i = 0 ; i < m_Entries.size() ; i++) { const aiMesh* paiMesh = pScene->mMeshes[i]; InitMesh(paiMesh, Positions, Normals, TexCoords, Indices); }
if (!InitMaterials(pScene, Filename)) { returnfalse; }
// Generate and populate the buffers with vertex attributes and the indices // 下面就是存储成SOA的格式 glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[POS_VB]); glBufferData(GL_ARRAY_BUFFER, sizeof(Positions[0]) * Positions.size(), &Positions[0], GL_STATIC_DRAW); glEnableVertexAttribArray(POSITION_LOCATION); glVertexAttribPointer(POSITION_LOCATION, 3, GL_FLOAT, GL_FALSE, 0, 0);
// Populate the index buffer for (unsignedint i = 0 ; i < paiMesh->mNumFaces ; i++) { const aiFace& Face = paiMesh->mFaces[i]; assert(Face.mNumIndices == 3); Indices.push_back(Face.mIndices[0]); Indices.push_back(Face.mIndices[1]); Indices.push_back(Face.mIndices[2]); } }
“Instanced rendering means that we can render multiple instances in a single draw call and provide each instance with some unique attributes.”(在一次draw call里绘制多个同一个instance)
Using the Instance Counter in Shaders: The index of the current instance is available to the vertex shader in the built-in variable gl_InstanceID. This variable is implicitly declared as an integer. It starts counting from zero and counts up one each time an instance is rendered.
Instancing Redux: Steps:
Create some vertex shader inputs that you intend to be instanced
Set the vertex attribute divisors with glVertexAttribDivisor()
Use the gl_InstanceID built-in variable in the vertex shader
Use the instanced versions of the rendering functions such as glDrawArraysInstanced() ……
for (unsignedint i = 0; i < 4 ; i++) { glEnableVertexAttribArray(WVP_LOCATION + i); // Note: "A vertex attribute can contain no more than 4 floating points or integers." // 因为vertex attribute不能超过4个float或integers,所以我们需要针对mat4每一行进行指定访问方式 glVertexAttribPointer(WVP_LOCATION + i, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(GLfloat) * i * 4)); // 这里是"makes this an instance data rather than vertex data." // 第一个参数指明特定attribute是instance data而非vertex data, // 第二个参数指明instance data的使用频率,比如1表示每一个instance渲染后就访问下一个atrribute值,2表示每两个 glVertexAttribDivisor(WVP_LOCATION + i, 1); }
lighting.vs #version 330 layout (location = 0) in vec3 Position; layout (location = 1) in vec2 TexCoord; layout (location = 2) in vec3 Normal; // 这里注意因为vertex attribute不能超过4个float或integers,我们前面指定了每一个mat4四次vertex attribute // 所以这里WVP location = 3 而 World location = 7 layout (location = 3) in mat4 WVP; layout (location = 7) in mat4 World; out vec2 TexCoord0; out vec3 Normal0; out vec3 WorldPos0; // "Since integers cannot be interpolated by the rasterizer we have to mark the output variable as 'flat' (forgetting to do that will trigger a compiler error)." // 因为integers不能被rasterizer interpolated,所以我们需要使用'flat'关键词避免编译错误 flat out int InstanceID; voidmain() { gl_Position = WVP * vec4(Position, 1.0); TexCoord0 = TexCoord; Normal0 = (World * vec4(Normal, 0.0)).xyz; WorldPos0 = (World * vec4(Position, 1.0)).xyz; InstanceID = gl_InstanceID; }
Final Effect:
Note: gl_InstanceID is always present in the vertex shader, even when the current drawing command is not one of the instanced ones.
GLFX - An OpenGL Effect Library
首先让我们了解一下,什么是Effect file? “An effect is a text file that can potentially contain multiple shaders and functions and makes it easy to combine them together into programs. This overcomes the limitation of the glShaderSource() function that requires you to specify the text of a single shader stage.” 可以看出,通过effect file,我们可以把所有shader写到一个文件里,不用再创建针对各个stage的shader的文件。这样一来我们在shader里定义的结构体就能在多个shader共用。
那什么是GLFX了? “Effects system for OpenGL and OpenGL ES” GLFX提供了方便的接口去转换effect file到GLSL program.
因为所有shader内容都写在一个文件里了,所以支持共用struct定义,不用再定义一个个in or out variables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
structVSoutput { vec2 TexCoord; vec3 Normal; };
shader VSmain(in vec3 Pos, in vec2 TexCoord, in vec3 Normal, out VSOutput VSout) { // do some transformations and update 'VSout' VSout.TexCoord = TexCoord; VSout.Normal = Normal; }
shader FSmain(in VSOutput FSin, out vec4 FragColor) { // 'FSin' matches 'VSout' from the VS. Use it // to do lighting calculations and write the final output to 'FragColor' }
be passed between shader stages. If you need to pass it as a whole to another function you will need to copy the contents to a struct. For example: interface foo { flat int a; noperspective float b; };
// specify the color buffer to be drawn into, we will output buffer data for each color buffer in FS with out key word variable // 这里应对的是FS里指定的out输出 glDrawBuffers(ARRAY_SIZE_IN_ELEMENTS(drawbuffers), drawbuffers);
......
returntrue; }
geometry_pass.vs // VS没什么变化,只是把我们需要保存的Position,TexCoord,Normal分别转换透视投影坐标系和世界坐标系里 #version 330 layout (location = 0) in vec3 Position; layout (location = 1) in vec2 TexCoord; layout (location = 2) in vec3 Normal;
uniform mat4 gWVP; uniform mat4 gWorld; out vec2 TexCoord0; out vec3 Normal0; out vec3 WorldPos0;
geometry_pass.fs // FS负责把转换后的Position,Diffuse,Normal,TexCoordOut输出到我们之前绑定的GL_COLOR_ATTACHMENT*上 #version 330 in vec2 TexCoord0; in vec3 Normal0; in vec3 WorldPos0;
staticvoidDSLightPass() { // Bound frame buffer target 0 to draw state, we will copy four color buffer to this buffer later // 因为glBlitFramebuffer()函数是把GL_READ_FRAMEBUFFER的target copy到GL_DRAW_FRAMEBUFFER的target上, // 所以我们要声明Frame buffer 0作为我们最终的目的地 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
// Bound frame buffer m_FBO to reading state, // we will copy four color buffer that attach to m_FBO to frame buffer target 0 later // BindForReading()就是设置m_FBO作为glBlitFramebuffer()里的copy来源, // 所以需要设置成GL_READ_FRAMEBUFFER gGbuffer.BindForReading();
// Color buffer for position // Set color buffer source for copying // 因为一次只能从一个texture里copy,所以我们需要指定是copy哪一个 gGbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_POSITION); // Set buffer copy info glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, 0, halfwidth, halfheight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
// Color buffer for diffuses gGbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_DIFFUSE); glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0,halfheight, halfwidth, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
// Color buffer for normal gGbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_NORMAL); glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, halfwidth,halfheight, WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
// Color buffer for TexCoor gGbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_TEXCOORD); glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, halfwidth,0, WINDOW_WIDTH, halfheight, GL_COLOR_BUFFER_BIT, GL_LINEAR); }
Point Light因为是范围光,所以我们需要知道Point Light所影响的范围去触发对应Pixel的光照计算。这里涉及到一个Point Light的Point Light光照削弱方程。这里我也没详细看了,想了解的可以去看一下。通过方程我们得出了Point Light的有效范围,这样一来我们只需要以Point Light所在位置为圆心绘制一个Sphere就能触发正确的Point Light光照计算了。 Point Light:
Render the objects as usual into the G buffer so that the depth buffer will be properly populated.
Disable writing into the depth buffer. From now on we want it to be read-only
Disable back face culling. We want the rasterizer to process all polygons of the sphere.
Set the stencil test to always succeed. What we really care about is the stencil operation.
Configure the stencil operation for the back facing polygons to increment the value in the stencil buffer when the depth test fails but to keep it unchanged when either depth test or stencil test succeed.
Configure the stencil operation for the front facing polygons to decrement the value in the stencil buffer when the depth test fails but to keep it unchanged when either depth test or stencil test succeed.
Render the light sphere.(only when the stencil value of the pixel is different from zero)
// We need the stencil test to be enabled but we want it // to succeed always. Only the depth test matters. // 指定stencil text always success, // 因为这里我只需要通过设定stencil buffer修改规则就能得到我们要的值了 glStencilFunc(GL_ALWAYS, 0, 0);
// We need stencil to be enabled in the stencil pass to get the stencil buffer // updated and we also need it in the light pass because we render the light // only if the stencil passes. glEnable(GL_STENCIL_TEST);
for (unsignedint i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_pointLight); i++) { DSStencilPass(i); DSPointLightPass(i); }
// The directional light does not need a stencil test because its volume // is unlimited and the final pass simply copies the texture. glDisable(GL_STENCIL_TEST);
“assimp is a library to load and process geometric scenes from various data formats. It is tailored at typical game scenarios by supporting a node hierarchy, static or skinned meshes, materials, bone animations and potential texture data. The library is not designed for speed, it is primarily useful for importing assets from various sources once and storing it in a engine-specific format for easy and fast every-day-loading. “
“open3mod is a Windows-based model viewer. It loads all file formats that Assimp supports and is perfectly suited to quickly inspect 3d assets.”
主要用于快速查看各种资源格式的模型。
GIMP
GNU Image Manipulation Program (GIMP) is a cross-platform image editor available for GNU/Linux, OS X, Windows and more operating systems. GIMPDownloadLink