रेकास्टर PixiJS के साथ और टकराव

यहां बताया गया है कि मैंने PixiJS के साथ अपने Raycasting सिस्टम को कैसे लागू किया है:

द्वारा: jonathan.lepage

11/11/2018

द्वारा छवि: jonathan.lepage

ECSGAMEDEVMATHPIXIJSTS

PixiJS में Raycaster का कार्यान्वयन Vertex टक्कर का पता लगाने के साथ'

/blogs/article-2/b2-0.gif

परिचय

एक बड़ी चुनौती जिसे मैंने उठाया है: मेरे गेम इंजन में टकराव के उचित प्रबंधन के लिए एक आवश्यक कार्यक्षमता को लागू करना।

अधिकांश गेम इंजनों के विपरीत जो वैक्टर का उपयोग करते हैं, PixiJS केवल आयताकार अनुमानों के रूप में bounds को संभालता है। यदि यह 2D में समस्याग्रस्त नहीं है, तो यह दृष्टिकोण एक गेम की आवश्यकताओं के लिए बहुत सीमित रहता है। ऊपर दी गई छवि हमारी वस्तुओं के bounds पर raycast के व्यवहार को दर्शाती है।


समस्या

हालाँकि, 3D में, यह दृष्टिकोण एक महत्वपूर्ण समस्या पैदा करता है। जब एक DisplayObject घूमता है, तो उसका bounds एक साधारण स्थिर आयत बना रहता है।

यहाँ एक उदाहरण है:



इसलिए मुझे इस कार्यक्षमता को लागू करने के लिए गणित और त्रिकोणमिति का अध्ययन करने में कई दिन समर्पित करने पड़े।

यह कार्यक्षमता आवश्यक होगी ताकि एक अभिनेता अपने परिवेश को समझ सके और वस्तुओं के साथ बातचीत कर सके, उनकी दूरी और ऊंचाई के आधार पर। यह raycast FOW (Fog of War) प्रणाली के लिए भी उपयोगी होगा, मानचित्र के कुछ हिस्सों और तत्वों को प्रदर्शित करेगा, उन टकरावों के आधार पर जो यह पता लगाता है।

यह एक ऐसा विषय है जिसे मैं केवल संक्षेप में छू रहा हूँ, क्योंकि यह बहुत जटिल है। मुझे फिर भी उम्मीद है कि यह लेख आपके अपने प्रोजेक्ट के लिए आपको प्रेरित कर सकता है।



एकीकरण

अपने दृष्टिकोण को सरल बनाने के लिए, मैंने अपनी वास्तुकला के भीतर एक Class Helper बनाया है। ये Class Helper `.h.ts` फाइलों में संग्रहीत हैं और विशेष रूप से Pixi ऑब्जेक्ट्स को निर्यात करते हैं।

ts
export class RayCasterHelper extends Container3d {
	public rays:Sprite3d[] = [];
	public raysVirtual:Graphics[] = [];
	public raysDistance:number[] = [];
	public boundsDebugs:Graphics[] = [];
	public raysTextDistance:Text3d[] = [];
	public rayLength:number = 900;
	public raySize:number = 12;
	public inCircleRadius:number = 10;
	public quadrans:number = 8;
	public shapes:CollisionShape[] = [];
}


आरंभीकरण पैरामीटर में निर्दिष्ट सेगमेंट की संख्या का निर्माण करता है। मेरे मामले में, 8 सेगमेंट का एक चतुर्थांश पर्याप्त से अधिक साबित हुआ है। यह कॉन्फ़िगरेशन डिबगिंग को भी आसान बनाता है, क्योंकि यह इन सेगमेंटों को 8 अलग-अलग रंग असाइन करने की अनुमति देता है।

ts
public initialize( renderer:Renderer ) {
		this.clear();
		const colors = [0x00ff00, 0xff0000, 0x0000ff, 0xffff00, 0x00ffff, 0xff00ff, 0x333444, 0xffffff];
		for ( let i = 0, l = this.quadrans; i < l; i++ ) {
			const color = colors[i % colors.length];
			const edges = [
				new Point( this.inCircleRadius, 0 ),
				new Point( this.inCircleRadius + this.rayLength, 0 ),
			];
			const ray = new Graphics();
			ray.lineStyle( this.raySize, color, 1 );
			ray.moveTo( this.inCircleRadius, 1 );
			ray.drawPolygon( edges );
			ray.endFill();

			const raySprite = new Sprite3d( renderer.generateTexture( ray ) );
			this.rays.push( raySprite );
			this.shapes[i] = new CollisionShape( raySprite, edges );

			raySprite.proj.euler.z = ( Math.PI * 2 / this.quadrans ) * i;
			this.raysDistance.push( 0 );


सेगमेंट के रूप में कार्य करने वाले बिंदुओं के निर्माण पर ध्यान देना उचित है। सामान्यतः, केवल विकर्णों के बिंदुओं की आवश्यकता होगी, क्योंकि एक आयत अपने क्षैतिज और ऊर्ध्वाधर अक्षों पर कोई ब्लाइंड स्पॉट प्रस्तुत नहीं करता है। इसलिए चतुर्थांश के प्रतिशत के आधार पर उन्हें अनदेखा करना संभव है, जिससे ऊर्ध्वाधर और क्षैतिज किरणों को छोड़कर, केवल विकर्णों पर बहुभुज उत्पन्न करना संभव हो जाता है।

ts
for ( let i = 0, l = this.quadrans; i < l; i++ ) {
			const color = colors[i % colors.length];

			// continue if is horizontal or vertical
			if( i % 2 === 0 ) continue;
			const edges = [...


फिर intersect विधि आती है, जो पैरामीटर के रूप में केवल DisplayObject स्वीकार करती है। इसे वे सभी DisplayObject प्रदान किए जाते हैं जिन पर raycast को दोहराना होगा।

ts
public intersects( worlds:Container3d[] ) {
		const { rays, quadrans, intersectedMap, intersectedObjectsMap, closestMap } = this;

		intersectedMap.clear();
		intersectedObjectsMap.clear();
		closestMap.clear();

		for ( let i = 0, l = quadrans; i < l; i++ ) {
			const ray = rays[i];
			const b1 = ray.getBounds( true, this._cacheRectangle1 );
			for ( const object of worlds ) {
				const b2 = object.getBounds( true, this._cacheRectangle2 );
				const intersect = intersects( b1, b2 );
				if ( intersect ) {
					const rayshape = this.shapes[i];
					if ( rayshape.intersectsShape( b2 ) ) {
						intersectedMap.set( i, ( intersectedMap.get( i ) ?? [] ).concat( object ) );
						intersectedObjectsMap.set( object, ( intersectedObjectsMap.get( object ) ?? [] ).concat( i ) );
					}
				}
			}
		}


मुझे PixiJS के एक फ़ंक्शन को भी अनुकूलित करना पड़ा ताकि प्रतिच्छेदन का पता लगाया जा सके और अतिरिक्त डेटा एकत्र किया जा सके। कोड नीचे प्रस्तुत किया गया है, यह मूल विधि से महत्वपूर्ण रूप से भिन्न नहीं है।

ts
/** return the intersection between two rectangles if any */
function intersects( source:Rectangle, other:Rectangle ) {
	let x0_1 = source.x < other.x ? other.x : source.x;
	let x1_1 = source.right > other.right ? other.right : source.right;
	if ( x1_1 <= x0_1 ) {
		return null;
	}
	let y0_1 = source.y < other.y ? other.y : source.y;
	let y1_1 = source.bottom > other.bottom ? other.bottom : source.bottom;
	if ( y1_1 <= y0_1 ) {
		return null;
	}

	// Compute the intersection distance
	const dx = x0_1 - x1_1;
	const dy = y0_1 - y1_1;
	const distance = Math.sqrt( dx * dx + dy * dy );

	// Return the intersection details
	return {
		x: x0_1,
		y: y0_1,
		x2: x1_1,
		width: x1_1 - x0_1,
		height: y1_1 - y0_1,
		distance: distance,
		inRatioXY: Math.abs( dx / dy ),
	};
}


अंत में, एक बार जब Pixi दो वस्तुओं के बीच प्रतिच्छेदनों का पता लगा लेता है, तो स्थानीय पता लगाने को परिष्कृत करना संभव है ताकि यह जांचा जा सके कि क्या हमारे विकर्ण सेगमेंट वस्तुओं को छूते हैं।


अनुकूलन मेमो:

पर्यावरण की वस्तुएँ क्लासिक वर्गाकार bounds का उपयोग करती हैं। दरअसल, कई हजारों संस्थाओं के साथ, एक अनुकूलित संरचना के बिना सभी सेगमेंटों पर आधारित एक टक्कर का पता लगाना बहुत गंभीर प्रदर्शन समस्याओं को जन्म देगा। इस प्रकार की गणना को अनुकूलित करने के लिए web workers या shaders का उपयोग करना शायद बेहतर होगा, बजाय इसे CPU द्वारा प्रबंधित करने के।

इसलिए केवल DisplayObject raycaster के विकर्ण ही बहुभुजीय टक्कर का पता लगाने का प्रबंधन करेंगे, इस प्रकार 50% की सटीकता का लाभ प्रदान करेंगे। 100% सटीकता प्राप्त करने के लिए, पर्यावरण की वस्तुओं को भी बहुभुज होना होगा, जो मेरे वर्तमान इंजन के लिए अनिवार्य नहीं है। शायद विकर्ण बाड़ को छोड़कर, जो संभावित रूप से समस्या पैदा कर सकते हैं... भविष्य बताएगा।

इसलिए यह IntersectsShape विधि vertex के प्रत्येक बिंदु पर दोहराएगी ताकि यह जांचा जा सके कि क्या ये बिंदु DisplayObject के bounds के भीतर हैं।

ts
intersectsShape( rec:Rectangle ) {
		const edges = this.edges;

		for ( let i = 0; i < edges.length; i++ ) {
			const edge = edges[i];
			const point = edge.intersectsRectangle( rec );
			if ( point ) {
				return true;
			}
		}

		return false;
	}

class Segment {
	public p1: Point;
	public p2: Point;
	constructor( p1 = new Point(), p2 = new Point() ) {
		this.p1 = p1;
		this.p2 = p2;
	}

	intersects( edge: Segment, asSegment = true, point = new Point() ) {
		const a = this.p1;
		const b = this.p2;
		const e = edge.p1;
		const f = edge.p2;

		const a1 = b.y - a.y;
		const a2 = f.y - e.y;
		const b1 = a.x - b.x;
		const b2 = e.x - f.x;

		const c1 = ( b.x * a.y ) - ( a.x * b.y );
		const c2 = ( f.x * e.y ) - ( e.x * f.y );
		const denom = ( a1 * b2 ) - ( a2 * b1 );

		if ( denom === 0 ) {
			return null;
		}

		point.x = ( ( b1 * c2 ) - ( b2 * c1 ) ) / denom;
		point.y = ( ( a2 * c1 ) - ( a1 * c2 ) ) / denom;

		if ( asSegment ) {
			const uc = ( ( f.y - e.y ) * ( b.x - a.x ) - ( f.x - e.x ) * ( b.y - a.y ) );
			const ua = ( ( ( f.x - e.x ) * ( a.y - e.y ) ) - ( f.y - e.y ) * ( a.x - e.x ) ) / uc;
			const ub = ( ( ( b.x - a.x ) * ( a.y - e.y ) ) - ( ( b.y - a.y ) * ( a.x - e.x ) ) ) / uc;

			if ( ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1 ) {
				return point;
			} else {
				return null;
			}
		}

		return point;
	}

	intersectsRectangle( rect: Rectangle ) {
		const rectEdges = [ //TODO: will need optimize with a cache pool of points
			new Segment( new Point( rect.left, rect.top ), new Point( rect.right, rect.top ) ),
			new Segment( new Point( rect.right, rect.top ), new Point( rect.right, rect.bottom ) ),
			new Segment( new Point( rect.right, rect.bottom ), new Point( rect.left, rect.bottom ) ),
			new Segment( new Point( rect.left, rect.bottom ), new Point( rect.left, rect.top ) )
		];

		for ( const rectEdge of rectEdges ) {
			if ( this.intersects( rectEdge, true ) ) {
				return true;
			}
		}
		return false;
	}
}


अंतिम प्रतिपादन


segmentIntersects विधि वह है जहाँ सबसे जटिल गणनाएँ की जाती हैं। इसे कार्यात्मक बनाने में मुझे दो दिन का गहन कार्य लगा, लेकिन परिणाम बहुत संतोषजनक है। जैसा कि इस gif में देखा जा सकता है, मैं अब अपने परिवेश के साथ बातचीत कर सकता हूँ और Raycasting के माध्यम से इसमें टैग और जानकारी जोड़ सकता हूँ।

यह साधारण उपकरण कई दिलचस्प कार्यक्षमताएँ प्रदान करता है जिनका उपयोग किया जा सकता है।