構造實體幾何CSG全稱Constructive solid geometry,是3D計算機圖形學中構建模型的常用技術,可通過合併Union、相減Subtraction和相交Intersction的三種取集的邏輯運算,將立方體、圓柱體和稜柱等簡單的基礎模型,巢狀組合成更復雜三維模型。
CSG的演算法這些年來已有各種語言平臺版本實現,C++版主流的是 http://opencsg.org/ 已有眾多基於該開源類庫的應用案例,JavaScript語言較早版實現 http://evanw.github.io/csg.js/ 影響較廣,很多其他js衍生版都是基於該版本進行改進完善,包括Java版的實現 https://github.com/miho/JCSG ,可參考基於JavaFX的3D列印IDE https://github.com/miho/JFXScad ,提起JavaFX視乎這些年完全消失在程式設計師視野毫無聲息,但還是有一群擁護者持續在使用著如今地位有點尷尬的JavaFX。
回到我們今天要搞的3D書架例子,我們將基於HT for Web的3D引擎來實現,HT已經內建了CSG功能的模型封裝,我們通過構建CSGNode圖元物件,該型別圖元可對Host吸附的圖元進行CSG技術的合集、並集和補集的三種操作,一般運用中裁剪的方式較為常用,因此CSGNode預設對Host圖元的操作就是裁剪。
上圖的例子效果可看出我們構建了一個DataModel資料模型,該模型繫結了一個TreeView樹元件和兩個Graph3dView的三維元件,上部分的Graph3dView元件新增了VisibleFunc的可見過濾器,隱藏瞭如下部分的Graph3dView中藍色立方體圖元,這些藍色立方體圖元就是CSGNode,其作用就是用來裁剪其吸附的書架Shelf物件,因此一般在3D編輯器狀態下才需要出現,執行時科如上部分Graph3dView元件那樣,通過新增可見過濾器將其隱藏,這樣就實現了有凹槽可擺放書籍內容的3D書架效果,本例我們作為示例僅放了一本《CSS3 The Missing Manual》,這本書其實是由一個六面體,front面顯示了書籍貼圖,然後旋轉一定角度進行擺放,btw《CSS3 The Missing Manual》第三版是本很不錯的CSS書籍,強烈推薦!
書架兩邊分別擺放了兩個不同風格的小書檯,通過上圖我拖拽改變了藍色CSGNode圖元的位置,大家通過兩張圖的對比能更直觀的體會到CSG的操作效果,玻璃門開關以及相簿效果都是直接利用HT for Web的3D引擎提供的模型,通過設定透明度、相片貼圖和旋轉動畫等呢只功能引數即可。
以下是該HT for Web的3D例子的所有JavaScript程式碼供參考:http://v.youku.com/v_show/id_XODU2MTQ4NjI4.html
ht.Default.setImage('ben12', { width: 100, height: 50, comps: [ { type: 'image', name: 'ben1', rect: [0, 0, 50, 50] }, { type: 'image', name: 'ben2', rect: [50, 0, 50, 50] } ] }); function init(){ dm = new ht.DataModel(); treeView = new ht.widget.TreeView(dm); gv1 = new ht.graph3d.Graph3dView(dm); gv2 = new ht.graph3d.Graph3dView(dm); splitView = new ht.widget.SplitView(gv1, gv2, 'v', 0.6); mainSplit = new ht.widget.SplitView(treeView, splitView, 'h', 0.27); view = mainSplit.getView(); view.className = 'main'; document.body.appendChild(view); window.addEventListener('resize', function (e) { mainSplit.invalidate(); }, false); gv1.setMoveStep(30); gv1.setGridVisible(true); gv1.setEye(0, 100, 1000); gv1.setCenter(0, 200, 0); gv1.pan(0, 100, true); gv1.getLabel = function(){ return null; }; gv1.getBrightness = function(data){ return null; }; gv1.setVisibleFunc(function(data){ if(data.showMe){ return true; } if(data instanceof ht.CSGNode && data.getHost()){ return false; } return true; }); gv2.setMoveStep(30); gv2.setEditable(true); gv2.setGridVisible(true); gv2.setEditable(true); gv2.pan(0, 200, true); gv2.getLabel = function(){ return null; }; initShelf1(); initShelf2(); initShelf3(); treeView.expandAll(); var angle = 0; setInterval(function(){ angle += Math.PI/40; earth.r3(0, angle, 0); photos.s('dw.angle', angle); }, 50); } function initShelf1(){ var shelf = new ht.CSGNode(); shelf.s3(500, 400, 120); shelf.p3(0, 200, 0); shelf.setName('shelf1'); shelf.s({ 'all.color': '#E5BB77' }); dm.add(shelf); for(var i=0; i<2; i++){ for(var j=0; j<5; j++){ var clipNode = new ht.CSGNode(); clipNode.setHost(shelf); clipNode.s3(80, 100, 120); clipNode.p3(-200+j*100, 340-i*120, 20); clipNode.setName('substract-'+i+'-'+j); clipNode.s('batch', 'tt'); clipNode.setParent(shelf); dm.add(clipNode); } } var leftNode = new ht.CSGNode(); leftNode.setHost(shelf); leftNode.s3(23, 380, 100); leftNode.p3(-255, 200, 0); leftNode.setName('substract left'); leftNode.setParent(shelf); dm.add(leftNode); var rightNode = new ht.CSGNode(); rightNode.setHost(shelf); rightNode.s3(23, 380, 100); rightNode.p3(255, 200, 0); rightNode.setName('substract right'); rightNode.setParent(shelf); dm.add(rightNode); var bottomNode = new ht.CSGNode(); bottomNode.setHost(shelf); bottomNode.s3(480, 140, 140); bottomNode.p3(0, 80, 0); bottomNode.setName('substract bottom'); bottomNode.setParent(shelf); dm.add(bottomNode); var topNode = new ht.CSGNode(); topNode.setHost(shelf); topNode.s3(480, 10, 100); topNode.p3(0, 400, 0); topNode.setName('union top'); topNode.s('attach.operation', 'union'); topNode.setParent(shelf); dm.add(topNode); var book = new ht.Node(); book.setName('CSS3: The Missing Manual'); book.s3(60, 80, 8); book.p3(-100, 210, 20); book.r3(-Math.PI/6, Math.PI/5, 0); book.setIcon('book'); book.s({ 'front.image': 'book', 'back.color': 'white', 'left.color': 'white', 'all.color': 'gray' }); book.setHost(shelf); book.setParent(shelf); dm.add(book); } function initShelf2(){ var shelf = new ht.CSGNode(); shelf.s3(120, 240, 120); shelf.p3(0, 120, 0); shelf.setName('shelf2'); shelf.s({ 'all.color': '#805642', 'csg.color': 'yellow', 'csg.reverse.flip': true }); dm.add(shelf); var clipNode = new ht.CSGNode(); clipNode.setName('shelf2-substract-up'); clipNode.s3(100, 100, 130); clipNode.p3(0, 180, 0); clipNode.setHost(shelf); clipNode.s('attach.cull', true); clipNode.setParent(shelf); dm.add(clipNode); clipNode = new ht.CSGBox(); clipNode.setName('CSGBox-Expand-Left'); clipNode.s3(100, 100, 120); clipNode.p3(0, 65, 0.1); clipNode.setHost(shelf); clipNode.showMe = true; clipNode.s({ 'all.visible': false, 'front.visible': true, 'front.toggleable': true, 'front.reverse.flip': true, 'front.transparent': true, 'front.end': Math.PI * 0.7, 'front.color': 'rgba(0, 50, 50, 0.7)' }); clipNode.setParent(shelf); clipNode.setFaceExpanded('front', true, true); dm.add(clipNode); earth = new ht.Node(); earth.setName('earth'); earth.setIcon('earth'); earth.s3(70, 70, 70); earth.p3(0, 50, 0); earth.s({ 'shape3d': 'sphere', 'shape3d.image': 'earth' }); earth.setHost(shelf); earth.setParent(shelf); dm.add(earth); shelf.t3(-360, 0, 50); shelf.r3(0, Math.PI/7, 0); } function initShelf3(){ var shelf = new ht.CSGNode(); shelf.s3(120, 240, 120); shelf.p3(0, 120, 0); shelf.setName('shelf3'); shelf.setIcon('ben'); shelf.s({ 'all.image': 'brick', 'all.uv.scale': [2, 4], 'top.uv.scale': [2, 2], 'bottom.uv.scale': [2, 2], 'csg.image': 'ben', 'csg.blend': 'yellow' }); dm.add(shelf); photos = new ht.DoorWindow(); photos.setName('DoorWindow-Photos'); photos.setIcon('ben12'); photos.s3(110, 100, 130); photos.p3(5, 180, 0); photos.setHost(shelf); photos.showMe = true; photos.s({ 'bottom.uv': [1,1, 1,0, 0,0, 0,1], 'bottom.uv.scale': [1, 1], 'left.uv.scale': [3, 3], 'top.uv.scale': [2, 2], 'dw.s3': [0.8, 0.9, 0.05], 'dw.t3': [0, -5, 0], 'dw.axis': 'v', 'dw.toggleable': false, 'front.image': 'ben1', 'back.image': 'ben2', 'all.color': '#F8CE8B' }); photos.setParent(shelf); dm.add(photos); var clipNode = new ht.CSGBox(); clipNode.setName('CSGBox-Expand-Top'); clipNode.s3(100, 100, 120); clipNode.p3(0, 65, 0.1); clipNode.setHost(shelf); clipNode.showMe = true; clipNode.s({ 'all.visible': false, 'front.visible': true, 'front.color': 'red', 'front.transparent': true, 'front.opacity': 0.7, 'front.reverse.flip': true, 'front.toggleable': true, 'front.axis': 'top', 'front.end': Math.PI * 0.4 }); clipNode.setParent(shelf); clipNode.setFaceExpanded('front', true, true); dm.add(clipNode); shelf.t3(360, 0, 50); shelf.r3(0, -Math.PI/7, 0); }