Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: JavaScript: Metapallojen optimointi

kilipukki [12.04.2017 22:15:39]

#

Moikka,

Olen aloitellut JavaScriptin ja ylipäätään ohjelmoinnin opettelua ja nyt kun hieman alkaa koodi taipua, niin tein perinteisen metapalloefektin. Lopputulos on minusta ihan hienon näköinen, mutta kun piirtoalueen kokoa kasvattaa, siitä tulee aivan tolkuttoman hidas. Olisiko kenelläkään antaa vinkkiä miten tätä voisi optimoida? Ongelma taitaa olla siinä, että tässä käsitellään joka ikinen pikseli joka kierroksella oli siinä tapahtunut jotain tai ei. En vain keksi miten voisin käsitellä vain ne pikselit, jossa jotain on tapahtunut. Ehkäpä se ei tällä menetelmällä olekaan mahdollista?

Piirtoalue käydään läpi kahdessa sisäkkäisessä silmukassa. Kokeilin, että auttaisiko, jos se käytäisiin läpi yhdessä silmukassa ja sijainti lasketaan kullekin pikselille erikseen, mutta se ei juuri parantanut suorituskykyä joten päätin käyttää kahta silmukkaa koska se oli minusta selkeämpi tapa. Jätin yhden silmukan piirtoalueen läpikäynnin kuitenkin kommentoituna koodiin jos jotakuta sattuu kiinnostamaan.

Koodi on alla ja täältä voi ihmetellä livenä: https://jsfiddle.net/mhpcLxsq/

<!DOCTYPE html>
<html>
<head>
	<title>Hellurei</title>

	<style type="text/css">
		body {margin: 0px}
		canvas {position: absolute}
	</style>

</head>
<body>

<div>
	<canvas id="canvas">
	</canvas>
</div>

<script type="text/javascript" src="Metapallot.js">
</script>

</body>
</html>
// canvas ja piirtokonteksti

const c = document.getElementById("canvas");
const width = c.width = window.innerWidth;
const height = c.height = window.innerHeight;
const ctx = c.getContext("2d");

// imagedata

const imageData = ctx.createImageData(width,height);
const data = imageData.data;
const dataLength = data.length;

// array palloja varten, ja pallojen lukumäärä

const balls = [];
const numBalls = 20;

// satunnaislukujen arpoja

function random(min, max){
	var num = Math.floor(Math.random() * (max - min + 1) + min);
	return num;
}

// pallon sijainti, koko ja liikkeen suunta ja nopeus

function ball() {
	this.posX = random(0, width);
	this.posY = random(0, height);
	this.dx = random(-4,4);
	this.dy = random(-10,10);
	this.radius = random(10, 100);
}

// pallon liikutus

ball.prototype.move = function() {
	if (this.posX > width || this.posX < 0) { // jos on kohdattu piirtoalueen reuna x-akselilla, vaihdetaan suuntaa
		this.dx = -(this.dx);
	}

	if (this.posY > height || this.posY < 0 ) { // jos on kohdattu piirtoalueen reuna y-akselilla, vaihdetaan suuntaa
		this.dy = -(this.dy);
	}

	this.posX += this.dx; // liikutetaan palloja
	this.posY += this.dy; // liikutetaan palloja
}

// pallojen liikutus

function updateLocation() {
	for (i=0; i<balls.length; i++) {
		balls[i].move();
	}
}

// luodaan pallot ja työnnetään taulukkoon

for (i=0; i<numBalls;i++) {
	var pallo = new ball();
	balls.push(pallo);
}

// piiretään liikutellaan pallukoita

function renderBalls(){

/*

	// piirtoalueen läpikäynti yhdessä silmukassa

	for (var index = 0; index < dataLength; index +=4 ){	// käydään piirtoalueen pikselit läpi, pituus
		var y = (index / 4) / width;						// pikselin y positio
		var x = (index / 4) % width;						// pikselin x positio
		var color = 0;										// pikselin väri

		for (i=0; i<numBalls; i++) {
			color += balls[i].radius * 5000 / (Math.pow((x - balls[i].posX),2) + Math.pow((y - balls[i].posY),2));
			if (color >= 255) break;
		}

			// annetaan pikselille väri
			data[index+0] = color*1.05;	// r
			data[index+1] = color;		// g
			data[index+2] = color*1.4;	// b
			data[index+3] = 255;		// a

	}

*/

	// piirtoalueen läpikäynti kahdessa silmukassa

	for (var y=0; y<height; y++){		// käydään piirtoalueen pikselit läpi, pituus
 		for (var x=0; x<width; x++){	// käydään piirtoalueen pikselit läpi, leveys
 			var index = (x+y*width)*4;	// pikselin indeksi
 			var color = 0;				// pikselin väri

 			for (i=0; i<numBalls; i++) {
 				color += balls[i].radius * 5000 / (Math.pow((x - balls[i].posX),2) + Math.pow((y - balls[i].posY),2)); // kaava metapalloja varten eli
 				if (color >= 255) break; // jos väri menee yli 255, lopetetaan
 			}

 			// annetaan pikselille väri
 			data[index+0] = color*1.05;	// r
 			data[index+1] = color;		// g
 			data[index+2] = color*1.4;	// b
 			data[index+3] = 255;		// a

 	}
 }

	// laitetaan kuvan data canvakselle

	ctx.putImageData(imageData, 0,0);

}

// metapallojen animointi

function animate() {
	renderBalls();
	updateLocation();
	requestAnimationFrame(animate);
}

animate();

Metabolix [12.04.2017 23:35:42]

#

Yksinkertaisella matematiikalla voi arvioida maksiminopeutta. Koodi vie parhaimmillaankin noin 20 operaatiota/pikseli/pallo. Oletetaan 20 palloa ja 1000×1000 pikseliä ja 2 GHz:n kone. Ruudun päivitysnopeudeksi tulee 2 GHz / (20 × 20 × 1000 × 1000) = 5 Hz. Edes huippuoptimoinnilla ei pääse paljon tämän paremmaksi. JS:llä varmasti jää vielä hieman tästäkin. Käyttämällä GPU:ta ja jotain rinnakkaislaskentaa toki voi päästä parempiin tuloksiin, mutta se on vielä aika kokeellista hommaa nettisivuilla.

Nyt käytät silmukoissa globaalia i:tä, mikä on hidasta. Ainakin Chromiumissa koodia nopeuttaa noin 20%, jos laitat var-sanat puuttuviin kohtiin. Kannattaa laittaa use strict koodin alkuun, ettei näitä virheitä tule.

Kun silmukat kääntää niin päin, että ulommassa käydään läpi pallot ja sisemmässä pikselit, nopeutuu vielä 60%. Tämä edellyttää, että ensin lasketaan color-taulukko ja lopuksi vasta laitetaan siitä pikselit ruudulle.

Koska metapallot vaikuttavat ainakin vähän joka pikseliin, on vaikea optimoida koodia siltä kannalta. Jos alue on todella iso ja pallot todella pieniä, voi rajata kunkin pallon kohdalla pikselisilmukan kapeammaksi niin, että lopetetaan, kun pallon vaikutus väriin on häviävän pieni. Ainakaan näillä palloilla ja normaalin kokoisella ruudulla tästä ei vielä tule apua...

Ohessa on koodi korjattuna kahdella ensiksi mainitsemallani optimoinnilla:

var color = new Array(width*height);
function renderBalls() {
	color.fill(0);
 	for (var i = 0; i < balls.length; ++i) {
		for (var y = 0; y < height; ++y) {
			for (var x = 0; x < width; ++x) {
				var index = x + y * width;
				var dx = x - balls[i].posX, dy = y - balls[i].posY;
				color[index] += balls[i].radius / (dx*dx + dy*dy);
 			}
 		}
 	}
	for (var y = 0; y < height; ++y) {
		for (var x = 0; x < width; ++x) {
			var index = x + y * width;
			var c = 5000 * color[index]
 			data[4*index+0] = c * 1.05;
 			data[4*index+1] = c;
 			data[4*index+2] = c * 1.4;
 			data[4*index+3] = 255;
		}
	}
	ctx.putImageData(imageData, 0,0);
}

groovyb [13.04.2017 00:10:23]

#

eikös tuosta saa vielä nuo alemmat loopit kokonaan pois, yhdistämällä data -asetukset suoraan ylempään looppiin?
Mietin vain mistä tuo nopeusetu tulee kahdella silmukalla, jos kerran index ja color[index] on jo tiedossa ylemmässä silmukassa.

**Edit** ähh jokainen koordinaattihan läpikäydään pallojen verran, jos ne yhdistetään samaan looppiin.

var color = new Array(width*height);
function renderBalls() {
	color.fill(0);

 	for (var i = 0; i < balls.length; ++i) {
		for (var y = 0; y < height; ++y) {
			for (var x = 0; x < width; ++x) {
				var index = x + y * width;
				var dx = x - balls[i].posX, dy = y - balls[i].posY;
				color[index] += balls[i].radius / (dx*dx + dy*dy);
       	        var c = 5000 * color[index]
 				data[4*index+0] = c * 1.05;
 				data[4*index+1] = c;
 				data[4*index+2] = c * 1.4;
 				data[4*index+3] = 255;
 			}
 		}

 	}

	ctx.putImageData(imageData, 0,0);
}

kilipukki [13.04.2017 14:24:04]

#

Kiitos vinkeistä!

Toinen tapa tehdä tämä olisi varmaan jokin sellainen, että pallolle piirrettäisiin liukuväri ja määriteltäisiin vaikutusalue (pallon säde?). Jos sitten pallot tulevat toistensa vaikutusalueelle, laskettaisiin väriarvot yhteen jolloin ne sulautuvat toisiinsa. Sillon ei tarvitsisi rullata koko ruudun pikseleitä läpi ja suorituskyky olisi varmaankin parempi.

groovyb [13.04.2017 14:58:13]

#

Aivan varmasti olisi. Eli jos koko canvas alustetaan mustaksi, ja tämän jälkeen päälle piirretään vain pallojen pikselit. Toki jos pallojen piirtoalue täyttää koko ruudun, ei tilanne olennaisesti muutu, lisähyöty saadaan vain mustaksi jääneiden pikselien osalta.

Metabolix [13.04.2017 15:39:02]

#

kilipukki kirjoitti:

Toinen tapa tehdä tämä olisi varmaan jokin sellainen, että pallolle piirrettäisiin liukuväri – –

Värin haku muistista on hitaampaa kuin laskeminen. Turhien pikseleiden ohittamisen voi tehdä aivan hyvin nykyiseenkin silmukkaan, kun vain laskee y- ja x-silmukoihin raja-arvot pallon sijainnin ja koon mukaan. Käytännössä tuossa pallot täyttävät niin suuren osan piirtopinnasta, ettei optimoinnista ole juurikaan hyötyä.

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta