Привет! Меня зовут Майкл Чанг, и я работаю в команде Data Arts в Google. Недавно мы завершили эксперимент Chrome «100 000 звезд» , визуализирующий близлежащие звезды. Проект был построен с использованием THREE.js и CSS3D. В этом примере я опишу процесс открытия, поделюсь некоторыми методами программирования и закончу некоторыми мыслями по поводу будущих улучшений.
Обсуждаемые здесь темы будут довольно обширными и потребуют некоторых знаний о THREE.js, хотя я надеюсь, что вы все равно сможете насладиться этим техническим анализом. Не стесняйтесь перейти к интересующей области, используя кнопку оглавления справа. Сначала я покажу часть проекта, связанную с рендерингом, затем управление шейдерами и, наконец, как использовать текстовые метки CSS в сочетании с WebGL.
Открытие космоса
Вскоре после того, как мы закончили 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, по сути, пиксель в 3D, как один световой год — соглашение, которое унифицировало размещение всего визуализируемого и, к сожалению, позже привело к серьезным проблемам с точностью.
Еще одно соглашение, которое я решил, — это вращать всю сцену, а не перемещать камеру, что я делал в нескольких других проектах. Одним из преимуществ является то, что все размещается на «поворотном столе», так что перетаскивание мышью влево и вправо вращает рассматриваемый объект, но увеличение масштаба — это всего лишь вопрос изменения camera.position.z.
Поле зрения (или FOV) камеры также является динамическим. По мере того, как вы выдвигаетесь наружу, поле зрения расширяется, охватывая все большую и большую часть галактики. Обратное верно: при движении внутрь к звезде поле зрения сужается. Это позволяет камере рассматривать объекты, которые бесконечно малы (по сравнению с галактикой), сжимая поле зрения до чего-то вроде богоподобного увеличительного стекла, не сталкиваясь с проблемами обрезки вблизи плоскости.
Отсюда я смог «поместить» Солнце на несколько единиц дальше от ядра галактики. Я также смог визуализировать относительный размер Солнечной системы, нарисовав вместо этого радиус Утеса Койпера (в конце концов я решил визуализировать Облако Оорта ). В рамках этой модели солнечной системы я также мог визуализировать упрощенную орбиту Земли и для сравнения фактический радиус Солнца.
Солнце было сложно визуализировать. Мне пришлось жульничать, используя столько графических техник реального времени, сколько я знал. Поверхность Солнца представляет собой горячую пену плазмы, которая должна пульсировать и меняться с течением времени. Это было смоделировано с помощью растровой текстуры инфракрасного изображения солнечной поверхности. Шейдер поверхности выполняет поиск цвета на основе оттенков серого этой текстуры и выполняет поиск в отдельной цветовой шкале. Когда этот взгляд смещается со временем, это создает искажение, подобное лаве.
Аналогичная техника использовалась для солнечной короны, за исключением того, что это была бы плоская спрайт-карта, которая всегда была обращена к камере, используя https://github.com/mrdoob/three.js/blob/master/src/extras/core. /гироскоп.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».
Вы можете найти демо-версию (и код в исходном коде) здесь. Однако я обнаружил, что порядок матриц для 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 по-прежнему работают вместе, например, можно навести указатель мыши на трехмерную текстовую метку и заставить ее светиться тенями.
При увеличении масштаба я обнаружил, что масштабирование типографики вызывает проблемы с позиционированием. Возможно, это из-за кернинга и заполнения текста? Другая проблема заключалась в том, что текст становился пиксельным при увеличении, поскольку средство рендеринга 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.