Instructables Universe in Three.js

13 Schritt:Schritt 1: Verarbeitung: Generieren Sie die Hintergrundbilder Schritt 2: Kinetic.js: Gruppieren der Sterne Schritt 3: Kinetic.js: Suggestive Constellations Schritt 4: Kinetic.js: Hit-Leistung mit 20.000 Punkten, in Canvas Rendered Schritt 5: Three.js: Erste Schritte Schritt 6: Three.js: Particle Systems Schritt 7: Three.js: Particle Systems in Motion Schritt 8: Three.js: eine andere Helligkeit für jeden Star: WebGL Shaders Schritt 9: Three.js: Hit-Leistung mit 20.000 Punkten in 3D Schritt 10: TweenMax: easy Animation (Camera Motion) Schritt 11: Three.js: Camera Positioning Schritt 12: Three.js: Nachbearbeitung & Effects: Dimmen, Verwischen Schritt 13: Andere Bibliotheken (Besonderer Dank an)

Instructables Universe in Three.js

Instructables Universe in Three.js

Instructables Universe in Three.js

Instructables Universe in Three.js

Instructables Universe in Three.js

Instructables Universe in Three.js

Was ist in der Instructables?

Wenn Sie kommen, um uns bei Instructables besuchen, werden Sie einen riesigen Touchscreen, der mit Unterstützung der Erklärung beauftragt zu sehen. Auf dieser Touchscreen sind etwa 20.000 Lichtpunkte, die jeweils ein Projekt auf Instructables, die Top-Performance all unserer vorgestellten Inhalte. Es ist da, um zu helfen, diese Frage zu beantworten für die Besucher, die nicht wissen können oder können mit ihm vertraut überhaupt, und um eine Vorstellung von der schieren Weite Leidenschaften in unserer Gemeinde zu geben. Die Instructables Galaxy ist ein Teil Daten-Visualisierung, und ein Teil der interaktiven Ausstellung. Es ist nicht dazu gedacht, in jedes Projekt zu graben, aber sie alle, und ihre Beziehungen untereinander einzuführen.

Das Galaxy Projekt war für die meisten von einem Jahr an dieser Stelle noch nicht abgeschlossen. Es ist durch viele Iterationen gegangen. Zum ersten Mal ist es nun klein genug, um auf dem Netz zu arbeiten. Sie werden Chrome benötigen . Das voll interaktiven 3D-Demonstration wird alles Ihrem Computer zu bieten hat.

ENTER THE GALAXY



Diese Instructable ist, wie die Galaxy gemacht. Ich werde Sie auf meiner Reise zu nehmen. Es war nicht linear. Ich begann in Kinetic.js und dann fallenge es für Three.js so dass ich könnte all die Sterne bewegen, und noch ein wenig mehr Whizknall Bewegung, wenn jemand den Bildschirm berührt. Hier ist, wo wir hingehen:

- Verarbeitung: eine Skizze, die die Hintergrundbilder erzeugt
- Kinetic.js: Wie die "Stars" gruppiert sind
- Kinetic.js: Wie man suggestive schau Konstellationen erzeugen
- Kinetic.js: Hit-Leistung mit 20.000 einzelnen Punkten, in Leinwand gemacht
- Three.js: Erste Schritte
- Three.js: Particle Systems
- Three.js: Particle Systems in Bewegung
- Three.js: Ein unterschiedlicher Helligkeit für jeden Stern WebGL Shaders
- Three.js: Hit-Leistung mit 20.000 einzelnen Punkte in 3D
- TweenMax: easy Animation
- Three.js: Kamerabewegung
- Three.js: Nachbearbeitung & Effekte: Dimmen, Verwischen
- Weitere Bibliotheken (Besonderer Dank an)

Schritt 1: Verarbeitung: Generieren Sie die Hintergrundbilder

  1. Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Alle 8 Artikel anzeigen

    Zunächst verbrachte ich einige Zeit Blick auf Raum . Nach fast Notwasserung die Idee, genau dann und dort, als ich bemerkte, dass die Standard-Mac-Desktop-Hintergrund war auffallend Nähe, wo ich unterwegs war, setzte ich mich auf einige Details über die Bilder des Raumes und der Sternhaufen sich feststellen:
    - Persönliche Sterne unterscheiden sich in Größe, Helligkeit, Farbe (rot bis blau), und die umliegenden Wolken aus Gas
    - Cluster erscheinen sowohl wegen Ansammlungen von Sternen und wegen der Art, wie sie Gase mehr in ihrer Nähe Licht
    - Die Schönheit kommt größtenteils aus Mischfarben
    - "Haze" ist entscheidend
    - Helligkeit Cluster in Gruppen, die konzentrisch zu fühlen, aber noch nicht einmal
    - Viele Bilder haben dunklere Bereiche an den Rändern; Vignette-Effekt
    - Star Farben müssen "Blend" mit ihren Hintergrundfarbe. Es gab sehr wenige blaue Sterne in sonst platz rot erscheinen.

    Die Verarbeitung Skizze für das Hintergrundbild versucht, diese Regeln in den Code zu aktivieren:
      PVector Zentrum;
     schwimmen diagonal;
     
     Leere setup ()
     {
       int width = 960, height = 440;
       Größe (Breite, Höhe);
      
       Mitte = new PVector (Breite / 2, Höhe / 2);
       diagonal = dist (0,0, center.x, center.y);
      
       noiseDetail (5, 0,5);
       colormode (HSB, 1);
    
       for (int i = 1; i <100; i ++) {
         makeNew (i);
       }
    
     }
    
     nichtig makeNew (int index) {
       schweben hueSeed = random (0.4,1), saturationSeed = random (0.4,1), brightnessSeed = random (0,2);
      
       Farbe dunkel = Farbe (hueSeed, saturationSeed, brightnessSeed); 
       Farblichtfarbe = (hueSeed-zufälligen (0,4), saturationSeed-zufälligen (0,4), brightnessSeed + random (0,3));
      
       setGradient (0, 0, (float) Breite (float) Höhe, dunkel, Licht);
    
       noStroke ();
     
       Wolken (1, zufälligen (1,3), Random (.5,1.5), Random (0,005, 0,02), Random (0,8));
       Wolken (1, zufälligen (1,3), gelegentlich (1,5), Random (0,005, 0,02), Random (0,8));
       Wolken (1, zufälligen (3,5), Random (3,4), Random (0,005, 0,02), Random (0,8));
      
       Speichern ("Hintergründe /" + Index + ".jpg");
     }
    
     Leere Wolken (float xCoeff, schweben yCoeff, schweben lightnessMultiplier, schweben kNoiseDetail, schweben maxOpacity) {
       for (int y = 0; y <Höhe; ++ y)
       {
         for (int x = 0; x <width; ++ x)
         {
           float v = Rauschen (x * kNoiseDetail * 1,2, y * kNoiseDetail * 1.2, millis () * 0001.);
           schweben Farbton, Sättigung, Helligkeit, alpha, Abstand;
           distance = dist (xCoeff * x, * y yCoeff, xCoeff * center.x, yCoeff * center.y);  // Beachten Sie, dass Abstand ellipsoid berechnet
           hue = 1;  // Suchen Bereich von 0,7 -> 0,4 ​​(Verpackung)
           Sättigung = 0,75 - V;
           Leichtigkeit = v * lightnessMultiplier;  // Heller zur Mitte hin
           alpha = maxOpacity - Abstand * 0,6 / diagonal;
           füllen (Farbton, Sättigung, Helligkeit, alpha);
           rect (x, y, 1,1);   
         }
       }
     }
    
     Leere setGradient (int x, int y, w float, float h, Farbe c1, c2 Farbe) {
       NOFILL ();
         for (int i = y; i <= y + h; i ++) {
           schweben inter = map (i, y, y + h, 0, 1);
           Farbe c = lerpColor (c1, c2, inter);
           Hub (c);
           Linie (x, i, x + w, i);
         }
     }
    

    Es ist nichts Besonderes hier:
    makeNew wählt zwei Farben (eine randomisierte Farbton, der andere einen dunkleren Farbton davon). Danach ruft Wolken dreimal mit verschiedenen Parametern zu mehreren übereinanderliegenden Variationen Trübung zu erzeugen. Dann speichert es das Bild.
    Wolken Schleifen über jedes Pixel, das Mischen Perlin Noise mit einer entfernungsabhängigen dropoff für alpha und Helligkeit der Wolke. Dies summiert sich zu einem splotchy + Vignette-Effekt für jedes Bild, unabhängig von der "Härte" der Wolkenrand, die Größe der Wolke in x oder y oder den beteiligten Farben. Die vielen magischen Zahlen in dieser Funktion enthalten sind das Ergebnis von Versuch und Irrtum, nicht jede Art von Strenge.
    setGradient gilt einen dunkleren Farbton auf den unteren Teil des Bildschirms als oben.
    Setup läuft dieser Schleife 100 Mal, so gibt es einige Hintergrundbilder zur Auswahl.
    Diese Bilder werden später in JavaScript die Leinwand, um die Kanten zu verbergen vignettiert. Es ist sicherlich richtig, dass dies in vielen anderen Orten (Verarbeitung, photoshop / gimp, threejs) getan worden, aber tut es in Javascript hat zwei Vorteile:
    1) Die Bilder müssen nicht vorher vignettiert werden; wenn ich meine Meinung über die Vignette Eigenschaften zu ändern, kann ich so tun, nachdem ich zusammen zu sehen, alle Teile und
    2) Mit der auf der Leinwand geladen Bild, habe ich die Gelegenheit, seine Pixel abzutasten, um eine Hintergrundfarbe für Three.js, die gut mit der jeweiligen Hintergrundbild passt zu wählen.

    Der Code, der dies im Wesentlichen tut lädt gerade ein Hintergrundbild (eine zufällige Auswahl aus der Verarbeitung Ausgang) und eine voreingestellte Transparenz Bild (in Gimp gezogen). Es nutzt die Transparenz JPEG für die Alpha-Kanal, und Rechtsnachfolger und RGBA Pixels entsprechend der Hintergrundbild und die Transparenz Bild. Das kombinierte Ausgangssignal wird als eine Textur für Three.js geladen. Inspiration für die Vignettierung Technik kommt von diesem Code , vollständige Tutorial hier .

Schritt 2: Kinetic.js: Gruppieren der Sterne


  1. Instructables Universe in Three.js

    Wenn auch nicht sofort ersichtlich ist die Galaxy nicht zufällig angeordnet.

    Klicken Sie auf eine der Top-Level-Kategorien, zum Beispiel, und du wirst einen Ring zu sehen:
    Instructables Universe in Three.js


    Untersuche einen einzelnen Kanal, und Sie werden eine enge Gruppe von Projekten finden Sie unter:
    Instructables Universe in Three.js

    Die Projekte werden von Kategorien und Kanäle Instructables 'geclustert. Die sechs Top-Level-Kategorie Ringe strahlen nach außen von der Mitte des Universums, und jeder Kanal innerhalb jeder Kategorie erhält einen gleichen Tortenstück des Rings. Dies führt zu einer "geclustert" Verteilung der Sterne, da einige Kategorien sind voller als andere, und einige Kanäle innerhalb der Kategorien sind voller als andere. Diese Cluster sind beide repräsentativ für die Balance von Projekten in Kategorien und Kanäle Instructables 'und bieten einige gewisse Ästhetik.

    Der Code, der dies geschehen war in KineticJS geschrieben, obwohl macht könnte im Klar JavaScript Processing, oder irgendetwas anderes geschrieben worden sein. Er weist einen Ring auf jede der sechs Kategorien und einem beliebigen Punkt (Null bis 2 * pi), die entlang dieses Rings, der als Mittelpunkt für jeden Kanal dient. Dies alles wird mit grundlegenden trigonometrischen Funktionen durchgeführt: x = r * cos (theta) und y = r * sin (theta), wobei r (Radius) aus dem Ring, an dem jedes Projekt befindet abgeleitet und theta aus dem "Zentrum abgeleitet Winkel "der einzelnen Kanäle. Als Projekte werden bearbeitet, erstellt Kinetic eine neue Ebene für den Kanal (wenn nötig) und fügt das Projekt in dieser Schicht. Die Schicht wird auf die Kategorie der Schicht gegeben, und alle Schichten werden zu dem Zeitpunkt hinzugefügt. Kinetic macht es einfach, all diese Zahlen bis zum Welt XY-Koordinaten, weshalb der Code nie umgeschrieben, wenn das Projekt zog nach Three.js ist zusammenbrechen. Jedes Projekt erhält eine Zufalls z, in einem engen Bereich.

    Vielleicht der interessanteste Teil ist die Schaffung einer Gauß-Verteilung, anstatt zufällige Streuung um diese Mittelpunkte:
      rnd_snd: function () {
         return (Math.random () * 2-1) + (Math.random () * 2-1) + (Math.random () * 2-1);
     } 
      Zufalls: function (Mittelwert, STABW) {
         zurück rnd_snd () * + stdev bedeuten;
     } 

    Dieser Trick hilfreich bereitgestellt durch Protonen-Fisch . Wenn Sie ein Ziel Mittelwert und Standardabweichung für jede Art von Normalverteilung Sie versuchen zu generieren sind zu haben, ist ein handliches nahen Annäherung an drei einheitliche randoms addieren.

Schritt 3: Kinetic.js: Suggestive Constellations

  1. Alle 9 Artikel anzeigen

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Blick durch Konstellationen
    Wenn Sie schielen, sieht eine Konstellation wie etwas.
    Mein Ansatz war hier, wieder, um durch Konstellationen aussehen . Die Sternbilder sah ich nur selten sah aus wie bestimmte Dinge (es schien mir, dass Löwen eine Person könnte eine andere Person die Maus, oder auch nur einen Platz mit ein paar Zeilen aus der IT sein), aber sie haben scheinen, eine suggestive Qualität von einigen abgeleitet teilen einfache und konsistente
    Geometrischen Regeln:
    - Es gibt keine kreuzenden Linien
    - Punkte (Sterne) verbinden meist mit benachbarten oder nahe benachbarte Sterne. Es ist ungewöhnlich, deutlich mehr Zeilen haben.
    - Es neigt dazu, sein (manchmal Null, manchmal auch zwei) geschlossen Polygon ... einen "Körper" in irgendeiner Form
    - Punkte haben ein, zwei, drei oder vier Verbindungen. Es gibt so gut wie nie fünf Verbindungen zu einem einzigen Punkt.
    - Sternbilder bestehen aus ca. 3-20 Sternen
    In Pseudocode, es ist so etwas wie dieses:

    Ersten Durchgang:
    - Starten Sie mit einem zufälligen Sterne
    - Vorschlag für eine Zeile auf die nächste UN-Attached-Sterne-
    - Prüfung, dass diese Linie keine vorhandenen Linien kreuzen
    - Zeichnen Sie diese Zeile, wenn er spielt nicht, wenn es nicht
    - Nächster nächste Stern Bewegen
    - Wiederholen

    Second Pass:
    - Suche Sternen ohne Verbindungen
    - Finden mindestens einer nicht-Kreuzungslinie von diesen Sternen zu ziehen, um sie zu verbinden

    Third Pass:
    - Fügen Sie eine Handvoll Nicht-Verbindungsleitungen
    Und schließlich der eigentliche Code landete ich mit:
      funktionieren ConstellationMaker3D (Optionen) {
         if (_.isUndefined (drei) || _.isUndefined (Galaxy) || _.isUndefined (Galaxy.Utilities) || _.isUndefined (Galaxy.TopScene)) {
             throw new Error ("fehlende Abhängigkeiten für ConstellationMaker3D");
         }
    
         // ConstellationMaker3D ist eine Funktion der Kamera-Objekt, da die 2-dimensionale Regeln müssen eine bestimmte Projektion um von der Arbeit
         this.init (Optionen);
     }
    
     ConstellationMaker3D.prototype.init = function (Optionen) {
         var Kamera = options.camera || Galaxy.Utilities.makeTemporaryCamera ();
         var Knoten = options.nodes;
    
         _.bindAll (this, 'GetConnections');
    
         this.camera = Kamera;  // Three.js Kameraobjekt
         this.nodes = this.projectPoints (Knoten);  // Vector2 die (Mathematik - abgeflachte Darstellung XYZ Punkte)
         this.segments = [];  // Line3 die (mathematische).  Beachten Sie, das sind 2D-Liniensegmente;  die 3d diejenigen erbracht werden, aber nicht Teil des Sternbildes Bau
         this.connections = [];  // Array verbundener instructable ids.  dh [[ID1, ID2], [ID2, ID3]]
         this.disconnectedNodes = []; // Vector3 ist noch nicht erledigt
         this.lineObject = null;  // THREE.Line () Objekt
    
         this.calculateConstellation ();
    
         if (options.hidden == true!) this.displayConstellation ();
     };
    
     ConstellationMaker3D.prototype.projectPoints = function (vector3List) {
         var, die diese =;
         zurück _.map (vector3List, Funktion (VEC) {
             var position = Galaxy.Utilities.vectorWorldToScreenXY (vec, that.camera)
                 vec2 = new THREE.Vector2 (Position.x, Position.y);
             vec2.instructableId = vec.instructableId;
             zurück vec2;
         });
     };
    
     ConstellationMaker3D.prototype.spatialPointsForConnections = function (connectionList) {
         zurück _.map (connectionList, Funktion (connectionPair) {
             zurück Galaxy.Utilities.worldPointsFromIbleIds (connectionPair);
         });
     };
    
     ConstellationMaker3D.prototype.displayConstellation = function (Rückruf) {
         // Das Three.js Objekte entsprechend der berechneten Objekten in der Szene
         var connectedPoints3d = this.spatialPointsForConnections (this.connections);
         var, die diese =;
    
         if (! _. isEmpty (connectedPoints3d)) {
             // Geometrie initialisieren, fügen ersten Punkt
             var LineGeometry = new THREE.Geometry ();
    
             // Verbinden nachfolgenden Punkten entlang der Kette von verbundenen Punkten
             _.each (connectedPoints3d, Funktion (Paar) {
                 var closerPair = Paar;
                 lineGeometry.vertices.push (closerPair [0]);
                 lineGeometry.vertices.push (closerPair [1]);
             });
    
             // Die Zeile angezeigt
             var Material = new THREE.LineBasicMaterial ({
                 linecap: "rund",
                 Farbe: 0xffffff,
                 Linienbreite: 2,
                 transparent: true,
                 Deckkraft: 0,5
             });
             this.lineObject = new THREE.Line (LineGeometry, Material, THREE.LinePieces);
             this.lineObject.name = "Konstellation";
             Galaxy.TopScene.add (this.lineObject);
         }
    
         if (typeof Rückruf === "Funktion") {
             zurückrufen ();
         }
     };
    
     ConstellationMaker3D.prototype.movePointsCloser = function (Paar) {
         // Teil der Anzeige der Konstellation Linien ist die Verkürzung der Segmente für grafische Wirkung.
         var end1 = pair [0] .clone ();
         var end2 = Paar [1] .clone ();
    
         // Jeden Punkt leicht nach der anderen zu bewegen
         var. diff = end2.clone () Unter (end1.clone ());
         diff.multiplyScalar (0,08);
    
         zurück [end1.add (diff.clone ()), end2.sub (diff.clone ())];
     };
    
     ConstellationMaker3D.prototype.clear = function () {
         if (! _. isNull (this.lineObject)) {
             Galaxy.TopScene.remove (this.lineObject);
         }
     };
    
     ConstellationMaker3D.prototype.calculateConstellation = function () {
         var currentNode = this.nodes.shift (), dass = this;
         while (this.nodes.length> 0) {
             currentNode = this.addSegmentFromNode (currentNode);
         }
     };
    
     ConstellationMaker3D.prototype.closestNodeToNodeFromNodeSet = function (testNode, nodesToTest) {
         _.each (nodesToTest, Funktion (potentialNextNode) {
             potentialNextNode.distance = testNode.distanceTo (potentialNextNode);
         });
    
         var sortiert = _.sortBy (nodesToTest, "Abstand");
         zurück nach;
     }
    
     ConstellationMaker3D.prototype.findLineLineIntersection = function (1, LEITUNG 2) {
         var eqn1, eqn2, INTX, inty;
    
         // Wenn die beiden Linien gemeinsam ein Ende (das heißt, sie sind aus dem gleichen Knoten gezeichnet), übergeben
         if (this.shareEndpoint (1, LEITUNG 2) === true) return false;
    
         eqn1 = this.equationForLine (line1);
         eqn2 = this.equationForLine (Linie 2);
    
         // Gleiche Steigung = keine Kreuzung
         if (eqn1.m == eqn2.m) return false;
    
         // X-Wert der Schnittpunkt
         INTX = (eqn2.b - eqn1.b) / (eqn1.m - eqn2.m);
    
         // Y-Wert der Schnittpunkt
         inty = eqn1.m * INTX + eqn1.b;
    
         // Wenn x oder y sind außerhalb des Bereichs für entweder Linie, gibt es keinen Schnittpunkt
         var Bereich = {
             Minx: Math.min (line1.start.x, line1.end.x)
             maxx: Math.max (line1.start.x, line1.end.x)
             miny: Math.min (line1.start.y, line1.end.y)
             maxy: Math.max (line1.start.y, line1.end.y)
         };
         if (INTX <range.minx ||​​ INTX> range.maxx) return false;
         if (inty <range.miny || inty> range.maxy) return false;
    
         Bereich = {
             Minx: Math.min (line2.start.x, line2.end.x)
             maxx: Math.max (line2.start.x, line2.end.x)
             miny: Math.min (line2.start.y, line2.end.y)
             maxy: Math.max (line2.start.y, line2.end.y)
         };
    
         if (INTX <range.minx ||​​ INTX> range.maxx) return false;
         if (inty <range.miny || inty> range.maxy) return false;
    
         return true;
     }
    
     ConstellationMaker3D.prototype.equationForLine = function (Linie) {
         // Gl Store m & b von y = mx + b
         var m, b;
    
         // Hang
         m = (line.end.y - line.start.y) / (line.end.x - line.start.x);
    
         // Y-Achsenabschnitt: b = y-mx.  Sub in Werte von einem bekannten Punkt.
         b = line.end.y - m * line.end.x;
         Rückkehr {m: m, b: b};
     }
     ConstellationMaker3D.prototype.shareEndpoint = function (1, LEITUNG 2) {
         if (line1.start.x == line2.end.x && line1.start.y == line2.end.y) return true;
         if (line1.end.x == line2.start.x && line1.end.y == line2.start.y) return true;
         if (line1.end.x == line2.end.x && line1.end.y == line2.end.y) return true;
         if (line1.start.x == line2.start.x && line1.start.y == line2.start.y) return true;
         return false;
     }
    
     ConstellationMaker3D.prototype.addSegmentFromNode = function (Knoten) {
         var nextNodeList = this.closestNodeToNodeFromNodeSet (Knoten, this.nodes);
         var proposedLine = this.lineConnectingNodes2D (Knoten, nextNodeList [0]);
    
         if (this.lineIntersectsPriorLines (proposedLine) == true) {
             this.disconnectedNodes.push (Knoten);
         } Else {
             this.connections.push ([node.instructableId, nextNodeList [0] .instructableId]);
             this.segments.push (proposedLine);
         }
    
         this.nodes = _.without (this.nodes, nextNodeList [0]);
         zurück nextNodeList [0];
     }
    
     ConstellationMaker3D.prototype.connectNodeMultipleTimes = function (Knoten, mal) {
         var engsten = this.closestNodeToNodeFromNodeSet (Knoten, this.allNodes)
         linecount = 0;
         for (var i = 2; i <closest.length && linecount <Zeiten; i ++) {
             var proposedLine = this.lineConnectingNodes2D (Knoten, die am nächsten [i]);
             if (! this.lineIntersectsPriorLines (proposedLine)) {
                 this.segments.push (proposedLine);
                 this.constellationLayer.add (proposedLine);
                 linecount ++;
             }
         }
     }
    
     ConstellationMaker3D.prototype.lineIntersectsPriorLines = function (proposedLine) {
         var, die diese =, intersectionFound = false;
         _.each (this.segments, Funktion (testSegment) {
             var schneiden = that.findLineLineIntersection.apply (dh, [testSegment, proposedLine]);
             if (intersect === true) {
                 intersectionFound = true;
             }
         });
         zurück intersectionFound;
     }
    
     ConstellationMaker3D.prototype.lineConnectingNodes2D = function (node1, node2) {
         zurück neue THREE.Line3 (neu THREE.Vector3 (node1.x, node1.y, 0), neue THREE.Vector3 (node2.x, node2.y, 0));
     }
    
     ConstellationMaker3D.prototype.getConnections = function (instructableId) {
         // Gibt ein Array von instructable-ID, an die die gelieferten id hat Verbindungen.
         var Wohnung = _.uniq (. _ glätten (this.connections));
         var index = _.indexOf (flach, instructableId);
    
         Schalter (index) {
             Bei -1:
                 zurück [];
             Bei 0:
                 zurück [Flach [1]];
             Bei flat.length-1:
                 zurück flat [flat.length-2];
             Standard:
                 zurück [flat [index-1], flach [index + 1]];
         }
         console.log (instructableId + + Index + 'in' + flat 'gefunden');
     }
    


    Der Umzug von KineticJS zu ThreeJS entschieden erschwert Dinge. Konstellationen sind grundsätzlich in der Natur 2d: sie Verbindungen zwischen den Punkten in 3 Dimensionen (auch wenn Sie Ptolemäus fragen), aber die Konstellation selbst spannt eine bestimmte Perspektive von der Erde. Linien, die an uns offenbar nicht überqueren kann in der Tat Kreuz, wenn Sie sie von der Seite zu sehen, wie sie in der interaktiven Demo zu tun.
    Da ThreeJS arbeitet auf 3D-Objekte, ein Verfahren zum Falten der Daten an eine Kameraebene notwendig. Ich führte einige Dienstprogrammmethoden, um den Bildschirm XY-Koordinaten eines Welt XYZ Punkt zu gelangen, da eine Kameraposition:
      vectorWorldToScreenXY: function (Vektor, Kamera) {
         // Vektor angenommen, in Weltkoordinaten xyz sein Kommen in. 
         var widthHalf = window.Galaxy.Settings.width / 2,
             heightHalf = window.Galaxy.Settings.height / 2,
             Projektor = new THREE.Projector ()
             screenPosition;
    
         projector.projectVector (Vektor, Kamera);       
         screenPosition = {
             x: (vector.x * widthHalf) + widthHalf,
             y: - (* vector.y heightHalf) + heightHalf
         };
         zurück screenPosition;
     }
    

Schritt 4: Kinetic.js: Hit-Leistung mit 20.000 Punkten, in Canvas Rendered


  1. KineticJS ist nicht einmal meine Lieblings 2D-Canvas-Bibliothek. Ich wählte es zunächst über die vielen, vielen anderen Leinwand und SVG JavaScript-Optionen (Staffelei, Papier, Weiterverarbeitung, raphael, Stoff, die Liste geht weiter) aus einem einzigen Grund: dieser Demo .
    Instructables Universe in Three.js

    Dies ist nur ein gif. Klicken Sie für die vollständige Demo!
    Kinetic hat für Objekte auf seine Leinwände hinzugefügt eingebauten Ereignis Delegation. Sie melden sich nur ein einziges Ereignis-Listener für die gesamte Leinwand und Kinetic bietet eine einfache Möglichkeit, um die einzelnen Partikel, die angeklickt wurde, abzurufen. Dies macht es extrem schnell (als Leinwand geht jedenfalls), auch mit 20.000 Objekten auf dem Bildschirm auf einmal. Du kannst das:
      stage.on ("Klick", Funktion (e) {
        var node = e.targetNode;
        console.log (Knoten);  // Das einzelne Objekt, auf das geklickt wurde!
     });   
    

    ... Und es wird nur um so schnell log, als ob es nur ein Knoten auf dem Bildschirm.
    Kinetic war toll, uns auf die Version 1.0 von diesem Projekt zu bekommen, aber letztendlich haben wir es ganz verschrottet. Das Problem? Performance. Nicht in der Kassa Ich erwartete eigentlich: es war der glühende und un-Glühen der Sterne, wie sie ausgewählt wurden. Einfach, aber wirkungslos. Es gab noch andere Probleme mit 1.0, die meine waren: Ich habe eine Reihe von "Beziehung" Wörter, die Personen fanden verwirrend, und es gab einige Frage, ob die Galaxie war etwa Instructables überhaupt. Ich hatte Festlegung der Projekte zu viel gespielt.

    Das, und klicken Sie bitte zuerst die Kinetic-Version gedacht, wir brauchten mehr Whizknall. Bewegung, 3D, zu Fuß bis zu dem Bildschirm und stoßen auf sie heftig mit einem Fettfinger, diese Art von Dingen. Dies führte schließlich in die Welt der ThreeJS, die die Anzeige viel mehr Aufmerksamkeit gemacht Grabbing geführt. In vielerlei Hinsicht aber, Version 1 war reiner und für mich besser. Klicken Sie bitte zuerst die Kinetic-Version . Bitte beachten Sie, dass diese Version des Projektes war nie web-optimierten überhaupt. Diese Verbindung schaltet den Debug-Modus, die Ihnen einen kleinen Datensatz und verlässt den Mauszeiger sichtbar; es ist immer noch ein 10 MB herunterladen. Die Touchscreen-Modus ist fast 60 MB, und es gibt keinen loader, so Gewährleistungsausschluss.

Schritt 5: Three.js: Erste Schritte

  1. Instructables Universe in Three.js

    Instructables Universe in Three.js

    Dies ist nur ein gif. Klicken Sie für die vollständige JSFiddle!
    Three.js kann schwierig sein, in die zum ersten Mal zu treten. Für mich war die Verwirrung herauszufinden, alle Stücke, die ich brauchte, um nur die grundlegendsten "Hallo Welt" für ein Beispiel zu montieren. Ich einige gute Tutorials, die mir den Code, um einen Würfel auf dem Bildschirm, rotierenden setzen würde finden konnte, aber ich konnte nicht herausfinden, was alle Teile taten. Ich würde etwas ändern und das ganze Chaos ausbrechen würde.

    Ich hoffe, dass das Diagramm oben löst dieses Problem für einige meiner Leser. Mit threejs Sie jedes Stück in diesem Diagramm benötigen, und Sie brauchen, um sie zu montieren, wie auch für die meisten minimal Beispiel beschrieben loszulegen. Es gibt nicht so etwas wie ein Einzeiler zu "zeichnen einen Würfel". Ich werde durch sie Schritt; Wenn Sie weitere Hilfe benötigen, würde ich beginnen mit Aerotwist Tutorial , das Beste aus die, die ich gefunden. Ebenfalls sehr nützlich, die "Erstellen einer Szene" Seite in den ThreeJS docs .
    Die oben erwähnte ThreeJS Tutorial geht, zu mir, in einer fremden Ordnung. Obwohl sicherlich schwächer Code, um jemanden, der bereits versteht, würde ich die "Animation cube" beispielsweise wie folgt mit Anmerkungen zu versehen (der Code ist der gleiche wie der ThreeJS doc, hier mit einer anderen Erklärung wiederholt):
    Erstellen Sie einen Würfel:
      var Geometrie = new THREE.CubeGeometry (1,1,1);
     var Material = new THREE.MeshBasicMaterial ({color: 0x00FF00});
     var cube = new THREE.Mesh (Geometrie, Material);
    
    Erstellen Sie eine Szene:
      var Szene = new THREE.Scene ();
    
    Fügen Sie den Cube auf die Szene:
      scene.add (Würfel);
    
    Erstellen Sie ein Renderer:
      var Renderer = new THREE.WebGLRenderer ();
     renderer.setSize (window.innerWidth, window.innerHeight);
     document.body.appendChild (renderer.domElement);
    
    Fügen Sie eine Kamera, und Render:
      var Kamera = new THREE.PerspectiveCamera (75, window.innerWidth / window.innerHeight, 0.1, 1000);
     renderer.render (Szene, Kamera);
    

    Schritt zurück für eine Sekunde. Hat es funktioniert? Ok, jetzt zu animieren.
    Fügen Sie einen Rendering Loop, ersetzen renderer.render () mit:
      Funktion render () {
    	 request (übertragen);
    	 cube.rotation.x + = 0,1;
    	 cube.rotation.y + = 0,1;
    	 renderer.render (Szene, Kamera);
     }
     render ();
    

    ... Und glücklich nach Hause gehen!
    request (übertragen) ist ein Abstandsstück für die native Methode von (ungefähr) mit dem gleichen Namen, die Sie von so ähnlich wie JavaScript die setInterval denken konnte (). Außer es gibt viele Vorteile: request wird nicht ausgelöst, wenn die Seite nicht angezeigt wird, zum Beispiel, so dass Sie nicht haben, um Mittel für eine Animation, die nicht gerade betrachtet wird verwenden. Baum im Wald Art von Situation. Es ist auch eine API, die der Browser zu viele Dinge auf einmal neu zu zeichnen (von JS, CSS, WebGL, etc.) ermöglicht, so dass es dann Ihre Neuzeichnen Zyklus zu optimieren. Lesen Sie mehr .

Schritt 6: Three.js: Particle Systems


  1. WebGL und ThreeJS durch Erweiterung, sind groß bei Anzeige von großen und detaillierten Maschen in 3-Raum. Das ist, was viele von 3D-Modellen aus, und selbst wenn Modelle gibt andere Dinge ("Feststoffe" oder "NURBs") sie schließlich auf den Computer-Bildschirm, indem sie zuerst verwandelte sich in sogenannten "Rendernetze" gerendert werden.
    "Punkte" sind nicht wirklich etwas.
    Partikelsysteme sind im Grunde eine Möglichkeit zur Verwaltung von 3-dimensionalen Gittergeometrie, wo man nicht über die Kanten nicht egal. Wenn Sie über Kanten einer Masche zu vergessen, sie unsichtbar machen, und dann geben wir eine Art von punktförmigen Materialqualität an den Schnittpunkten, haben Sie ein Partikelsystem. Partikelsystemen können Sie alle Punkte eines Gitters um unabhängig voneinander bewegen, und kann gut für Effekte, die in der Regel in Form von Teilchen gemacht werden können. Ja, Sie können eine Sanduhr auf diese Weise, wie die einzelnen Partikel von Sand Fluss aneinander vorbei modellieren könnte, aber man könnte auch so etwas wie Wolken , Schnee , ja, Sterne , verrückte Selbstorganisation oder einer heiligen Chaos . Eckpunkte sind mächtig in anderer Hinsicht, zu. Wenn Sie bereit sind, sich einen Vertex-Shader sind, ist es ziemlich schnell zu einem zu schönen Chromkugel .
    Ich eigentlich nicht verwenden Partikelsysteme für viel, aber threejs macht es einfach, Vertex-only Materialien particlesystems anzuwenden. Particle Systems Hand in Hand gehen mit Partikelsystem Materialien. Also statt cubegeometry + Material = Würfel (von oben), können wir sagen particleSystem + particleSystemMaterial = Object3D, und fügen hinzu, dass in der Szene. Weitere Einzelheiten zu dieser Basisversion, empfehle ich Aerotwist Tutorial auf Partikel . Ich landete einen etwas anderen Weg: meine eigene benutzerdefinierte gewalzten Vertex- und Fragment-Shader.
    Die JavaScript ist geradlinig genug:
      var particleGeometry = new THREE.Geometry ();
    
     // Fügen Sie eine Reihe von Eckpunkten der Geometrie
     var Partikel = new THREE.Vector3 (Px, Py, PZ);
     particleGeometry.vertices.push (Partikel);  // Wiederholen für jeden Punkt
    
     var Material = new THREE.ShaderMaterial ({
                     Uniformen: Uniformen,
                     Attribute: Attribute,
    		 Vertexshader: document.getElementById ('Vertexshader') .textContent,
    		 fragmentShader: document.getElementById ('fragmentshader') .textContent,
                     Mischen: THREE.AdditiveBlending,
                     depthTest: false,
                     transparent: true
                 })
    
     var System = new THREE.ParticleSystem (
                     particleGeometry,
                     Material
                  );
    
     // Szene an anderer Stelle definiert
     scene.add (System);
    

    Aber die "Vertexshader" und "fragmentshader" genannten Elemente sind WebGL-Code von einem fremden Planeten:
      <Script type = "x-shader / x-Vertex" id = "Vertexshader">
         Attribut float alpha;
         Attribut float Größe;
         Attribut vec3 ca;
         Variieren vec3 vColor;
         Variieren float Valpha;
    
         void main () {
             vColor = ca;
             Valpha = alpha;
             vec4 mvPosition = modelViewMatrix * vec4 (Position, 1,0);
    
             gl_PointSize = size * (1.0 + 300,0 / Länge (mvPosition.xyz));
             gl_Position = projectionMatrix * mvPosition;
         }
     </ Script>
    
     <Script type = "x-shader / x-Fragment" id = "fragmentshader">
         einheitliche vec3 Farbe;
         einheitliche sampler2D Textur;
         Variieren vec3 vColor;
         Variieren float Valpha;
    
         void main () {
             gl_FragColor = vec4 (vColor, Valpha);
             gl_FragColor = Valpha * Texture2D (Textur, gl_PointCoord);
         }
     </ Script>
    

    Schreiben dieser Shader-Code war schmerzhaft für mich, weil ich nicht wusste, WebGL und immer noch nicht. Was ich auf dem Weg zu lernen, neben einigen Kleinigkeiten, die nicht sinnvoll, zu teilen, war eine interessante Tatsache. Ich wusste nie, warum eine GPU war nützlich. Sicher, es ist gut, einen zweiten Prozessor haben. Aber warum nicht zwei CPUs? Nur weil ein GPU ist billiger?
    Der Unterschied ist die Parallelverarbeitung. Für die Architektur einer CPU, jeden Pixel auf dem Bildschirm zu malen, im Grunde muss es Code, der jedes Pixel in Folge tut folgen:
      für (jede Zeile) {
       für (jede Spalte) {
          etwas tun, um Pixel bei (Zeile, Spalte);
       }
     }
    

    Die GPU-Code sieht anders aus, da Prozess Pixel GPUs parallel, nicht Folge. Sie einen Effekt auf den gesamten Bildschirm auf einmal, und Ebeneneffekte auf einander. Dadurch können Sie einige wirklich spannende Sachen mit sehr wenig Code. Siehe Nachbearbeitung später für einen Geschmack.

Schritt 7: Three.js: Particle Systems in Motion


  1. Die Sterne könnte auf verschiedene Weise animiert werden:
    -. Jeder Knoten kann jeder Rahmen bewegt werden Dies bietet enorme Flexibilität (jeden Punkt überall zu jeder Zeit), und ist, wie die meisten der Feuerwerk / Brunnen / Schnee partikel WebGL Beispiele zu arbeiten. Aber mit einer großen Anzahl von Punkten, wird die Leistung ein Anliegen, da die JavaScript muss Schleife über jedes Teilchen, jeden Rahmen. Wenn Sie 20.000 Punkte mit 3 Dimensionen verändert sich jeden Rahmen und Sie einen seidenweichen 60fps Framerate beibehalten wollen, ist, dass 3,6 Millionen Rechenoperationen pro Sekunde. Es ist wahrscheinlich, ein wenig rutschen.
    -. Verwenden Vertex-Shader, um die Ecken direkt in WebGL verdrängen Dies ist wahrscheinlich die beste Lösung für die Leistung (die JavaScript nichts tut jedes Bild, und alle von der Animation ist direkt in WebGL). Sie tun die Verschiebung und Lärmberechnungen direkt auf der GPU, so dass die CPU ganz frei für andere Aufgaben, wie Benutzerinteraktion. Hier ist eine hervorragende Demo und Lernprogramm für diese Art von Trick. Sie sollten Besuche die Tornados zu, die auch verwendet eine ähnliche Strategie. Obwohl cool aussehende, das macht es schwierig, Dinge wie ein Benutzer oder tippen Sie auf einen Stern zu behandeln. Soweit JavaScript betroffen ist, werden die Scheitel fixiert. Kartierung der WebGL Lage zurück zu einem JavaScript-Objekt entweder über mich oder praktisch nicht möglich. Ich brauchte, um in der Lage, Sterne im Weltall basierend auf Benutzerinteraktion zu lokalisieren, so dass diese Option war out.
    - Gruppe die Punkte in Objekte und beleben jedes Objekt unabhängig endete Dies up ist meine Lösung.. Die sechs Ringe für sechs Top-Level-Kategorien Instructables "sind sechs unabhängige Partikelsysteme in ThreeJS. Um eine Illusion, dass die Galaxy ist ständig in Bewegung zu erzeugen, drehen I jedem der particleSystems mit unterschiedlichen Geschwindigkeiten, in verschiedene Richtungen, und um verschiedene Mittelpunkte. Dies benötigt Javascript für eine neue Rotation für sechs Objekte jeder Rahmen zu berechnen, aber die große Mehrheit der Arbeit basiert auf der GPU, die jeden Scheitelpunkt zu einem Punkt auf dem Bildschirm Karten getan. Da ein JavaScript wird auf jeden Punkt erhalten, ist es möglich, herauszufinden, welche Stelle ein Benutzer tippen, wenn sie auf den Bildschirm tippen.
    Jeder Rahmen, in diesem Fall:
      // Animationsschleife
     Funktion update () {
         // Anmerkung: Three.js umfasst request Shim
         request (Update);
    
         // Sachen zu bewegen, wie es sein muss:
         if (interactionHandler.frozen === false) {
             particleSystemsArray [0] .rotation.z - = 0,00008;
             particleSystemsArray [1] .rotation.z + = 0,00002;
             particleSystemsArray [2] .rotation.z + = 0,00012;
             particleSystemsArray [3] .rotation.z - = 0,00009;
             particleSystemsArray [4] .rotation.z + = 0,00016;
             particleSystemsArray [5] .rotation.z - = 0,00005;
             sky.rotation.z + = 0,00015;  // Drehen Sie das Hintergrundbild auch!
         }
    
        // The little tags that travel with stars need to have updated positions
        interactionHandler.getTagManager().updateActiveTagPositions();
    
        // draw. I'll explain this code later, but you can think of it
        // for now as renderer.render()
        _.each(Galaxy.Composers,function(composer){
            composer.render();
         });
     }
    

Step 8: Three.js: A Different Brightness for Each Star: WebGL Shaders

  1. html5rocks.com
    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Instructables Universe in Three.js

    Stars aren't uniform. They're different sizes, different brightnesses, and different colors. My trick with having six objects from before didn't seem to apply here: I really wanted each star to be unique, and to reflect the relative importance of the project. Basically, the projects that are the "brightest stars" should shine. Making each vertex unique is a perfect use for WebGL shaders. Recall the shader code from before:
     <script type="x-shader/x-vertex" id="vertexshader">
        attribute float alpha;
        attribute float size;
        attribute vec3 ca;
        varying vec3 vColor;
        varying float vAlpha;
    
        void main() {
            vColor = ca;
            vAlpha = alpha;
            vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
    
            gl_PointSize = size * (1.0+ 300.0 / length( mvPosition.xyz ) );
            gl_Position = projectionMatrix * mvPosition;
         }
    </script>
    
    <script type="x-shader/x-fragment" id="fragmentshader">
        uniform vec3 color;
        uniform sampler2D texture;
        varying vec3 vColor;
        varying float vAlpha;
    
        void main() {
            gl_FragColor = vec4( vColor, vAlpha );
            gl_FragColor = vAlpha * texture2D( texture, gl_PointCoord );
         }
    </script>
    

    These shaders work hand-in-hand with this ThreeJS code:
     Galaxy.Utilities.projectData = parsedData;
    Galaxy.Datasource = parsedData;
    var instructableIds = _.keys(parsedData);
    
    // create geometries for each of the six rings, so the particle systems can move independently
    var particleGeometries = [];
    _.each(window.Galaxy.Settings.categories,function(){
        particleGeometries.push(new THREE.Geometry());
    
        attributes = {
            size: { type: 'f', value: [] },
            ca: { type: 'c', value: [] },
            alpha: { type: 'f', value: [] }
        };
    
        uniforms = {
            color: { type: "c", value: new THREE.Color( 0xffffff ) },
            texture: { type: "t", value: THREE.ImageUtils.loadTexture("images/particle4B.png")}
        };
    
        shaderMaterialsArray.push(new THREE.ShaderMaterial( {
            uniforms: uniforms,
            attributes: attributes,
            vertexShader: document.getElementById( 'vertexshader' ).textContent,
            fragmentShader: document.getElementById( 'fragmentshader' ).textContent,
            blending: THREE.AdditiveBlending,
            depthTest: false,
            transparent: true
        }));
     });
    
    _.each(instructableIds,function(id){
        var pX = parsedData[id].x - window.Galaxy.Settings.width/2,
                pY = parsedData[id].y - window.Galaxy.Settings.height/2,
                pZ = Galaxy.Utilities.random(0,10),
                particle = new THREE.Vector3(pX, pY, pZ);
    
        // add each particle to the correct geometry (ring) so it will end up in an associated particle system later
        var ring = indexForCategory(parsedData[id].category);
        if (ring !== -1) {
            particleGeometries[ring].vertices.push(particle);
    
            var appearance = Galaxy.Utilities.vertexAppearanceByViews(parsedData[id].views);
            shaderMaterialsArray[ring].attributes.size.value.push(appearance.size);
            shaderMaterialsArray[ring].attributes.ca.value.push(appearance.ca);
            shaderMaterialsArray[ring].attributes.alpha.value.push(appearance.alpha);
    
            // we need to keep references both directions. User clicks particle, we need to look up details by id
            // Also, if we want to highlight related instructables, we'll need fast easy access to vertices with referenced ids.
            particle.instructableId = id;
            parsedData[id].particleMaterial = shaderMaterialsArray[ring];
            parsedData[id].vertexNumber = particleGeometries[ring].vertices.length-1;
         }
     });
    
    // main scene, for all regular galaxy appearances
    var scene = new THREE.Scene();
    Galaxy.Scene = scene;
    
    // create the particle system
    _.each(particleGeometries,function(particleGeometry, index){
        particleGeometry.applyMatrix( new THREE.Matrix4().makeTranslation( Math.random()*50, Math.random()*50, 0 ) );
        var system = new THREE.ParticleSystem(
                particleGeometry,
                shaderMaterialsArray[index]
            );
        particleSystemsArray.push(system);
        scene.add(system);
     });
    

    Here's what's happening in plain English:

    1. I set up a THREE.ShaderMaterial for each THREE.ParticleSystem. Recall from the last step that there are six ParticleSystems, one for each category of Instructables, and that each ParticleSystem needs two things to be instantiated: a Geometry and a Material. (see ThreeJS: Getting Started above)
    2. Each THREE.ShaderMaterial is essentially the same at this point: they're set up to use the fragment and vertex shaders loaded in the script tags above. They include uniforms and attributes passed in from the JavaScript. These are two of the three types of variables you can send to WebGL. As well explained on html5rocks.com :
    - Uniforms don't change within a given frame. They are sent to both fragment and vertex shaders. In this case, the color and texture image (the nice glowing star-like image) is the same for each star.
    - Attributes apply to individual vertices. They are sent to vertex shaders only. In this case, the attributes that can vary by star are color, size, and alpha.
    - Varyings allow the vertex shaders to pass values into the fragment shader.
    3. With the empty THREE.ShaderMaterials defined, my next block steps over every Instructable that appears on screen. It passes the Instructable to a helper function that determines how the star should appear, based on the number of views for that Instructable:
     vertexAppearanceByViews : function(viewcount){
        var prominence = (Math.pow(Math.log(viewcount),3))/(1000);
        return {
            size: prominence*9,
            ca: Galaxy.Utilities.whiteColor,
            alpha: Math.min(0.15 + 0.3*prominence,0.9)
         }
     }
    

    In the final version of the code, I don't actually vary the color of the stars! Alpha does that for me, since each star appears on top of a colored background.
    4. Inside this loop, each Particle is pushed into an array of vertices in the Geometry:
     var particle = new THREE.Vector3(pX, pY, pZ);
     ...
    particleGeometries[ring].vertices.push(particle);
    

    and a corresponding set of "attributes" values is pushed into the corresponding ShaderMaterial:
     var appearance = Galaxy.Utilities.vertexAppearanceByViews(parsedData[id].views);
    shaderMaterialsArray[ring].attributes.size.value.push(appearance.size);
    shaderMaterialsArray[ring].attributes.ca.value.push(appearance.ca);
    shaderMaterialsArray[ring].attributes.alpha.value.push(appearance.alpha);
    

    Finally, with an array of particle geometries and corresponding array of particle materials, the two are merged into a single array of ParticleSystems, and each ParticleSystem is added to the scene:
     // for each item in the particleGeometries array:
    var system = new THREE.ParticleSystem(
        particleGeometry,
        shaderMaterialsArray[index]
    );
     ...
    scene.add(system);
    

Step 9: Three.js: Hit-Performance with 20,000 Points in 3D


  1. ThreeJS has gotten us a long ways. Instead of 20,000 flat, unmoving points, we now have 20,000 points in three-space, spinning around uneven centers and with the ultimate promise that we can animate the camera, too, and really explore inside the Galaxy.
    But when you tap a star, how fast can ThreeJS locate it? Pretty fast it turns out. As of this writing, ThreeJS doesn't support raycaster intersections for ParticleSystems (womp womp) but fortunately someone else figured this out. There are lots of forks you can use, or just put the code in the library yourself and rebuild a custom copy. This is what you'd add to Raycaster.js (adapted from similar code I found all over, like here ):
      ...
    } else if (object instanceof THREE.ParticleSystem) {
        //See: <a href="https://github.com/mrdoob/three.js/issues/3492"> <a href="https://github.com/mrdoob/three.js/issues/3492"> https://github.com/mrdoob/three.js/issues/3492
    </a>
    </a>
    
        var vertices = object.geometry.vertices;
        var point, distance, intersect, threshold = 3;
        var localMatrix = new THREE.Matrix4();
        var localtempRay = raycaster.ray.clone();
        var localOrigin = localtempRay.origin;
        var localDirection = localtempRay.direction;
    
        localMatrix.getInverse(object.matrixWorld);
        localOrigin.applyMatrix4(localMatrix);
        localDirection.transformDirection(localMatrix);
    
        for ( var i = 0; i < vertices.length; i ++ ) {
            point = vertices[ i ];
    
            distance = localtempRay.distanceToPoint(point);
            if ( distance > threshold ) {
                continue;
             }
    
            intersect = {
                distance: distance,
                point: point,
                face: null,
                object: object,
                vertex: i
            };
            intersects.push(intersect);
         }
    } else if ...
    

    Once the library is built appropriately (I'll leave this as an exercise for the reader), getting the intersection from a user's tap or mouse-click is pretty straightforward: it's a raycaster using the current camera's position, the scene, and the (x,y) coordinates of the user's click. Psuedo code :
     -Get (x,y) of user's click
    -Turn this into a THREE.Vector3()
    -un-project the vector based on the camera position
    -Set up a raycaster with the camera position and unprojected vector
    -run ray.intersectObjects()
    -do something with the results 

    The reality of the code for this project, with its multiple particlesystems and the desire to find the closest intersection amongst intersections from all of the particlesystems is somewhat more complex. Plus, as well explained by Jens Arps, there's some mystery meat when turning a screen point into a 3d vector (you see this in my code too: new THREE.Vector3( ( e.clientX / Galaxy.Settings.width ) * 2 - 1, - ( e.clientY / Galaxy.Settings.height ) * 2 + 1, 0.5 ) ):
     canvasClickEvent: function(e){
        e.preventDefault();
        e.stopPropagation();
        this.resetInteractionTimer(); // stops "auto mode" from resuming for 90s
    
        var vector = new THREE.Vector3( ( e.clientX / Galaxy.Settings.width ) * 2 - 1, - ( e.clientY / Galaxy.Settings.height ) * 2 + 1, 0.5 );
        var projector = new THREE.Projector();
        projector.unprojectVector( vector, this.camera );
    
        var ray = new THREE.Raycaster( this.camera.position, vector.sub( this.camera.position ).normalize() );
    
        // If there are already selected stars out in the field, ie, from an author constellation or related group,
        // we assume the user is trying to select one of those. However, if each of these systems contains
        // only a single vertex, that indicates the user may just be clicking around individually. So don't use pre-selected
        // stars for the intersection in that case.
        var intersectSystems = this.particleSystemsArray,
            that = this;
        if (!_.isUndefined(this.__glowingParticleSystems)) {
            _.each(this.__glowingParticleSystems,function(system){
                if (system.geometry.vertices.length !== 1) {
                    // intersect with the glowing systems instead
                    intersectSystems = that.__glowingParticleSystems;
                 }
             });
         }
    
        // When the camera is very close to the star that's selected, distance is deceiving. We basically need to adjust hit tolerance based on the distance to camera
        // Calculate the distance camera --> star by converting star's position to world coords, then measuring
        // intersection.point = Vector3
        // intersection.object = ParticleSystem it's a part of
        var getCameraDistanceForHit = function(intersection){
            var intersectionVect = intersection.point.clone();
            intersectionVect = intersection.object.localToWorld(intersectionVect);
            return intersectionVect.distanceTo(that.camera.position.clone());
        };
    
        // intersects sorted by distance so the first item is the "best fit"
        var intersects = _.sortBy(ray.intersectObjects( intersectSystems, true ),function(intersection){
            return getCameraDistanceForHit(intersection) / intersection.distance;
         });
    
        // When a hit is too close to the camera for its hit tolerance, it doesn't count. Remove those values.
        intersects = _.filter(intersects, function(intersection){
            return getCameraDistanceForHit(intersection) / intersection.distance > 100;
         });
    
        if ( intersects.length > 0 ) {
            this.selectVertex(intersects[0])
         } Else {
            // no intersections are within tolerance.
            this.reset({projectTagsAddAfterCameraReset: true});
         }
    },
    

    And it turns out to be pretty snappy!

Step 10: TweenMax: Easy Animation (Camera Motion)


  1. TweenMax is dead simple. Put the library in your project, then pass in an object with numerical values (start and later finish), give it a duration and any options you like (easing, callbacks, etc), and watch your thing animate nicely. My main use for this is actually part of the next step (camera motions), but it removes all the pain from calculating all of the midpoints along an animation path. It also lets you stop, reverse, repeat, etc. Reversing an animation that's already been started and has gone halfway is not a small bit of code.
    For my camera motions, the animation was the easy part, even though it requires me to separately animate the camera position, target, and up vector. Here's a random animation:
     TweenMax.to(upCurrent,duration/1.5,{x: upGoal.x,y: upGoal.y,z: upGoal.z});
    TweenMax.to(targetCurrent,duration/1.5,{x: center.x,y: center.y, z: center.z});
    TweenMax.to(positionCurrent,duration,{x: home.x,y: home.y, z: home.z,ease: Power1.easeInOut,
        onUpdate: function(){
    	// every frame, update the camera with all of the current values IN THIS ORDER!
            that.target = new THREE.Vector3(targetCurrent.x,targetCurrent.y,targetCurrent.z);
            that.camera.position.set(positionCurrent.x,positionCurrent.y,positionCurrent.z);
            that.camera.up.set( upCurrent.x,upCurrent.y,upCurrent.z );
            that.camera.lookAt(that.target.clone());
            that.camera.updateProjectionMatrix();
        },
        onComplete: function(){
    	// when the animation finishes
            that.endAnimation();
            that.firstClick = true;
            if (typeof callback === "function") callback();
        },
        onStart: that.startAnimation
    })
    

    This can look a little confusing since there are a lot of things happening, but it's simple. There's a variable holding each of these things:
    Camera position (the value that tweens)
    Camera position (the end point)
    Up vector (the value that tweens)
    Up vector (the destination orientation)
    Target (the value that tweens)
    Target (what we'd like to end up looking at)
    In addition, in the same scope, it's important to .clone() the THREE.Vector3's representing start position, up, and target, so that the actual values aren't changed by calculations during the animation.
    With the exception of the two different durations, all of this animation code could easily be turned into a single TweenMax() call simply by combining the objects into one. For clarity and modularity, I left it separate

Step 11: Three.js: Camera Positioning


  1. Camera control is probably the hardest part of working with ThreeJS. I strongly encourage everyone not to bother with this exercise in frustration. Pick one of the example generic camera controllers , insert the script on your page, and enjoy. If you do need to calculate custom camera parameters, you're probably best off learning about Quaternions, which I did not.
    Going the plain-Jane trig route leaves you in a world of pain. You have essentially no useful tools to debug your math, and even once your math is right the results can look wrong because of things like the "Euler Order" (which you should set to YXZ if you want the camera's controls to feel like Pitch, Roll, and Yaw... just set camera.rotation.order = "YXZ";). The camera's rotations are in the camera's own coordinate system, so you always have to remember to give it instructions that way or use the hacky "target" and "lookAt" strategy (which I did) which will invariably lead you to totally screwy orientations.... that's when you start having to set the camera's "up" vector manually to keep it facing up, and you always have to be on the lookout for things like gimbal lock (where you lose a degree of freedom because of two parallel axes in the camera) and the fact that tweening the *values* of the camera's rotation vectors may take you correctly from A-->B in space, but along the wrong rotational direction. You may think it's natural to turn 180 degrees by turning your head sideways, but the simplest rotational path may well be the one that goes straight overhead instead. Blech!
    I'm not going to take you through the solutions to all of those issues, and if you look closely you'll find that my solutions are still, in some places, a little rough. Instead, I'm just going to paste all of the camera motion code along with little notes here about what parts of it do. If you're really diving into a piece and you'd like more explanation, go ahead and post a comment so I can flesh out that part.
    Camera Criteria: Highlights
    - Putting a selected star in the center of the screen (zoomAndDollyToPoint)
    - Putting a selected star in the center of the screen with the center of the galaxy in the background to prevent browsing off the edge of the universe (zoomToFitPointsFrom, CAMERA_RELATION.TOWARD_CENTER)
    - Moving from one selected star to another without changing the camera's angle (strafeFromPointToPoint)
    - Finding the bounding sphere for a group of stars, and then finding a camera position such that those stars would fit on the screen (zoomToFitPointsFrom, all CAMERA_RELATION s)
    - Put adjacent stars in the same cluster in comfortable screen positions relative to a single "currently selected" star in an author's constellation (showThreePointsNicely)
    - Return to a "home" position (reset)
    - Parameterizing a path for the camera so that it can slowly fly through the galaxy on its own when not attended (wait 90s without clicking to see the animation start) -- (beginAutomaticTravel and cameraSetupForParameter)
    The Code:
     Galaxy.Settings = Galaxy.Settings || {};
    
    Galaxy.CameraMotions = function(camera){
        _.bindAll(this,'zoomToFitPointsFrom','startAnimation','endAnimation','cameraSetupForParameter','beginAutomaticTravel');
        this.target = Galaxy.Settings.cameraDefaultTarget.clone();
        this.camera = camera;
    
        // delete this property eventually
        this.firstClick = true;
    
        this.isAnimating = false;
     }
    
    Galaxy.CameraMotions.prototype = {
        constructor: Galaxy.CameraMotions,
        startAnimation: function(){
            // startAnimation refers to user-initiated animations. The default animation must be removed if ongoing.
            this.endAutomaticTravel();
            this.isAnimating = true;
        },
        endAnimation: function(){this.isAnimating = false;},
        zoomAndDollyToPoint: function(point,callback){
            if (this.isAnimating === true) return;
            // temporarily: the first click will zoom in, and we'll strafe after that.
            if (this.firstClick === false) {
                //this.strafeFromPointToPoint(this.target,point,callback);
                this.zoomToFitPointsFrom([point],this.CAMERA_RELATION.TOWARD_CENTER,callback);
                 zurück;
             }
            this.firstClick = false;
    
            var that = this,
                pointClone = point.clone(),
                cameraPath = this.cameraPathToPoint(this.camera.position.clone(), point.clone()),
                currentPosition = {now: 0},
                duration = 1.3,
                upClone = Galaxy.Settings.cameraDefaultUp.clone(),
                targetCurrent = this.target.clone();
            TweenMax.to(targetCurrent,duration/1.5,{
                x:pointClone.x,
                y:pointClone.y,
                z:pointClone.z
             });
            TweenMax.to(currentPosition,duration,{
                now:0.8,
                onUpdate: function(){
                    var pos = cameraPath.getPoint(currentPosition.now);
                    that.target = new THREE.Vector3(targetCurrent.x,targetCurrent.y,targetCurrent.z);
                    that.camera.position.set(pos.x,pos.y,pos.z);
                    that.camera.up.set(upClone.x,upClone.y,upClone.z);
                    that.camera.lookAt(that.target);
                    that.camera.updateProjectionMatrix();
                },
                onStart: that.startAnimation,
                onComplete: function(){
                    that.endAnimation();
                    if (typeof callback === "function") callback();
                 }
             });
        },
    
        cameraPathToPoint: function(fromPoint,toPoint){
            var spline = new THREE.SplineCurve3([
               fromPoint,
               new THREE.Vector3( (toPoint.x-fromPoint.x)*0.5 + fromPoint.x, (toPoint.y-fromPoint.y)*0.5 + fromPoint.y, (toPoint.z-fromPoint.z)*0.7 + fromPoint.z),
               toPoint
            ]);
    
            return spline;
        },
    
        strafeFromPointToPoint: function(fromPoint,toPoint,callback){
            var dest = toPoint.clone(),
                current = this.camera.position.clone(),
                duration = 0.5,
                that = this;
            dest.sub(fromPoint.clone());
            dest.add(current.clone());
            //console.log("\n\n",fromPoint,toPoint,current,dest);
    
            if (that.isAnimating === true) return;
    
            TweenMax.to(this.camera.position,duration,{x: dest.x,y: dest.y, z: dest.z,
                onComplete: function(){
                    that.endAnimation();
                    that.camera.lookAt(toPoint.clone());
                    that.target = toPoint.clone();
                    if (typeof callback === "function") callback();
                },
                onStart: that.startAnimation
            })
        },
    
        reset: function(callback){
            var duration = 2,
                that = this,
                home = Galaxy.Settings.cameraDefaultPosition.clone(),
                center = Galaxy.Settings.cameraDefaultTarget.clone(),
                upGoal = Galaxy.Settings.cameraDefaultUp.clone(),
                upCurrent = this.camera.up.clone(),
                targetCurrent = this.target.clone(),
                positionCurrent = this.camera.position.clone();
    
            // never do anything when nothing will suffice. The callback should have no delay.
            if (this.camera.up.equals(Galaxy.Settings.cameraDefaultUp) &&
                this.camera.position.equals(Galaxy.Settings.cameraDefaultPosition) &&
                this.target.equals(Galaxy.Settings.cameraDefaultTarget)) {
    
                duration = 0.1;
             }
            if (that.isAnimating === true) return;
    
            TweenMax.to(upCurrent,duration/1.5,{x: upGoal.x,y: upGoal.y,z: upGoal.z});
            TweenMax.to(targetCurrent,duration/1.5,{x: center.x,y: center.y, z: center.z});
            TweenMax.to(positionCurrent,duration,{x: home.x,y: home.y, z: home.z,ease: Power1.easeInOut,
                onUpdate: function(){
                    that.target = new THREE.Vector3(targetCurrent.x,targetCurrent.y,targetCurrent.z);
                    that.camera.position.set(positionCurrent.x,positionCurrent.y,positionCurrent.z);
                    that.camera.up.set( upCurrent.x,upCurrent.y,upCurrent.z );
                    that.camera.lookAt(that.target.clone());
                    that.camera.updateProjectionMatrix();
                },
                onComplete: function(){
                    that.endAnimation();
                    that.firstClick = true;
                    if (typeof callback === "function") callback();
                },
                onStart: that.startAnimation
            })
        },
    
        CAMERA_RELATION : {
            ABOVE: 0,
            SAME_ANGLE: 1,
            TOWARD_CENTER: 2
        },
    
        zoomToFitPointsFrom: function(pointList,cameraRelation,callback) {
            if (!_.has(_.values(this.CAMERA_RELATION),cameraRelation)) {
                // console.log(_.values(this.CAMERA_RELATION));
                console.error(cameraRelation + " is not one of RELATIVE_LOCATION");
                 zurück;
             }
            if (this.isAnimating === true) return;
    
            // pointList assumed to already be in world coordinates. Figure out bounding sphere, then move camera relative to its center
            var bSphere = new THREE.Sphere(new THREE.Vector3(0,0,0),5);
            bSphere.setFromPoints(pointList);
    
            // how far away do we need to be to fit this sphere?
            var targetDistance = (bSphere.radius / (Math.tan(Math.PI*this.camera.fov/360))),
                cameraPositionEnd,
                that = this,
                duration = 1,
                up = this.camera.up.clone(),
                currentCameraPosition = this.camera.position.clone();
    
            switch (cameraRelation) {
                case 0:
                    // CAMERA_RELATION.ABOVE
                    cameraPositionEnd = bSphere.center.clone().add(new THREE.Vector3(40,40,targetDistance));
                     brechen;
    
                case 1:
                    // CAMERA_RELATION.SAME_ANGLE dollies the camera in/out such that these points become visible
                    var center = bSphere.center.clone(),
                        currentPos = that.camera.position.clone(),
                        finalViewAngle = currentPos.sub(center).setLength(targetDistance);
                    cameraPositionEnd = bSphere.center.clone().add(finalViewAngle);
    
                    // to prevent camera from going under the background plane:
                    cameraPositionEnd.z = Math.max(cameraPositionEnd.z,40);
                     brechen;
    
                case 2:
                    // CAMERA_RELATION.TOWARD_CENTER Draws a line from world origin through the bounding sphere's center point,
                    // and puts the camera at the end of a vector twice that length.
                    cameraPositionEnd = bSphere.center.clone().multiplyScalar(2);
                    if (cameraPositionEnd.length() < 125) cameraPositionEnd.setLength(125); // It's weird when the camera gets too close to stars in the middle
                     brechen;
    
             }
            var cameraTargetCurrent = {x: this.target.x, y: this.target.y, z: this.target.z};
            var cameraTargetEnd = bSphere.center.clone();
    
    // that.logVec('up',that.camera.up.clone());
    // that.logVec('target',that.target.clone());
    // that.logVec('position',that.camera.position.clone());
            TweenMax.to(cameraTargetCurrent,duration/1.5,{x: cameraTargetEnd.x,y: cameraTargetEnd.y, z: cameraTargetEnd.z});
    
            // DO NOT change "up" for high angle. It gets screwy and spins the camera unpleasantly.
            if (cameraRelation !== 0) {TweenMax.to(up,duration/1.5,{x: 0,y: 0, z: 1});}
    
            TweenMax.to(currentCameraPosition,duration,{x: cameraPositionEnd.x,y: cameraPositionEnd.y, z: cameraPositionEnd.z,
                onUpdate: function(){
                    that.target = new THREE.Vector3(cameraTargetCurrent.x,cameraTargetCurrent.y,cameraTargetCurrent.z);
                    that.camera.position.set(currentCameraPosition.x,currentCameraPosition.y,currentCameraPosition.z);
                    that.camera.up.set( up.x,up.y,up.z );
                    that.camera.lookAt(that.target.clone());
                    that.camera.updateProjectionMatrix();
                },
                onComplete: function(){
    // that.logVec('up',that.camera.up.clone());
    // that.logVec('target',that.target.clone());
    // that.logVec('position',that.camera.position.clone());
                    that.endAnimation();
                    if (typeof callback === "function") callback();
                },
                onStart: that.startAnimation
            })
        },
    
        showThreePointsNicely: function(pointList, callback){
            // Find a camera location and rotation such that the first point appears towards the bottom of the screen, and
            // the other two appear up and to the left and right. Or so.
            if (this.isAnimating === true) return;
            this.firstClick = false;
            if (!_.isArray(pointList)) {
                 throw new Error ("Array of points required for showThreePointsNicely");
            } else if (pointList.length !== 3) {
                // just show the first one.
                this.zoomAndDollyToPoint(pointList[0],callback);
                 zurück;
             }
    
            var pointZero = pointList[0].clone();
    
            // look at the world from the perspective of the star that will be centered:
            var viewFromPointZero = function(vector){
                return vector.clone().sub(pointZero.clone());
            };
    
            // The "bisect" vector is a central angle between the Left and Right stars that we're trying to make visible on screen, along with vector Zero
            var bisectLocal = viewFromPointZero( pointList[1]).add(viewFromPointZero( pointList[2])).multiplyScalar(0.5);
    
            // The linear path would be described as....
            var A = viewFromPointZero(pointList[1]);
            var B = viewFromPointZero(pointList[2]);
            var theta = Math.acos(A.clone().dot(B.clone()) / A.length() / B.length() );
            var distanceAwayBasedOnAngle = Math.min(Math.max(theta*2.5,2),4);
    
            var cameraEndPosition = pointZero.clone().sub(bisectLocal.clone().multiplyScalar(distanceAwayBasedOnAngle));
            var cameraStartPosition = this.camera.position.clone();
            var cameraPathMidpoint = cameraEndPosition.clone().add(cameraStartPosition.clone()).multiplyScalar(0.5);
    
            // The circular path around the linear path's midpoint would be, then:
            var radius = cameraStartPosition.clone().sub(cameraPathMidpoint.clone()).length();
    
            var that = this;
            var worldPointOnCircularPath = function(t){
                var x = radius * Math.cos(t);
                var y = radius * Math.sin(t);
                var vectorPointLocal = new THREE.Vector3(x,y,0);
                return vectorPointLocal.add(cameraPathMidpoint.clone());
            };
    
            // backsolve the start angle for the circular path. It's the inverse of x=a+r*cos(theta) => theta = acos((xa)/r);
            var startPointRelativeToMidpoint = cameraStartPosition.clone().sub(cameraPathMidpoint.clone());
            var startAngle = Math.atan(startPointRelativeToMidpoint.y/startPointRelativeToMidpoint.x);
    
            // Is this the start angle or the end angle? It's one or the other, but we need to know which.... The other will be this + PI
            if (worldPointOnCircularPath(startAngle).setZ(0).distanceTo(cameraStartPosition.clone().setZ(0)) > 100) {
                // gotta start halfway around instead. such is the world of inverse trig functions
                startAngle += Math.PI;
             }
    
            var parameters = {
                    t: startAngle,
                    z: cameraStartPosition.clone().z
                },
                duration = 2.0,
                up = that.camera.up.clone();
    
            var pointZeroClone = pointZero.clone();
            TweenMax.to(that.target,duration/1.5,{x: pointZeroClone.x,y: pointZeroClone.y,z: pointZeroClone.z});
            TweenMax.to(up,duration/1.5,{x: 0,y: 0, z: 1});
    
            TweenMax.to(parameters,duration,{
                t: startAngle - Math.PI,
                z: cameraEndPosition.clone().z,
                ease: Power1.easeOut,
                onUpdate: function(){
                    var xyCurrent = worldPointOnCircularPath(parameters.t);
                    that.camera.position.set(xyCurrent.x,xyCurrent.y,parameters.z);
                    that.camera.up.set( up.x,up.y,up.z );
                    that.camera.lookAt(that.target);
                    that.camera.updateProjectionMatrix();
                },
                onComplete: function(){
                    that.endAnimation();
                    if (typeof callback === "function") callback();
                },
                onStart: that.startAnimation
             });
        },
    
    
        // The camera can also "travel" while in unattended mode. This behavior requires some code to parametrically define and then animate the complex path,
        // But it is somewhat different in kind from the user-initiated camera motions described above.
        beginAutomaticTravel: function(){
            // This function absolutely positively must begin from the camera home positions.
            // console.log('commencing automatic camera travel');
            var obj = {cameraParameter: Math.PI/2},
                that=this,
                loopConstants = this.loopConstants();
            this.reset(function(){
                that.__automaticCameraAnimation = TweenMax.to(obj, loopConstants.duration, {
                    cameraParameter: 5*Math.PI/2,
                    onUpdate:function(){
                        that.cameraSetupForParameter(obj.cameraParameter,loopConstants);
                    },
                    ease: null,
                    repeat: -1 // loop infinitely
                 });
             });
        },
        loopConstants: function(){
            var galaxyLoopStart = new THREE.Vector3(200,0,15),
                targetLoopStart = new THREE.Vector3(200,200,5),
                upLoopStart = new THREE.Vector3(0,0,1);
            return {
                duration : 400, // seconds
                galaxyLoopStart : galaxyLoopStart,
                targetLoopStart: targetLoopStart,
                upLoopStart: upLoopStart,
                galaxyLoopToHome : Galaxy.Settings.cameraDefaultPosition.clone().sub(galaxyLoopStart.clone()),
                targetLoopToHome : Galaxy.Settings.cameraDefaultTarget.clone().sub(targetLoopStart.clone()),
                upLoopToHome : Galaxy.Settings.cameraDefaultUp.clone().sub(upLoopStart.clone())
             }
        },
        cameraSetupForParameter: function(cameraParameter,loopConstants){
            var pos,lookAt = loopConstants.targetLoopStart.clone(),up = loopConstants.upLoopStart.clone();
            if (cameraParameter < 2*Math.PI && cameraParameter > Math.PI) {
                cameraParameter -= Math.PI;
                // go a full circle around the galaxy
                pos = new THREE.Vector3(200*Math.cos(cameraParameter),200*Math.sin(cameraParameter),15);
                var copy = pos.clone();
                lookAt = new THREE.Vector3(copy.x ,copy.y + copy.x,5);
             } Else {
                // after going a full circle around the galaxy, animate all camera characteristics to the "home" position, then repeat from start
                var pathMultiplier = Math.sin(cameraParameter ); // good from 0 to PI. Outside that range, this goes negative and looks haywire.
                pos = loopConstants.galaxyLoopStart.clone().add(loopConstants.galaxyLoopToHome.clone().multiplyScalar(pathMultiplier));
                lookAt = loopConstants.targetLoopStart.clone().add(loopConstants.targetLoopToHome.clone().multiplyScalar(pathMultiplier));
                up = loopConstants.upLoopStart.clone().add(loopConstants.upLoopToHome.clone().multiplyScalar(pathMultiplier));
             }
            this.camera.position.set(pos.x,pos.y,pos.z);
            this.camera.up.set(up.x,up.y,up.z);
            this.target = lookAt;
            this.camera.lookAt(lookAt);
            this.camera.updateProjectionMatrix();
        },
        endAutomaticTravel: function(){
            if (this.__automaticCameraAnimation) {
                this.__automaticCameraAnimation.kill();
             }
        },
    
        // DEBUGGING TOOLS
        logVec: function(message,vec){
            console.log(message + ": " + vec.x + " " + vec.y + " " + vec.z);
        },
        addTestCubeAtPosition: function(position){
            var cube = new THREE.Mesh( new THREE.CubeGeometry( 5, 5, 5 ), new THREE.MeshNormalMaterial() );
            cube.position = position.clone();
            Galaxy.Scene.add( cube );
         }
     }
    

Step 12: Three.js: Post-processing & Effects: Dimming, Blurring


  1. Check this out! ( and the tutorial that goes with it )
    Crazy effects are possible with postprocessing in ThreeJS, but I just needed something simple: to dim everything except for the constellation I wanted to show. Bright white lines and points on top of an otherwise dimmed scene.
    Turns out, the 3D world isn't like KineticJS. I can't just add a layer on top and count on the browser rendering the alpha channel of that layer to dim the stuff behind it. Or rather I could, but I'd need a whole separate scene, canvas, and context, and it's not totally clear what happens when you have two webgl windows on top of each other. I don't believe it's trivial to make a semi-transparent webgl rendering context.
    So the next best thing was to add postprocesses to my existing renderer. This is how, when I was talking about Particle Systems in Motion ,
     renderer.render()
    
    became
     _.each(Galaxy.Composers,function(composer){
        composer.render();
     });
    

    Prepare:
    1. In the threejs repo, take a look at examples/js/shaders and examples/js/postprocessing
    2. Choose some shaders. These are just WebGL code stored in .js files, but they're the same in principle as the shaders I went through when describing A Different Brightness for Each Star.
    3. The postprocessing directory has a bunch of utilities, essentially, that let you layer effects on top of each other
    Assemble (put the scripts on your page):
    - THREE.EffectComposer
    - THREE.RenderPass (you need a renderpass so your affects apply to something)
    - THREE.ShaderPass (or two or three -- these wrap your effects)
    - THREE.CopyShader (in case you want to display without effects sometimes)
    - THREE.AdditiveBlendShader (to blend other shaders)
    - some shaders you like

    The Code:
     Galaxy.ComposeScene = function(options){
        var composers = [];
    
        var mainComposer = new THREE.EffectComposer( renderer, renderTarget );
        var renderPass = new THREE.RenderPass( scene, camera );
        mainComposer.addPass( renderPass );
    
        if (_.isObject(options) && options.blur === true) {
            var bluriness = 0.9;
    
            // Prepare the blur shader passes
            var hblur = new THREE.ShaderPass( THREE.HorizontalBlurShader );
            hblur.uniforms[ "h" ].value = bluriness / Galaxy.Settings.width;
            mainComposer.addPass(hblur);
    
            var vblur = new THREE.ShaderPass( THREE.VerticalBlurShader );
            vblur.uniforms[ "v" ].value = bluriness / Galaxy.Settings.height;
            mainComposer.addPass( vblur );
    
            var brightnessContrastPass = new THREE.ShaderPass( THREE.BrightnessContrastShader );
            brightnessContrastPass.uniforms[ "brightness" ].value = -0.3;
            brightnessContrastPass.uniforms[ "contrast" ].value = -0.2;
            mainComposer.addPass(brightnessContrastPass);
         } Else {
            mainComposer.addPass( new THREE.ShaderPass( THREE.CopyShader ) );
         }
        var topComposer = new THREE.EffectComposer(renderer, renderTarget2);
        var topRenderPass = new THREE.RenderPass(topScene,camera);
        topComposer.addPass(topRenderPass);
        topComposer.addPass( new THREE.ShaderPass( THREE.CopyShader ) );
    
         ////////////////////////////////////////////////// //////////////////////
        // final composer will blend composer2.render() results with the scene
         ////////////////////////////////////////////////// //////////////////////
        var blendPass = new THREE.ShaderPass( THREE.AdditiveBlendShader );
        blendPass.uniforms[ 'tBase' ].value = mainComposer.renderTarget1;
        blendPass.uniforms[ 'tAdd' ].value = topComposer.renderTarget1;
        var blendComposer = new THREE.EffectComposer( renderer );
        blendComposer.addPass( blendPass );
        blendPass.renderToScreen = true;
    
        composers.push(mainComposer,topComposer,blendComposer);
    
        return composers;
    };
    

    Basically you'll be calling .render() on a composer now, instead of directly on a renderer. You can assemble pretty much any range of effects you like by first doing a render pass, adding it to the composer, then adding subsequent shaderpasses to the same composer. Eventually, you'd want to add a copyshader to gather everything together, set it to renderToScreen, and render. Boiled down, it might look like this:
     var mainComposer = new THREE.EffectComposer( renderer, renderTarget );
    var renderPass = new THREE.RenderPass( scene, camera );
    mainComposer.addPass( renderPass );
    
    // add a shader
    var shaderpass = new THREE.ShaderPass( THREE.SomeShader );
    ... (do some settings for the shader here)
    mainComposer.addPass(shaderpass);
    
    // add a copy, so webgl knows what to render:
    var copypass = new THREE.ShaderPass( THREE.CopyShader );
    copypass.renderToScreen = true;
    mainComposer.addPass(copypass);
    
    // render
    mainComposer.render();
    

    But in the case of what I've done here, there are actually two separate composers that render two separate sets of objects in the scene. When a constellation is active, its stars and connecting lines are rendered without blur, and at full brightness. The background stars, however, are darkened and blurred. So to get the effects applied to one set of things and not the other, you need a whole separate scene with cloned objects (topScene), and a whole separate composer stack.
    This causes the slight complication that the two need to be blended together as a last step: I could render the topscene, but it would obscure the bottom scene. So the final thing that gets rendered in my case is an additive blend of the two composers' results:
     var blendPass = new THREE.ShaderPass( THREE.AdditiveBlendShader );
    blendPass.uniforms[ 'tBase' ].value = mainComposer.renderTarget1;
    blendPass.uniforms[ 'tAdd' ].value = topComposer.renderTarget1;
    var blendComposer = new THREE.EffectComposer( renderer );
    blendComposer.addPass( blendPass );
    blendPass.renderToScreen = true;
    
    The final bit: you have to render all three composers each frame, or the contents of that composer won't update:
     // in the composeScene function
    return composers.push(mainComposer,topComposer,blendComposer);
    
     ...
    
    // results of ComposeScene are stored each time the user enters/exits a constellation
    Galaxy.Composers = Galaxy.ComposeScene();
    
     ...
    
    // In the render loop, each composer is rendered
    _.each(Galaxy.Composers,function(composer){
        composer.render();
     });
    

    Das ist alles dort ist zu ihm!

Step 13: Other Libraries (Special Thanks)


  1. There are many open-source libraries that help to make this project possible, even though they aren't central to its mission. I've found them all useful, and I recommend checking them out if they look enticing!
    - For the loading indicator when functioning on the web, Pace.js is awesome and easy to implement
    - Animation made easy with TweenMax
    - Parsing and displaying dates: Moment.js
    - Of the many jquery-based on-screen keyboards I came across, Chris Cook's was by far the best
    - This project and many others owe a debt to JQuery , Backbone , and Underscore
    - I love tinyColor.js . It's best-in-class for dealing with colors in JavaScript
    - For CSS-based UI elements, Bootstrap cuts time here and there
    - Touch-enabled scrolling in a web environment would be a hassle without Overscroll.js
    - Cycle2 is one in a crowded field of image sliders, but it's simple and effective
    - Mentioned already: a stupid-simple approach to generating Normally-distributed random numbers deserves a shout-out.
    - The world may love require , but I still love head.js

    Danke fürs Lesen!