基於HT的CSG功能構建HTML5的3D書架

圖撲軟體發表於2014-12-25

構造實體幾何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。

Screen Shot 2014-12-25 at 12.19.17 AM

回到我們今天要搞的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書籍,強烈推薦!

Screen Shot 2014-12-25 at 12.22.57 AM

書架兩邊分別擺放了兩個不同風格的小書檯,通過上圖我拖拽改變了藍色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);
				  
}   

 

相關文章