Java-esimerkkien vähäisyyden vuoksi metapallot vaihteeksi javalla toteutettuna.
Toisin kuin yleisesti luullaan, java ei enää nykyään ole kovin paljon hitaanpaa kuin esim. C++. Tästä esimerkkinä laiton metapalloja ohjelmaa hulppeat 75, joiden pitäisi liikkua suht' sulavasti vähän vanhemmallakin koneella.
Nopeus perustuu pitkälti taulukkoon laskettuun metapallon kuvaajaan ja rajoittumiseen yhden pallon laskennassa melko pienelle alueelle.
Tässä on käytössä myös harvemmin java-esimerkeissä käytetty 320x200 fullscreen moodi.
Valmiiksi käännetty binaari ja alkuperäinen tiedosto
http://www.niksula.cs.hut.fi/~jasainio/java/metaballs.zip
Ajetaan komentoriviltä: java MetaBalls
MetaBalls.java
import java.awt.*; import javax.swing.*; import java.awt.image.*; import java.awt.event.*; import java.util.Arrays; import java.util.Random; public class MetaBalls extends Canvas implements KeyListener { /** Ruudun muistipuskuri */ private int[] screen; //pallojen lukumäärä ja tietorakenteet private static final int BALLS = 75; private int[] ballXY = new int[BALLS]; private int[] ballSpeed = new int[BALLS]; private int[] ballRadius = new int[BALLS]; /** Taulukko, jossa pallon kuvaaja */ private int[] metaArray; /** Käytetty väripaletti */ private int[] colorMap = new int[256]; /** Ruudulle piirrettävä renderöity kuva */ private Image screenImage; /** * Taikakalu, joka saa screenImagen käyttämään * screen-taulukkoa muistipuskurina */ private MemoryImageSource mis; /** Kuvan koko */ public int w, h; private Random r = new Random(); public MetaBalls() { w = 320; h = 200; metaArray = new int[256*256]; initMetaArray(); initBalls(); initColorMap(); screen = new int[ w * h ]; // luodaan yhteys integer taulukon ja piirrettävän kuvan välille mis=new MemoryImageSource(w,h,ColorModel.getRGBdefault(),screen,0,w); mis.setAnimated(true); mis.setFullBufferUpdates(true); screenImage = createImage(mis); addKeyListener(this); } /** * Luo väripaletin. */ private void initColorMap() { int r = 255; int g = 0; int b = 0; // musta -> punainen for (int i = 0; i < 128; i++) { colorMap[i] = 0xff000000 | (((int)(( i * r )/128.0))<<16); } int g2 = 255; int b2 = 0; // punainen -> keltainen for (int i = 128; i < 192; i++) { int green = g + (int)(( (i-128) * ( g2-g))/64.0); int blue = b + (int)(( (i-128) * ( b2-b))/64.0); colorMap[i] = 0xff000000 |(255 <<16)|(green<<8)|blue; } b = 255; // keltainen -> valkoinen for (int i = 192; i < 256; i++) { int blue = b2 + (int)(( (i-192) * ( b-b2))/64.0); colorMap[i] = 0xff000000 |(255 <<16)|(255<<8)|blue; } } /** * Arpoo palloille alkupaikan, koon ja nopeuden. */ private void initBalls() { int n = w*h; for (int i = 0; i < BALLS; i++) { ballXY[i] = r.nextInt(n); ballSpeed[i] = (r.nextInt(16)-8)*w+ r.nextInt(16)-8; ballRadius[i] = 40 + r.nextInt(40); } } /** * Lasketaan taulukkoon yhden metapallon arvot. * Kaikki pallot pirretään skaalaamalla koordinaatit * tähän taulukkoon. */ private void initMetaArray() { //luku, jolla kerrotaan kaavan tulokset //ja jota pienempiä arvoa ei sallita //(jotta kuvio pysyisi pyöreänä) int cutLevel = 4; for (int x = 0; x < 256; x++) for (int y = 0; y < 256; y++) { int dx = x-128; int dy = y-128; int D = dx*dx + dy*dy; double d; if (D != 0) { d = (cutLevel*16384.0 / D); //c*128*128 / d^2 if (d > 255) D = 255; else if (d < cutLevel) D = 0; else D = (int)d; } else D = 255; metaArray[(y<<8) + x] = D; } } /** * Piirretään ainoastaan jo valmiiksi laskettu kuva */ public void paint(Graphics g) { g.drawImage(screenImage,0,0,this); } public void update(Graphics g) { paint(g); } /** * Renderointilooppi. Piirtää yhden kuvan muistipuskuriin. */ private final void render() { //tyhjennetään ruutu Arrays.fill(screen, 0); for (int i = 0; i < BALLS; i++) { int r = ballRadius[i]; //pallojen liikutus ballXY[i] -= ballSpeed[i]; if (ballXY[i] < -r*w) ballXY[i] += w*(h+r); else if (ballXY[i] > (h+r)*w) ballXY[i] -= w*(h+r); int bY = ballXY[i] / w; int bX = ballXY[i] % w; int minY = bY-r+1; if (minY < 0) minY = 0; int maxY = bY+r-1; if (maxY >= h) maxY = h-1; int minX = bX-r+1; if (minX < 0) minX = 0; int maxX = bX+r-1; if (maxX >= w) maxX = w-1; int rowStart = minY*w; int rowMinX = minX; //lasketaan rajat taulukkoon sopivaksi //(jaetaan lisäksi säteellä ja lisätään 128 // niin saadaan arvoja 0-255) minX = (minX - bX) << 7; maxX = (maxX - bX) << 7; minY = (minY - bY) << 7; maxY = (maxY - bY) << 7; int offs; // Pääasiallinen laskentalooppi for (int yDiff = minY; yDiff < maxY; yDiff+=128) { offs = rowStart+rowMinX; int mbY = ((128 - yDiff/r) //skaalataan etäisyys taulukkoon sopivaksi << 8 ) // Y on taulukon rivi (*256) +128; // X valmiiksi puoleenväliin for (int xDiff = minX; xDiff < maxX; xDiff+= 128) { screen[offs++ ] += metaArray[mbY + xDiff/r]; } rowStart += w; } } // piirretään lopullinen kuva ruudulle int n = w*h; for (int i = 0; i < n; i++) { int x = screen[i]; if (x > 255) x = 255; else if (x < 30) x = 0; screen[ i ] = colorMap[x]; //screen[ i ] = 0xff000000 | x; } } public void keyPressed(KeyEvent e) { if (e.getKeyCode() == e.VK_ESCAPE) System.exit(0); } public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) {} /** * Laskee seuraavan kuvan * ja ilmoittaa, että uusi kuva * olisi tarjolla. */ public void nextFrame() { render(); mis.newPixels(0, 0, w, h); } public static void main(String[] args) { MetaBalls mb = new MetaBalls(); JFrame frame = new JFrame("MetaBalls"); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.setSize(mb.w,mb.h); frame.getContentPane().add(mb); GraphicsEnvironment env = GraphicsEnvironment. getLocalGraphicsEnvironment(); GraphicsDevice device = env.getDefaultScreenDevice(); DisplayMode[] modes = device.getDisplayModes(); try { //vaihdettaan 320x200 fullscreen moodiin, jos sitä tuetaan for (int i = 0; i < modes.length; i++) { if (modes[i].getWidth() == 320 &&modes[i].getHeight() == 200 &&modes[i].getBitDepth() == 32) { frame.setUndecorated(true); device.setFullScreenWindow(frame); device.setDisplayMode(modes[i]); break; } } frame.show(); //focus sisäkomponentille, jolla on näppänhandleri mb.requestFocus(); Thread.currentThread().setPriority(Thread.MIN_PRIORITY); //päivityslooppi while(frame.isShowing()) { mb.nextFrame(); try { Thread.sleep(1); }catch(Exception e) {return;} } } finally { //pois fullscreenistä device.setFullScreenWindow(null); } } }
Varsin kauniin näköinen efekti, ja suht. nopeakin vielä. Voisitko kommentoida jonkun seuraavanlaisen pätkän:
colorMap[i] = 0xff000000 | (((int)(( i * r )/128.0))<<16);
Itselleni on hieman epäselvää, mitä tuo vaiheittain tekee.
Toimii tosiaan ihan kivalla nopeudella. Oma kone pyörittää 200 palloa ilman mitään ongelmia, ja tuhannella pallollakin yhden kuvan renderöinti kestää n. 150 millisekuntia (vaikka tulos onkin melko tylsän yksivärinen...)
Aikas nätti
colorMap[i] = 0xff000000 | (((int)(( i * r )/128.0))<<16);
Tuossa lasketaan punavärin liuku 0-255.
"0xff000000" on tuon värimallin vaatima alpha-arvo eli läpinäkyvyys (asetataan maksimiin eli ei läpinäkyvyyttä)
"<< 16" siirtää sisemmässä lausekkeessa lasketun arvon punavärin paikkaan rgb-arvossa.
"i * r / 128.0"
tuossa i saa arvot 0-127 ja r = 255, joten punainen värikomponentti saa arvoja 0-255.
Tuo ei kuitenkaan ole kovin kriittinen asia koodissa. Kyseinen väripaletti lasketaan vain kerran ohjelman alussa ja väripalettina voi käyttää mitä palettia vaan.
Yleisesti olen joka paikassa pyrkinyt korvaamaan kaikki jako ja kertolaskut shift operaatioilla, koska ne vaan ovat niin paljon nopeampia. Eli vasemmalle shiftaus (<<) kertoo integer lukua jollakin 2:n potenssilla ja oikealle shiftaus (>>) jakaa samaisella luvulla.
Kiitoksia selityksistä. Itse en ole vielä tuohon shiftaukseen pahemmin tutustunut. Löytyisikö noiden värien määrityksestä jotain URLia, kun nuo ovat ilmeisesti jonkin yleisen säännön mukaan määritelty? Tuossa "i * r / 128.0" suurin arvo taitaa olla 253(?), mutta sillä nyt ei liene niin kovin suurta väliä :).
EDIT: Nyt meni hieman keskusteluksi, mutta annan tämän viestin kuitenkin olla, jotta yhteys seuraavaan vastaukseen säilyisi muillekin lukijoille selvänä.
Yleisesti argb arvot esitetään integereinä siten, että ylimmät 8 bittiä ovat alpha arvoa, seuraavat 8 puna-arvoa, sitten 8 bittiä vihreää ja 8 sinistä.
8 bittiä pystyy sisältämään lukuarvoja 0-255. Yleisesti esim. HTML-puolella väriarvot esitetään heksa-lukuna jolloin kunkin 8 bittiä pystyy esittämään kahdella heksamarkillä (00-FF).
Koko luku voidaankin esittää siis muodossa 0xAARRGGBB, jossa kirjainten paikalle asetetaan haluttu arvo (0x alussa on C:n ja Javan tapa ilmaista luku heksana).
True-color väreissä siis lasketaan jokaiselle värille luku 0-255 ja siten yhdistetään ne yhdeksi luvuksi.
(alpha<<24) | (red << 16) | (green << 8) | blue
Shiftaukset siirtävät luvun oikeaan paikkaan ja OR-operaatio yhdistää luvut bitti kerrallaan (bitti on 1, jos jommassa kummassa operaation luvussa kyseinen bitti on 1, muutoin 0).
Onko tuo Javassa erilailla kuin C:ssä vai oletko nyt hieman erehtynyt? Tässä on C-kääntäjän RGB-makro, joka siis luo väriarvoista yhtenäisen luvun.
#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(
Kuten näet, (r | (g << 8) | (b << 16) | (a << 24))
C on vähän laiteläheisempi kieli kuin java ja noiden väritavujen järjestys riippuu siitä onko kone big endian vai
little endian eli onko merkitsevin bitti lopussa vai alussa. En ole kuitenkaan grafiikkaa viimeaikoina koodannut C:llä, joten en osaa tarkenpaa sanoa C:n toiminnasta.
Javassa asia kuitenkin on noin kuten tuossa esitin.
Metabolix: tuo nyt ei kuitenkaan ole C-kääntäjän makro, vaan windows.h:n. Eli itse C-kielen kanssa tuolla ei sinänsä ole tekemistä.
Harmi, se käyttää Swingiä... Joillekin vanhoille kääntäjille extends Frame-jutut olisi mukavampia
Harmi, se käyttää Swingiä... Joillekin vanhoille kääntäjille extends Frame-jutut olisi mukavampia
lainaus:
Harmi, se käyttää Swingiä... Joillekin vanhoille kääntäjille extends Frame-jutut olisi mukavampia
Eipä se nyt kovin paljon swingiä käytä; JFrame taisi tulla vanhasta muistista. Tein toisen version, jossa käytetään Frame:a. http://www.niksula.cs.hut.fi/~jasainio/java/
Toisaalta vanhojen java-kääntäjien ja virtuaalikoneiden käyttämiseen ei ole kovin monia hyviä syitä. (MS:n bugisen virtuaalikoneen tukeminen ei ole hyvä syy :)
Oon miettiny pääni puhki, että miten niitä palloja voisi lisätä VK_UP nappia painamalla yksitellen, ja vielä lisäksi ne seurais hiirtä. Jos tiedät miten se onnistuu niin voitko neuvoa :D Nuo ois kivoja lisiä, ehkä vähän monimutkaisempia, tuohon ohjelmaan.
Tein tuollaisen esimerkin, jossa palloja voi lisäillä ja poistaa nuolinäppäimillä. Pallot nyt myös pörräävät hiiren ympärillä. Muistaakseni metapalloista oli ihan hyvä hiirtä seuraava esimerkki myös VB esimerkkien puolella.
http://www.niksula.cs.hut.fi/~jasainio/java/MB.
Aihe on jo aika vanha, joten et voi enää vastata siihen.