[OpenGL ES 03]3D變換:模型,視圖,投影與Viewport

[OpenGL ES 03]3D變換:模型,視圖,投影與Viewport

羅朝輝 (http://blog.csdn.net/kesalin)

本文遵循“署名-非商業用途-保持一致”創作公用協議

系列文章
[OpenGL ES 01]OpenGL ES之初體驗

[OpenGL ES 02]OpenGL ES渲染管線與著色器


前言

本來打算直接寫教程 04 的,但是想到3D 變換涉及的數學知識較多,往往是很多初學者的攔路虎(比如我自己)。再加上OpenGL ES 2.0 不再提供OpenGL ES 1.0中 3D 變換相關的一些重量級函數,如 glMatrixMode(GL_PROJECTION); glMatrixMode(GL_MODELVIEW); glLoadMatrixf; glMultMatrix 等,這些函數在 OpenGL ES 2.0 中均需要我們自己去實現。 如果不對線性代數與幾何知識作一些簡單介紹,恐怕不少人難以理解文中的一些步驟為什么要那么做。因此今天這一篇文章將放棄原定計劃,先來介紹一些 3D 數學以及 3D 變換相關的知識。BTW,原定計劃的代碼示例已經寫好了,有興趣的同學可以先行瀏覽,代碼放在這里,運行效果如下:


?

一,3D數學歷史

我們都學過幾何學,應該都知道歐幾里得(公元前3世紀希臘數學家)這位幾何學鼻祖,正是這位大牛創建了歐幾里得幾何學,他提出了基于 X,Y,Z 三軸的三維空間概念。到了17世紀,又出了位大牛笛卡爾,我們通常所說的笛卡爾坐標就是他的創造,笛卡爾坐標非常完美地將歐幾里得幾何學理論與代數學聯系到一塊。正是因為有了笛卡爾坐標,我們才能夠用簡單的矩陣(Matrix)來表示三維變換。但用矩陣來表示三維變換操作有一個無法解決的問題-萬向節鎖?。什么是萬向節鎖呢?簡單地說就是兩個軸旋轉到同一個方向上去了,這兩個軸平行了,因此就比原來少了一維(詳情可參考這里)。過了一百多年,漢密爾頓(Sir William Rowan Hamilton)創建了四元數(quaternion)解決了因為旋轉而導致萬向節鎖的問題,四元數還有其他用處,但在3D數學里主要是用來處理旋轉問題。

好吧,或許你看得一頭霧水,不要緊,你只要知道:用矩陣來表示3D變換,但矩陣在表示旋轉時可能會導致萬向節鎖的問題,而使用四元數可以避免萬向節鎖就可以了。

?

二,矩陣變換

在前面提到可使用 Matrix 來表示三維變換操作,那么變換又是如何通過 Matrix 實現的呢?下面就來講這個。在這里我推薦一本3D數學入門書籍:《3D數學基礎:圖形與游戲開發》

通常我們使用 4 維向量 (x, y, z, w) 表示在3D空間中的一個點,最后一維 w 表示齊次坐標。齊次坐標的含義是兩條平行線在投影平面的無窮遠處相交于一點,但在 Matrix 中沒有表示無窮大,所以增加了齊次坐標這一維。你可以想象下,火車軌道的兩條邊在無限遠處看起來就相交于一點,齊次坐標詳細的介紹可以參考這篇文章。


矩陣運算規則

1) 若矩陣 A 和 B 不是互逆矩陣,則不滿足乘法交換律,即 A × B 不等于 B × A;?
2) M × N 階的矩陣只能和 N × O 階的矩陣相乘,即 N 的階數相等,結果為 M × O 階的矩陣;?
3) 矩陣 A × B 的運算過程是 A 的每一行依次乘以 B 的每一列作為結果矩陣中的一行;?
4) 矩陣 A 的逆矩陣?B 滿足 A × B = B × A = 單位矩陣。??
5) 單位矩陣是對角線上的值為1,其余均為 0 的矩陣。單位矩陣不影響坐標變換(你可以將下面的3D變換矩陣換成單位矩陣來思考下)。

3D空間的物體投影到2D平面上時,就需要使用到齊次坐標,因此我們需要使用 4 × 4 的 Matrix 來表示變換。在編程語言中,這樣的 Matrix 可用大小為 16 的一維數組或4?× 4 的二維數組來表示。由于矩陣乘法不滿足乘法交換律,用數組表示 Matrix 又分為兩種形式:行主序和列主序,它們在本質上是等價的,只不過是一個是右乘(行主序,矩陣放右邊)和一個是左乘(列主序,矩陣放左邊)。OpenGL 使用列主序矩陣,即列矩陣因此我們總是倒過來算的(左乘矩陣,變換效果是按從右向左的順序進行): 投影矩陣 × 視圖矩陣 × 模型矩陣 × 3D位置。

4× 4列矩陣的數組表示:數字表示數組下標對應的行列位置:


那么

平移矩陣可表示為:


平移矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a + x, b + y, c + z, 1)。

縮放矩陣可表示為:


縮放矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × sx, b × sy, c × sz, 1)。

繞 X 軸旋轉的旋轉矩陣可表示為:

?

繞 X 軸旋轉的旋轉矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a, b × cos(θ) - c × sin(θ), b × -sin(θ) + c × cos(θ), 1)。

繞 Y 軸旋轉的旋轉矩陣可表示為:


繞 Y 軸旋轉的旋轉矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × cos(θ) - c × sin(θ), b , a × -sin(θ) + c × cos(θ), 1)。

繞 Z 軸旋轉的旋轉矩陣可表示為:

?

繞 Z 軸旋轉的旋轉矩陣 × 列矩陣(a, b, c, 1) = 列矩陣(a × cos(θ) - b × sin(θ),? a × -sin(θ) + b × cos(θ), c, 1)。

?

三,OpenGL 中的實現

OpenGL 使用右手規則進行旋轉,因此逆時針方向的選擇是正角度的,而順時針方向的旋轉是負角度的。還記得中學學物理時候的右手規則么?忘記了的話,看下圖:

?

注意:

前面說到矩陣乘法不滿足乘法交換律,因此你對一個3D坐標先進行旋轉,然后進行平移(平移矩陣 × 旋轉矩陣 × 3D坐標);與先進行平移,然后進行旋轉(旋轉矩陣 × 平移矩陣 × 3D坐標)得到的效果是大為迥異的。如下圖所示:


在第一種情況下,我們通常稱旋轉是在 local space 中進行,因為它是繞著物體自己的中心點進行的,而在后一種情況下的旋轉通常稱為是在 world space 中進行的。我們知道點是可以在坐標空間之間相互轉換的,這是一個很重要的概念。OpenGL 中物體最初是在本地坐標空間中,然后轉換到世界坐標空間,再到 camera 視圖空間,再到投影空間,這一系列轉換都是靠 matrix 計算來實現。

上面的這個過程在 OpenGL 及 OpenGL ES 1.0 中,對應的代碼類似于:

   glViewport (0, 0, (GLsizei) w, (GLsizei) h);    a)glMatrixMode (GL_PROJECTION);            b)glLoadIdentity ();glFrustum (-1.0, 1.0, -1.0, 1.0, 1.5, 20.0);    c)glMatrixMode (GL_MODELVIEW);             d)glClear (GL_COLOR_BUFFER_BIT);glColor3f (1.0, 1.0, 1.0);glLoadIdentity ();             /* clear the matrix *//* viewing transformation  */gluLookAt (0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);    e)glScalef (1.0, 2.0, 1.0);      /* modeling transformation */  f)glutWireCube (1.0);                          g)glFlush ();

說明:

a) 是用于viewport(視口)變換,viewport 變換發生在投影到2D 投影平面之后,該變換是將投影之后歸一化的點映射到屏幕上一塊區域內的坐標。視口變換的目的是指定投影之后圖像在屏幕上顯示的區域。如下示意圖所示:


視口變換 glViewport(x, y, width, height); x,y 是投影平面描繪在屏幕或窗口上的起始位置(注意屏幕坐標以左上方為原點),width和height是以像素為單位,指投影平面在屏幕上描繪的區域大小。如果投影平面的寬高比,與width/height比不相同(如上面的右圖),那么描繪的場景就會扭曲。

從裁剪到屏幕的整個過程如下圖所示,w 就是前面提到的齊次坐標那一維,從 Clip Space 到 Normalized Device Space 就是投影規范化的過程,從 Normalized Device Space 到 Window Space 就是 viewport 變換過程。


該轉換內部計算公式為:


?(xw, yw)是屏幕坐標,(x, y, width, height)是傳入的參數,(xnd, ynd)是投影之后經歸一化之后的點(上圖中 Normalized Device Space 空間的點)。因此 viewport 變換就是將投影之后歸一化的點轉換為真正可用于在屏幕上進行渲染的屏幕坐標;

b) 是說明下面的 matrix 是用于投影變換的,在本例中,是通過語句 c)?glFrustum?來設置透視投影變換的。投影變換有兩種:正交投影和透視投影,后面會有詳細介紹;

d) 是說明下面的 matrix 是用于模型視圖變換,注意,OpenGL 和 OpenGL ES 都將模型變換與視圖變換結合在一起,而不是分開為兩個,這是因為模型變換等價于視圖變換的逆變換。視圖變換是將物體轉換到觀察者(一般稱之為 camera)的視線空間中。你可以想象一下,照相時,你可以:A)照相機不懂,旋轉自己的頭找個側面像,也可以B)自己不動,照相機旋轉一定的角度來達到同樣的效果。下面的兩幅圖分別描述了情形A)和情形B):

情形A):旋轉物體,相機不動


情形B):旋轉相機,物體不動


在 OpenGL 中,我們在設置場景(scene)的時候通常是采取情形B)的做法,因此在語句 e) 處,我們設置相機的位置和朝向,來設定視圖變換,之后的語句 f) glScale 是設定在模型變換的,最后語句 g) 在本地空間描繪物體。

注意

寫 OpenGL 代碼時從前到后的順序依次是:設定 viewport(視口變換),設定投影變換,設定視圖變換,設定模型變換,在本地坐標空間描繪物體。而在前面為了便于理解做介紹時,說的順序是OpenGL 中物體最初是在本地坐標空間中,然后轉換到世界坐標空間,再到 camera 視圖空間,再到投影空間。由于模型變換包括了本地空間變換到世界坐標空間,所以我們理解3D 變換是一個順序,而真正寫代碼時則是以相反的順序進行的,如果從左乘矩陣這點上去理解就很容易明白為什么會是反序的。

有了上面 3D 變換的整體概念,下面來詳細說說投影變換與視圖變換。

?

四,投影變換

投影變換的目的是確定 3D 空間的物體如何投影到 2D 平面上,從而形成2D圖像,這些 2D 圖像再經視口變換就被渲染到屏幕上。前面提到投影變換有兩種:正交投影和透視投影。透視投影用的比較廣泛,它與真實世界更相近:近處的物體看起來要比遠處的物體大;而正交投影沒有這個效果,正交投影通常用于CAD或建筑設計。下面是正交投影與透視投影效果示意圖:

正交投影 透視投影

?

? ? ? ?

?

?

?

?

透視投影可以通過兩種方式來表述,OpenGL 及 OpenGL ES 1.0 提供其中一種:?glFrustum,而 glut 輔助庫提供了另外一種:gluPerspective。它們本質上是相同的,只不過是不同的表述而已:

視錐體/視景體



glFrustum(left, right, bottom, top, zNear, zFar);

left,right, bootom,top 定義了 near 裁剪面大小,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。由這六個參數可以定義出六個裁剪面構成的錐體,這個錐體通常被稱之為視錐體或視景體。只有在這個錐體內的物體才是可以見的,不在這個錐體內的物體就相當于不再視線范圍內,因而會被裁減掉,OpenGL 不會這些物體進行渲染。

由于 OpenGL ES 2.0 不提供此函數,因此我們需要自己實現該函數。其計算公式如下:

假設:l = left, r = right, b = bottom, t = top, n = zNear, f = zFar,有


透視圖


gluPerspective(fovy, aspect, zNear, zFar);

fovy 定義了 camera 在 y 方向上的視線角度(介于 0 ~ 180 之間),aspect 定義了近裁剪面的寬高比 aspect = w/h,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。這四個參數同樣也定義了一個視錐體。

在 OpenGL ES 2.0 中,我們也需要自己實現該函數。我們可以通過三角公式 tan(fovy/2) ?= (h / 2)/zNear 計算出 h ,然后再根據 w = ?h * aspect 計算出 w,這樣就可以得到 left, right, top, bottom, zNear, zFar 六個參數,代入在介紹視錐體時提到的公式即可。?

正交投影在 OpenGL 及 OpenGL ES 1.0 中是由?glOrtho 來提供的,我們可以把正交投影看成是透視投影的特殊形式:即近裁剪面與遠裁剪面除了Z 位置外完全相同,因此物體始終保持一致的大小,即便是在遠處看上去也不會變小。


glOrtho(left,?right,?bottom,?top,?zNear,?zFar);

left,right, bootom,top 定義了 near 裁剪面大小,而 zNear 和 zFar 定義了從 Camera/Viewer 到遠近兩個裁剪面的距離(注意這兩個距離都是正值)。

假設:xmax = right, xmin = left, ymax = top, ymin = bottom, zmax = far, zmin = near,正交投影的計算可分為兩步:首先平移到視錐體的中心,然后縮放。

平移矩陣:(圖中的2min 應為 zmin)

?

縮放矩陣:


正交投影矩陣 R = S ×?T:


?

五,視圖變換

視圖變換的目的是為了讓我們能觀察到某個角度的場景(從觀察者的角度來說)或者說是為了將物體從世界坐標轉換到相機視線所在視圖空間中來(從3D物體角度來說)。這可以通過設定觀察者的位置和朝向來實現的或對物體進行3D變換來實現,通常前面一種方式來實現(即設定觀察者的位置與朝向)。如下圖所示,xyz坐標軸表示的是世界坐標,藍白色區域為視圖空間,視圖變換就是要將長方體從世界空間中轉換到視圖空間的坐標體系中去,然后再投影規范化,然后再經 viewport 轉換映射到屏幕上渲染出來。


在 OpenGL 中,我們可以通過工具庫提供的 gluLookAt 這個函數來實現此功能。該函數的原型為:

gluLookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz);

eye 表示 camera/viewer 的位置, center 表示相機或眼睛的焦點(它與 eye 共同來決定 eye 的朝向),而 up 表示 eye 的正上方向,注意 up 只表示方向,與大小無關。通過調用此函數,就能夠設定觀察的場景,在這個場景中的物體就會被 OpenGL 處理。在 OpenGL 中,eye 的默認位置是在原點,指向 Z 軸的負方向(屏幕往里),up 方向為 Y 軸的正方向。在接下來的教程 04 中,使用的就是這個默認設置。

OpenGL ES 2.0 也沒有提供該函數,glulookat 的內部實現其實就是先旋轉到與觀察者視線相同的方向,然后再平移到觀察者所在的位置。其實現偽碼如下:

[csharp] view plaincopy print?
  1. Matrix4?GetLookAtMatrix(Vector3?eye,?Vector3?at,?Vector3?up){??
  2. ????Vector3?forward,?side;??
  3. ????forward?=?at?-?eye;??
  4. ????normalize(forward);??
  5. ????side?=?cross(forward,?up);??
  6. ????normalize(side);??
  7. ????up?=?cross(side,?forward);??
  8. ??
  9. ????Matrix4?res?=?Matrix4(??
  10. ??????????????????????????side.x,?up.x,?-forward.x,?0,??
  11. ??????????????????????????side.y,?up.y,?-forward.y,?0,??
  12. ??????????????????????????side.z,?up.z,?-forward.z,?0,??
  13. ??????????????????????????0,?0,?0,?1);??
  14. ????translate(res,?Vector3(0?-?eye));??
  15. ????return?res;??
  16. }??

上面代碼中的 cross 是叉積,normalize 是規范化,Matrix4 是列主序,translate 是平移。

?

六,后記

3D 變換是對初學者來說是比較困難的,我盡量寫得明白點,但效果如何就不得而知了。寫這一篇花了我不少時間,但對四元數和萬向節鎖也只是提及而已,未詳細介紹,以后再單獨介紹吧。Nate Robin 寫了一個3D 變換的可視化教程工具,對于理解投影,視圖,模型變換非常有幫助,強烈建議下載運行該程序,并調整相關參數看看效果。下面傳張截圖以誘惑你去下載:點此進入下載頁面(Windows 和 Mac 版本都有)

轉載于:https://www.cnblogs.com/zhoug2020/p/6084717.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/253943.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/253943.shtml
英文地址,請注明出處:http://en.pswp.cn/news/253943.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

LOAM_velodyne學習(三)

終于到第三個模塊了,我們先來回顧下之前的工作:點云數據進來后,經過前兩個節點的處理可以完成一個完整但粗糙的里程計,可以概略地估計出Lidar的相對運動。如果不受任何測量噪聲的影響,這個運動估計的結果足夠精確&…

監控視頻線種類 視頻信號傳輸介紹及各種視頻接口的傳輸距離

一.視頻信號接口 監控視頻線種類介紹: 按照材料區分有SYV及SYWV兩種,絕緣層的物理材料結構不同,SYV是實心聚乙烯電纜,SYWV是高物理發泡電纜,物理發泡電纜傳輸性能優于聚乙烯。 S--同軸電纜 Y--聚乙烯 V--聚氯乙烯 W…

免費節假日API 更新新功能了 新增農歷信息返回

感謝大家對免費節假日API的支持.最近看了別家的api于是增加了一些新功能即獲取日期的農歷信息. 這個新功能還處于測試階段如有問題歡迎反饋 檢查一個日期是詳細信息 https://tool.bitefu.net/jiari/?d20180101&info1 返回值 {"status": 1,"type": 1,…

新手算法學習之路----二叉樹(二叉樹最大路徑和)

摘抄自:https://segmentfault.com/a/1190000003554858#articleHeader2 題目: Given a binary tree, find the maximum path sum. The path may start and end at any node in the tree. For example: Given the below binary tree, 1/ \2 3Return 6. 思…

Ajax工作原理

詳見:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt238 在這篇文章中,我將從10個方面來對AJAX技術進行系統的講解。 1、ajax技術的背景 不可否認,ajax技術的流行得益于google的大力推廣,正是由于google earth、go…

各種視頻信號格式及端子介紹/VGA DVI HDMI區別

視頻信號是我們接觸最多的顯示信號,但您并不一定對各種視頻信號有所了解。因為國內用到的視頻信號格式和端子非常有限,一般就是復合視頻和S端子,稍高級一些的就是色差及VGA。對于那些經常接觸國外電器和二手設備的朋友,就會遇到各…

LOAM_velodyne學習(四)

TransformMaintenance 來到了最后一個模塊,代碼不是很長,我們在看完代碼之后,再詳細說明這個模塊的功能 依然主函數開始 int main(int argc, char** argv) {ros::init(argc, argv, "transformMaintenance");ros::NodeHandle nh;…

PHP數據庫類

<?phpclass Db{//私有靜態屬性存儲實例化對象自身private static $instance;//存儲PDO類的實例化private $pdo;//PDOStatement類private $stmt;//禁止外部實例化對象&#xff0c;鏈接數據庫private function __construct($config,$port,$charset){try{$this->pdo new P…

oracle參數文件、控制文件、數據文件、日志文件的位置及查詢方法

參數文件&#xff1a;所有參數文件一般在 $ORACLE_HOME/dbs 下 sqlplus查詢語句&#xff1a;show parameter spfile; 網絡連接文件&#xff1a; $ORACLE_HOME/dbs/network/admin 目錄中 控制文件&#xff1a;select * from v$controlfile; 數據文件&#xff1a;一般在oracleda…

Bishops Alliance—— 最大上升子序列

原題鏈接&#xff1a;http://codeforces.com/gym/101147/problem/F 題意&#xff1a;n*n的棋盤&#xff0c;給m個主教的坐標及其私有距離p&#xff0c;以及常數C&#xff0c;求位于同一對角線上滿足條件&#xff1a;dist(i, j) > p[i]^2 p[j]^2 C 的主教集合的元素個數最…

LeGO-LOAM學習

前言 在學習了LOAM之后&#xff0c;了解到LeGO-LOAM&#xff08;面向復雜情況的輕量級優化地面的雷達里程計&#xff09;&#xff0c;進行了一個學習整理。 Github&#xff1a;https://github.com/RobustFieldAutonomyLab/LeGO-LOAM 論文&#xff1a;https://github.com/Robu…

char data[0]用法總結

struct MyData { int nLen; char data[0]; }; 開始沒有理解紅色部分的內容&#xff0c;上網搜索下&#xff0c;發現用處很大&#xff0c;記錄下來。 在結構中&#xff0c;data是一個數組名&#xff1b;但該數組沒有元素&#xff1b;該數組…

(一)低功耗設計目的與功耗的類型

一、低功耗設計的目的 1.便攜性設備等需求 電子產品在我們生活中扮演了極其重要的作用&#xff0c;便攜性的電子設備便是其中一種。便攜性設備需要電池供電、需要消耗電池的能量。在同等電能提供下&#xff0c;低功耗設計的產品就能夠工作更長的時間。時間的就是生命&#xff…

(轉)徹底學會使用epoll(一)——ET模式實現分析

注&#xff1a;之前寫過兩篇關于epoll實現的文章&#xff0c;但是感覺懂得了實現原理并不一定會使用&#xff0c;所以又決定寫這一系列文章&#xff0c;希望能夠對epoll有比較清楚的認識。是請大家轉載務必注明出處&#xff0c;算是對我勞動成果的一點點尊重吧。另外&#xff0…

MFC的消息映射有什么作用

絕對以下這三個解釋的比較簡潔&#xff0c;特此做個記錄&#xff01;以感謝回答的這些人&#xff01; MFC的消息映射有什么作用: Windows操作系統主要是有消息來處理的&#xff0c;每個程序都有自己的消息隊列&#xff0c;并且這些消息是有優先級的&#xff0c;也就是誰會先…

線性表的鏈式存儲結構

鏈式存儲結構的定義 1.概念定義&#xff1a; - n個結點離散分配 - 彼此通過指針相連 - 每個結點只有一個前驅結點和一個后繼結點 - 首結點沒有前驅結點&#xff0c;尾結點沒有后繼結點 2.專業術語 -首結點&#xff1a;第一個有有效數據的結點 -尾結點&#xff1a;最后一個有有效…

Apache 設置http跳轉至HTTPS訪問

為什么80%的碼農都做不了架構師&#xff1f;>>> <VirtualHost>...</VirtualHost> 中添加如下配置 <IfModule mod_rewrite.c>RewriteEngine onRewriteCond %{SERVER_PORT} 80RewriteRule ^(.*)$ https://域名/$1 [R301,L] </IfModule> 轉…

JAVA線程概念

一、程序與進程 1、程序&#xff1a;一段靜態的代碼。 2、進程&#xff1a;程序的一次動態執行過程&#xff0c;它對應從代碼加載、執行到執行完畢的一個完整過程。 3、進程也稱任務&#xff0c;支持多個進程同時執行的OS就被稱為多進程OS或多任務OS。 二、進程與線程 在一…

(二)功耗的分析

前面學習了進行低功耗的目的個功耗的構成&#xff0c;今天就來分享一下功耗的分析。由于是面向數字IC前端設計的學習&#xff0c;所以這里的功耗分析是基于DC中的power compiler工具&#xff1b;更精確的功耗分析可以采用PT&#xff0c;關于PT的功耗分析可以查閱其他資料&#…

Hibernate創建hqll時報錯

Hibernate 問題,在執行Query session.createQuery(hql) 報錯誤 出錯截圖&#xff1a; 這條語句在java運行環境下&#xff0c;直接連數據庫不出錯&#xff0c;如果在hiberante,struts環境下就出錯 出錯原因&#xff1a;jar包沖突&#xff0c;struts2和hibernate框架中都有antlr包…