Создание 100 000 звезд

Привет! Меня зовут Майкл Чанг, и я работаю в команде Data Arts в Google. Недавно мы завершили эксперимент Chrome «100 000 звезд» , визуализирующий близлежащие звезды. Проект был построен с использованием THREE.js и CSS3D. В этом примере я опишу процесс открытия, поделюсь некоторыми методами программирования и закончу некоторыми мыслями по поводу будущих улучшений.

Обсуждаемые здесь темы будут довольно обширными и потребуют некоторых знаний о THREE.js, хотя я надеюсь, что вы все равно сможете насладиться этим техническим анализом. Не стесняйтесь перейти к интересующей области, используя кнопку оглавления справа. Сначала я покажу часть проекта, связанную с рендерингом, затем управление шейдерами и, наконец, как использовать текстовые метки CSS в сочетании с WebGL.

100 000 звезд: эксперимент Chrome от команды Data Arts
100,000 Stars использует THREE.js для визуализации ближайших звезд Млечного Пути

Открытие космоса

Вскоре после того, как мы закончили Small Arms Globe , я экспериментировал с демонстрацией частиц THREE.js с глубиной резкости. Я заметил, что могу изменить интерпретируемый «масштаб» сцены, регулируя степень применяемого эффекта. Когда эффект глубины резкости был действительно экстремальным, удаленные объекты становились очень размытыми, подобно тому, как фотография с наклоном и сдвигом создает иллюзию просмотра микроскопической сцены. И наоборот, при выключении эффекта создавалось впечатление, будто вы смотрите в глубокий космос.

Я начал искать данные, которые можно было бы использовать для определения положений частиц. Путь, который привел меня к базе данных HYG на astronexus.com , компиляции трех источников данных (Hipparcos, Йельский каталог ярких звезд и каталог Gliese/Jahreiss), сопровождался по заранее рассчитанным декартовым координатам xyz. Давай начнем!

Построение звездных данных.
Первый шаг — отобразить каждую звезду в каталоге как одну частицу.
Названные звезды.
Некоторые звезды в каталоге имеют имена собственные, отмеченные здесь.

На то, чтобы собрать что-то, помещающее данные о звездах в трехмерное пространство, потребовалось около часа. В наборе данных ровно 119 617 звезд, поэтому представление каждой звезды частицей не является проблемой для современного графического процессора. Также имеется 87 индивидуально идентифицированных звезд, поэтому я создал наложение маркера CSS, используя ту же технику, которую описал в Small Arms Globe.

В это время я только что закончил серию Mass Effect . В игре игроку предлагается исследовать галактику, сканировать различные планеты и читать об их полностью вымышленной истории, напоминающей Википедию: какие виды процветали на планете, ее геологическую историю и так далее.

Зная огромное количество имеющихся данных о звездах, можно было бы представить реальную информацию о галактике таким же образом. Конечной целью этого проекта будет воплотить в жизнь эти данные, позволить зрителю исследовать галактику в стиле Mass Effect, узнать о звездах и их распределении и, надеюсь, вызвать чувство трепета и удивления по поводу космоса. Уф!

Вероятно, мне следует предварить остальную часть этого тематического исследования, сказав, что я ни в коем случае не астроном и что это работа любительского исследования, подкрепленная некоторыми советами внешних экспертов. Этот проект определенно следует рассматривать как художественную интерпретацию пространства.

Создание галактики

Мой план состоял в том, чтобы процедурно создать модель галактики, которая могла бы поместить звездные данные в контекст — и, надеюсь, дать потрясающее представление о нашем месте в Млечном Пути.

Ранний прототип галактики.
Ранний прототип системы частиц Млечного Пути.

Чтобы создать Млечный Путь, я породил 100 000 частиц и поместил их по спирали, имитируя способ формирования галактических рукавов. Меня не слишком беспокоили особенности формирования спиральных рукавов, потому что это была бы репрезентативная модель, а не математическая. Однако я попытался получить более или менее правильное количество спиральных рукавов и их вращение в «правильном направлении».

В более поздних версиях модели Млечного Пути я отказался от использования частиц в пользу плоского изображения галактики, сопровождающего частицы, надеясь придать ему более фотографический вид. Настоящее изображение представляет собой спиральную галактику NGC 1232, находящуюся примерно в 70 миллионах световых лет от нас, изображение обработано так, чтобы оно выглядело как Млечный Путь.

Выясняем масштаб галактики.
Каждая единица GL представляет собой световой год. В данном случае ширина сферы составляет 110 000 световых лет и охватывает систему частиц.

Вначале я решил представить одну единицу GL, по сути, пиксель в 3D, как один световой год — соглашение, которое унифицировало размещение всего визуализируемого, но, к сожалению, впоследствии привело ко мне к серьезным проблемам с точностью.

Еще одно соглашение, которое я решил, — это вращать всю сцену, а не перемещать камеру, что я делал в нескольких других проектах. Одним из преимуществ является то, что все размещается на «поворотном столе», так что перетаскивание мышью влево и вправо вращает рассматриваемый объект, но увеличение масштаба - это всего лишь вопрос изменения camera.position.z.

Поле зрения (или FOV) камеры также является динамическим. По мере того, как вы выдвигаетесь наружу, поле зрения расширяется, охватывая все большую и большую часть галактики. Обратное верно: при движении внутрь к звезде поле зрения сужается. Это позволяет камере рассматривать объекты, которые бесконечно малы (по сравнению с галактикой), сжимая поле зрения до чего-то вроде богоподобного увеличительного стекла, не сталкиваясь с проблемами обрезки в ближней плоскости.

Различные способы рендеринга галактики.
(вверху) Галактика ранних частиц. (внизу) Частицы, сопровождаемые плоскостью изображения.

Отсюда я смог «поместить» Солнце на несколько единиц дальше от ядра галактики. Я также смог визуализировать относительный размер Солнечной системы, нарисовав вместо этого радиус Утеса Койпера (в конце концов я решил визуализировать Облако Оорта ). В рамках этой модели солнечной системы я также мог визуализировать упрощенную орбиту Земли и для сравнения реальный радиус Солнца.

Солнечная система.
Вокруг Солнца вращаются планеты и сфера, представляющая пояс Койпера.

Солнце было сложно визуализировать. Мне пришлось жульничать, используя столько графических техник реального времени, сколько я знал. Поверхность Солнца представляет собой горячую пену плазмы, которая должна пульсировать и меняться с течением времени. Это было смоделировано с помощью растровой текстуры инфракрасного изображения солнечной поверхности. Шейдер поверхности выполняет поиск цвета на основе оттенков серого этой текстуры и выполняет поиск в отдельной цветовой шкале. Когда этот взгляд смещается со временем, это создает искажение, подобное лаве.

Аналогичная техника использовалась для солнечной короны, за исключением того, что это была бы плоская спрайт-карта, которая всегда была обращена к камере, используя https://github.com/mrdoob/three.js/blob/master/src/extras/core . /Gyrscope.js .

Рендеринг Сол.
Ранняя версия Солнца.

Солнечные вспышки были созданы с помощью вершинных и фрагментных шейдеров, примененных к тору, вращающемуся вокруг края солнечной поверхности. Вершинный шейдер имеет функцию шума, заставляющую его переплетаться в виде капли.

Именно здесь я начал сталкиваться с некоторыми проблемами z-fighting из-за точности GL. Все переменные точности были заранее определены в THREE.js, поэтому я не мог реально повысить точность без огромной работы. Проблемы с точностью были не такими серьезными в начале координат. Однако когда я начал моделировать другие звездные системы, это стало проблемой.

Звездная модель.
Код для рендеринга Солнца позже был обобщен для рендеринга других звезд.

Я использовал несколько приемов, чтобы смягчить z-файт. Material.polygonoffset THREE — это свойство, которое позволяет отображать полигоны в другом воспринимаемом месте (насколько я понимаю). Это было использовано для того, чтобы плоскость короны всегда отображалась поверх поверхности Солнца. Ниже был визуализирован солнечный «гало», дающий резкие лучи света, удаляющиеся от сферы.

Другая проблема, связанная с точностью, заключалась в том, что модели звезд начинали дрожать при увеличении сцены. Чтобы исправить это, мне пришлось «обнулить» вращение сцены и по отдельности вращать модель звезды и карту окружения, чтобы создать иллюзию того, что вы вращаетесь по орбите. звезда.

Создание бликов

С большой властью приходит большая ответственность.
С большой властью приходит большая ответственность.

Космические визуализации — это то, где я чувствую, что могу избежать чрезмерного использования бликов. THREE.LensFlare служит этой цели, все, что мне нужно было сделать, это добавить несколько анаморфных шестиугольников и немного JJ Abrams . Во фрагменте ниже показано, как создать их в вашей сцене.

// This function returns a lesnflare THREE object to be .add()ed to the scene graph
function addLensFlare(x,y,z, size, overrideImage){
var flareColor = new THREE.Color( 0xffffff );

lensFlare = new THREE.LensFlare( overrideImage, 700, 0.0, THREE.AdditiveBlending, flareColor );

// we're going to be using multiple sub-lens-flare artifacts, each with a different size
lensFlare.add( textureFlare1, 4096, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );
lensFlare.add( textureFlare2, 512, 0.0, THREE.AdditiveBlending );

// and run each through a function below
lensFlare.customUpdateCallback = lensFlareUpdateCallback;

lensFlare.position = new THREE.Vector3(x,y,z);
lensFlare.size = size ? size : 16000 ;
return lensFlare;
}

// this function will operate over each lensflare artifact, moving them around the screen
function lensFlareUpdateCallback( object ) {
var f, fl = this.lensFlares.length;
var flare;
var vecX = -this.positionScreen.x _ 2;
var vecY = -this.positionScreen.y _ 2;
var size = object.size ? object.size : 16000;

var camDistance = camera.position.length();

for( f = 0; f < fl; f ++ ) {
flare = this.lensFlares[ f ];

flare.x = this.positionScreen.x + vecX * flare.distance;
flare.y = this.positionScreen.y + vecY * flare.distance;

flare.scale = size / camDistance;
flare.rotation = 0;

}
}

Простой способ прокрутки текстур

Вдохновлен Родным миром.
Декартова плоскость, помогающая ориентироваться в пространстве.

Для «плоскости пространственной ориентации» был создан гигантский объект THREE.CylinderGeometry(), центрированный на Солнце. Чтобы создать «волну света», расходящуюся наружу, я изменил смещение ее текстуры с течением времени следующим образом:

mesh.material.map.needsUpdate = true;
mesh.material.map.onUpdate = function(){
this.offset.y -= 0.001;
this.needsUpdate = true;
}

map — это текстура, принадлежащая материалу, которая получает функцию onUpdate, которую вы можете перезаписать. Установка его смещения приводит к «прокрутке» текстуры вдоль этой оси, а спам-рассылка «doesUpdate = true» приведет к зацикливанию этого поведения.

Использование цветовых рамок

Каждая звезда имеет свой цвет в зависимости от «индекса цвета», присвоенного им астрономами. В целом красные звезды холоднее, а синие/фиолетовые — горячее. В этом градиенте присутствует полоса белого и промежуточного оранжевого цветов.

При рендеринге звезд я хотел придать каждой частице свой цвет на основе этих данных. Это можно было сделать с помощью «атрибутов», присвоенных шейдерному материалу, примененному к частицам.

var shaderMaterial = new THREE.ShaderMaterial( {
uniforms: datastarUniforms,
attributes: datastarAttributes,
/_ ... etc _/
});
var datastarAttributes = {
size: { type: 'f', value: [] },
colorIndex: { type: 'f', value: [] },
};

Заполнение массива colorIndex придаст каждой частице уникальный цвет в шейдере. Обычно передается цвет vec3, но в данном случае я передаю число с плавающей запятой для возможного поиска цветовой шкалы.

Цветовая рампа.
Цветовая шкала, используемая для поиска видимого цвета по индексу цвета звезды.

Цветовая шкала выглядела вот так, однако мне нужно было получить доступ к данным цвета растрового изображения из JavaScript. Я сделал это следующим образом: сначала загрузил изображение в DOM, нарисовал его в элементе холста, а затем получил доступ к растровому изображению холста.

// make a blank canvas, sized to the image, in this case gradientImage is a dom image element
gradientCanvas = document.createElement('canvas');
gradientCanvas.width = gradientImage.width;
gradientCanvas.height = gradientImage.height;

// draw the image
gradientCanvas.getContext('2d').drawImage( gradientImage, 0, 0, gradientImage.width, gradientImage.height );

// a function to grab the pixel color based on a normalized percentage value
gradientCanvas.getColor = function( percentage ){
return this.getContext('2d').getImageData(percentage \* gradientImage.width,0, 1, 1).data;
}

Этот же метод затем используется для раскрашивания отдельных звезд на виде звездной модели.

Мои глаза!
Тот же метод используется для поиска цвета спектрального класса звезды.

Шейдерные споры

На протяжении всего проекта я обнаружил, что мне нужно писать все больше и больше шейдеров для реализации всех визуальных эффектов. Для этой цели я написал собственный загрузчик шейдеров, потому что мне надоело хранить шейдеры в index.html.

// list of shaders we'll load
var shaderList = ['shaders/starsurface', 'shaders/starhalo', 'shaders/starflare', 'shaders/galacticstars', /*...etc...*/];

// a small util to pre-fetch all shaders and put them in a data structure (replacing the list above)
function loadShaders( list, callback ){
var shaders = {};

var expectedFiles = list.length \* 2;
var loadedFiles = 0;

function makeCallback( name, type ){
return function(data){
if( shaders[name] === undefined ){
shaders[name] = {};
}

    shaders[name][type] = data;

    //  check if done
    loadedFiles++;
    if( loadedFiles == expectedFiles ){
    callback( shaders );
    }

};

}

for( var i=0; i<list.length; i++ ){
var vertexShaderFile = list[i] + '.vsh';
var fragmentShaderFile = list[i] + '.fsh';

//  find the filename, use it as the identifier
var splitted = list[i].split('/');
var shaderName = splitted[splitted.length-1];
$(document).load( vertexShaderFile, makeCallback(shaderName, 'vertex') );
$(document).load( fragmentShaderFile,  makeCallback(shaderName, 'fragment') );

}
}

Функция loadShaders() принимает список имен файлов шейдеров (ожидается расширение .fsh для фрагментных и .vsh для вершинных шейдеров), пытается загрузить их данные, а затем просто заменяет список объектами. Конечным результатом является то, что в вашу униформу THREE.js вы можете передать ей шейдеры следующим образом:

var galacticShaderMaterial = new THREE.ShaderMaterial( {
vertexShader: shaderList.galacticstars.vertex,
fragmentShader: shaderList.galacticstars.fragment,
/_..._/
});

Я, вероятно, мог бы использовать require.js, хотя для этой цели потребовалась бы некоторая перекомпоновка кода. Это решение, хотя и намного проще, я думаю, можно улучшить, возможно, даже в виде расширения THREE.js. Если у вас есть предложения или способы сделать это лучше, дайте мне знать!

Текстовые метки CSS поверх THREE.js

В нашем последнем проекте, Small Arms Globe, я экспериментировал с отображением текстовых меток поверх сцены THREE.js. Метод, который я использовал, вычисляет абсолютную позицию модели того места, где я хочу, чтобы отображался текст, затем определяет положение экрана с помощью THREE.Projector() и, наконец, использует CSS «сверху» и «слева», чтобы разместить элементы CSS в желаемом месте. позиция.

В ранних версиях этого проекта использовалась та же техника, однако мне не терпелось попробовать другой метод, описанный Луисом Крузом.

Основная идея: сопоставьте преобразование матрицы CSS3D с камерой и сценой THREE, и вы сможете «размещать» элементы CSS в 3D, как если бы они находились поверх сцены THREE. Однако здесь есть ограничения: например, вы не сможете разместить текст под объектом THREE.js. Это все равно намного быстрее, чем попытка выполнить макет с использованием атрибутов CSS «top» и «left».

Текстовые метки.
Использование преобразований CSS3D для размещения текстовых меток поверх WebGL.

Вы можете найти демо-версию (и код в исходном коде) здесь. Однако я обнаружил, что порядок матриц для THREE.js изменился. Функция, которую я обновил:

/_ Fixes the difference between WebGL coordinates to CSS coordinates _/
function toCSSMatrix(threeMat4, b) {
var a = threeMat4, f;
if (b) {
f = [
a.elements[0], -a.elements[1], a.elements[2], a.elements[3],
a.elements[4], -a.elements[5], a.elements[6], a.elements[7],
a.elements[8], -a.elements[9], a.elements[10], a.elements[11],
a.elements[12], -a.elements[13], a.elements[14], a.elements[15]
];
} else {
f = [
a.elements[0], a.elements[1], a.elements[2], a.elements[3],
a.elements[4], a.elements[5], a.elements[6], a.elements[7],
a.elements[8], a.elements[9], a.elements[10], a.elements[11],
a.elements[12], a.elements[13], a.elements[14], a.elements[15]
];
}
for (var e in f) {
f[e] = epsilon(f[e]);
}
return "matrix3d(" + f.join(",") + ")";
}

Поскольку все трансформируется, текст больше не обращен к камере. Решением было использование THREE.Gyrscope() , который заставляет Object3D «потерять» унаследованную от сцены ориентацию. Этот прием называется «билбординг», и для этого идеально подходит Гироскоп.

Что действительно приятно, так это то, что все обычные DOM и CSS по-прежнему работают вместе, например, возможность навести указатель мыши на трехмерную текстовую метку и заставить ее светиться тенями.

Текстовые метки.
Текстовые метки всегда обращены к камере, если прикрепить их к THREE.Gyrscope().

При увеличении масштаба я обнаружил, что масштабирование типографики вызывает проблемы с позиционированием. Возможно, это из-за кернинга и заполнения текста? Другая проблема заключалась в том, что текст становился пиксельным при увеличении, поскольку средство рендеринга DOM обрабатывало визуализированный текст как текстурированный четырехугольник, о чем следует помнить при использовании этого метода. Оглядываясь назад, я мог бы просто использовать текст гигантского размера шрифта, и, возможно, это что-то для будущего исследования. В этом проекте я также использовал текстовые метки размещения CSS «сверху/слева», описанные ранее, для очень маленьких элементов, сопровождающих планеты в Солнечной системе.

Воспроизведение музыки и циклическое воспроизведение

Музыкальное произведение, звучавшее во время «Галактической карты» Mass Effect, было написано композиторами Bioware Сэмом Хьюликом и Джеком Уоллом, и оно вызывало те эмоции, которые я хотел, чтобы посетитель испытал. Мы хотели, чтобы в нашем проекте было немного музыки, потому что мы чувствовали, что это важная часть атмосферы, помогающая создать то чувство трепета и удивления, к которому мы пытались стремиться.

Наш продюсер Вальдин Кламп связался с Сэмом, у которого была куча музыки из Mass Effect, которую он очень любезно разрешил нам использовать. Трек называется «В чужой стране».

Я использовал тег audio для воспроизведения музыки, однако даже в Chrome атрибут «loop» был ненадежным — иногда он просто не мог зациклиться. В конце концов, этот хак с двумя аудиотегами был использован для проверки окончания воспроизведения и перехода к другому тегу для воспроизведения. Что было разочаровывающим, так это то, что это все еще не было идеальным циклом все время, увы, я чувствую, что это было лучшее, что я мог сделать.

var musicA = document.getElementById('bgmusicA');
var musicB = document.getElementById('bgmusicB');
musicA.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playB = function(){
musicB.play();
}
// make it wait 15 seconds before playing again
setTimeout( playB, 15000 );
}, false);

musicB.addEventListener('ended', function(){
this.currentTime = 0;
this.pause();
var playA = function(){
musicA.play();
}
// otherwise the music will drive you insane
setTimeout( playA, 15000 );
}, false);

// okay so there's a bit of code redundancy, I admit it
musicA.play();

Возможности для совершенствования

Поработав некоторое время с THREE.js, я чувствую, что дошёл до того, что мои данные слишком сильно смешивались с моим кодом. Например, определяя материалы, текстуры и инструкции по геометрии в режиме реального времени, я, по сути, занимался «3D-моделированием с помощью кода». Это было очень плохо, и это область, которую в будущем можно было бы значительно улучшить с помощью THREE.js, например, определение данных о материале в отдельном файле, желательно с возможностью просмотра и настройки в некотором контексте, и который можно было бы вернуть обратно в основной проект.

Наш коллега Рэй МакКлюр также потратил некоторое время на создание потрясающих генеративных «космических шумов», которые пришлось вырезать из-за нестабильности API веб-аудио, время от времени приводившей к сбою Chrome. Это прискорбно… но это определенно заставило нас задуматься о звуковом пространстве для будущей работы. На момент написания этой статьи мне сообщили, что API веб-аудио был исправлен, поэтому, возможно, он работает сейчас, и на это стоит обратить внимание в будущем.

Типографские элементы в сочетании с WebGL по-прежнему остаются проблемой, и я не на 100% уверен, что то, что мы здесь делаем, правильно. Это все еще похоже на взлом. Возможно, будущие версии THREE с будущим CSS Renderer можно будет использовать для лучшего объединения этих двух миров.

Кредиты

Спасибо Аарону Коблину за то, что позволил мне приехать в город с этим проектом. Джоно Бранделю за отличный дизайн пользовательского интерфейса + реализацию, обработку шрифтов и реализацию тура. Валдеану Клампу за название проекта и всю его копию. Сабаху Ахмеду за очистку метрической тонны прав использования источников данных и изображений. Клему Райту за обращение к нужным людям для публикации. Дугу Фрицу за техническое мастерство. Джорджу Брауэру за то, что научил меня JS и CSS. И, конечно же, мистер Дуб для THREE.js.

Рекомендации