Буквально вчера была официально обнародована первая в мире фотография черной дыры, и черная дыра из брикетов сходила с ума в кругу друзей. Удивлен или нет, удивлен или нет? Подумайте об этой статье, переведенной 4 года назад, а затем отправьте ее, наши фронтенд-инженеры используют JS, чтобы нарисовать черную дыру самостоятельно!
Черная дыра кажется огромной естественной границей.В черной дыре и квантовая теория, и классическая механика в том виде, в каком мы ее знаем, потерпят неудачу. Физики были вынуждены придумать действительно странные идеи, например, наша Вселенная — это голограмма, вся ее информация хранится на огромной двумерной поверхности вокруг нас. Физики оказались в парадоксальном ящике Пандоры: нет сомнения, что исследователь мог бы не заметить ничего необычного, когда полностью прошел через черную дыру, но сторонний наблюдатель мог бы к тому моменту, когда исследователь был разорван и деформирован при пересечении черной дыры, обе истории, вероятно, правдивы, невероятны. В любом случае, это довольно круто.
В фильме Нолана «Интерстеллар» в космос помещена огромная черная дыра, опасная и торжественная, и ее предел света выходит за рамки здравого смысла. Из этого фильма я впервые ощутил всю прелесть гравитационного линзирования, я часто встречал концепцию гравитационного линзирования в учебниках по физике, но никогда не видел такого увлекательного объяснения. Поэтому недавно я решил изучить гравитационное линзирование, чтобы посмотреть, что я могу сделать с тем, что я узнал.
Следуя обычной практике, выберите существительное и создайте файл .js, и я очень горжусь выпуском black-hole.js (GitHub). Он использует численный решатель дифференциальных уравнений number.js и прекрасные инструменты рендеринга WebGL glfx.js для визуализации гравитационного линзирования черных дыр.
Пример
Наведите указатель мыши на холст ниже, чтобы увидеть черную дыру.
Аннотация: Из-за ограничения знания столбца вы можете перейти к исходному black-hole.js, чтобы просмотреть эффект.
Что такое гравитационное линзирование?
Одним из выдающихся вкладов Эйнштейна в физику была идея о том, что гравитация не только притягивает объекты друг к другу, но и искривляет само пространство. То есть во Вселенной даже незначительные, почти безмассовые фотоны притягиваются огромными небесными телами. Существует общее мнение о черных дырах, что их сильное гравитационное притяжение может ограничивать свет краями черной дыры, оставляя сферическое пространство вообще без света (отсюда и название). Но подумайте о тех световых лучах, проходящих по краю черной дыры, они должны сильно изгибаться, но, вероятно, не попадут в черную дыру.
На краях черных дыр наблюдается несколько странных явлений. Из-за сильного искривления пространства элементы, расположенные дальше от одной стороны черной дыры, скорее всего, кажутся близкими к другой стороне; искривленные лучи света очень близко к краю черной дыры, вероятно, сделают полный круг обратно к наблюдателю. глаз, а это значит, что вы очень можете увидеть себя в свете, искривленном черной дырой.
Если мы хотим рассчитать эти эффекты гравитационного линзирования, нам нужно немного погрузиться в математику. В Википедии есть как строгие объяснения, так и вульгарные, но лучшее резюме, которое я нашел, — это запись в блоге Джейсона на jasmcole.com. Прочтите эту статью, чтобы узнать некоторые более тонкие физические подробности о гравитационном линзировании.
Изображение с jasmcole.com
Если из наблюдающего глаза исходит луч света,стреляя в черную дыру под углом, расстояние между наблюдателем и черной дырой равно, если вы можете знать угол, под которым луч удаляется от черной дыры Достаточно. Мы можем использовать полярную координату r как расстояние от черной дыры,Угол — это угол, образованный фотоном, черной дырой и наблюдателем, чтобы проследить путь света вокруг черной дыры. Насколько я знаю, дляа такжеНе существует решения этой зависимости в закрытой форме, но благодаря опыту Джейсона в области физики на jasmcole.com существует обыкновенное дифференциальное уравнение, выражающее взаимосвязь между этими величинами.
Решение в закрытой форме: решение, выраженное базовыми функциями (такими как полиномы, тригонометрические функции, логарифмические функции и т. д.) и специальными функциями, или решение, которое может быть выражено в определенной форме. —— Аннотация
Для дальнейшего упрощения используйтезаменять,сделать, мы получаем очень простое дифференциальное уравнение 2-го порядка (ОДУ 2-го порядка).
Если мы нацелимся на одногоРешая это дифференциальное уравнение, мы можем узнать xy-координаты траектории фотона и выяснить, где фотон в конечном итоге приземляется. Многократно решая множество, мы получим достаточно данных, чтобы найтиа такжесоответствующее отношение. Вы можете использовать MATLAB или аналогичные инструменты для решения этого дифференциального уравнения. Но так как я хотел создать компонент JavaScript, а все окончательные параметры можно было настроить, я решил использовать numeric.js, отличную математическую библиотеку, с доступным онлайн-тестовым стендом REPL, графиками и отладкой.
Используйте mumeric.js для решения ODE 2-го порядка (дифференциальные уравнения второго порядка)
numeric.js содержит множество полезных математических инструментов, которые вы можете изучить здесь и опробовать на тестовом онлайн-стенде. Нас больше интересует функция numeric.dopri(), которая использует метод РК Дорманда-Принса для выполнения интегральной операции дифференциального уравнения. Популярное объяснение состоит в том, что эта функция принимает дифференциальное уравнение в качестве входных данных и выводит ряд из множества точек, которые удовлетворяют ограничениям дифференциального уравнения, так что мы можем нарисовать график.
Давайте посмотрим, сможем ли мы преобразовать наше дифференциальное уравнение в форму, приемлемую для numeric.dopri() . Это требует, чтобы наше дифференциальное уравнение имело форму функции, которая принимает два аргумента: независимую переменную (здесь), и вектор, содержащий две независимые переменные, который в нашем случае равена также. Затем он также ожидает, что мы вернем вектор, содержащийа также. Выписанная функция дифференциального уравнения выглядит следующим образом:
var blackHoleODESystem = function (phi, vec) {
var u = vec[0];
var u_prime = vec[1];
var u_double_prime = 3*u*u - u;
return [u_prime, u_double_prime];
}
Чтобы решить наше дифференциальное уравнение, числовое значение должно знатьа такженачальное значение . Не сложный,, который затем можно найти геометрически. По своему опыту я убедился, что, помещаяУстановка 20 дает более красивые результаты. Вы можете использовать разныечтобы проверить, как выглядит путь, я обнаружил, что когдаПри
Имея начальные значения системы, мы можем вызвать numeric.dopri(), задав координаты, удовлетворяющие дифференциальному уравнению:
var getBlackHoleSolution = function (startAngle, startRadius, startPhi, endPhi, optionalNumIterations) {
var numIterations = optionalNumIterations || 100;
return numeric.dopri(
startPhi,
endPhi,
[1.0/startRadius,
1.0/(startRadius * Math.tan(startAngle))],
blackHoleSystem,
undefined, // tolerance not needed
numIterations
);
}
var startAngleInDegrees = 15;
var startRadius = 20;
var sol = getBlackHoleSolution(
startAngleInDegrees * Math.PI / 180,
startRadius,
0,
10 * Math.PI);
Было бы неплохо иметь возможность построить полученные координаты xy, чтобы увидеть путь вокруг черной дыры. Этого можно достичь следующим образом:
var getXYPhotonPathFromSolution = function (sol) {
// Note: transpose(sol.y)[0] contains the u values.
// transpose(sol.y)[1] contains u_prime values.
var rValues = numeric.transpose(sol.y)[0]
.map(function (u) {
// we want the radius r = 1/u
return 1 / u;
});
var phiValues = sol.x;
var photonPath = [];
for (var i = 0; i < rValues.length; i++) {
// Negative r is nonsensical, and this can occur as
// radius goes to infinity. Stop when you start
// hitting infinity
if (rValues[i] < 0) break;
var x = rValues[i] * Math.cos(phiValues[i]);
var y = rValues[i] * Math.sin(phiValues[i]);
photonPath.push([x, y]);
}
return photonPath;
}
Наконец, мы можем построить координаты xy пути фотона, используя тестовый стенд REPL numeric.js. Вам нужно скопировать предыдущие функции в REPL, чтобы вы могли использовать их в вышеуказанных функциях.
Чтобы нарисовать ответ в REPL, нам нужно сделать несколько вещей: Если границы рисунка фиксированы, результаты можно легко увидеть, поэтому мы сначала устанавливаем границы рисунка по умолчанию. Затем создайте два решения, одно для луча под углом 15°, а другое для угла 40°. Затем мы получаем xy пути фотонов для этих двух решений. Наконец, нарисуйте результат.
> var plotOptions = { xaxis: { min: -20, max: 20 }, yaxis: { min: -20, max: 20 } }
> var sol1 = getBlackHoleSolution(15 * Math.PI / 180, startRadius, 0, 10 * Math.PI);
> var sol2 = getBlackHoleSolution(40 * Math.PI / 180, startRadius, 0, 10 * Math.PI);
> var photonPath1 = getXYPhotonPathFromSolution(sol1);
> var photonPath2 = getXYPhotonPathFromSolution(sol2);
> workshop.plot([photonPath1, photonPath2]);
Вот результаты сюжета с разных точек зрения. Траектория с малым начальным углом закручивается вокруг черной дыры и падает в нее. Чуть более широкий угол света позволяет наблюдателю видеть закругленные углы, очень большой угол света лишь немного отклоняет.
построить подходящуюсопоставить сПолиномиальная функция
Теперь, учитываяМы можем получить путь фотона с координатами xy, так как же мы найдем его по этому пути?. Я обнаружил, что выборка из 100 начальных углов от 0° до 80° работает лучше всего. Учитывая координаты xy пути фотона, мы делаем следующие шаги:
-
Когда начальный угол мал,Потому что свет значительно непосредственно попадает в черную дыру. Чтобы определить, попал ли Photon Path в черную дыру, нам нужно проверить координаты из окончательной черной дыры (0,0) на небольшом расстоянии. В конечном итоге мы найдем одну из самых больших черной дыры в путь начального угла и помните его;
-
При других начальных углах фотоны в конечном итоге находятся на определенном расстоянии от черной дыры. Для полученного углаДля определения нам нужно просто получить его по арктангенсу наклона двух последних точек на пути xy. В результатах некоторые ракурсы указывают на то, что свет попадает за край изображения, а некоторые даже возвращаются обратно в глаз зрителя. В конечном итоге решение о том, как раскачивать эти точки, полностью зависит от вас. Потому что, в отличие от природы, у нас нет полной трехмерной картины нашего окружения. В итоге я просто приблизился к самому дальнему пикселю, и все выглядело нормально.
Получив угол падения к углу излучения, мы можем сгенерировать подходящую полиномиальную функцию с помощью функции numeric.uncmin(). Мы можем оптимизировать числовой.js, поставив задачу «минимальной дисперсии», выбрав наилучшие коэффициенты для полиномиальной функции для аппроксимации наших данных. Я обнаружил, что существует простая полиномиальная функция второго порядка, которая соответствует лучше всего. Эта функция:
Ниже приведен алгоритм, который numeric.js использует для оптимизации дисперсии (между результатом оценки функции и нашими данными)., пока функция не станет очень близкой к данным:
-
выберите,,, определить новую полиномиальную функцию;
-
Вычислите сумму этой формулы:
-
Постоянно корректируйте, а также , пока результат приведенной выше формулы не станет достаточно малым.
Выражение этого процесса в JavaScript выглядит следующим образом:
var angleTables = computeBlackHoleTables(startRadius, numAngleDataPoints, maxAngle, optionalNumIterationsInODESolver);
var polynomial = function (params, x) {
var sum = 0.0;
for (var i = 0; i <= polynomialDegree; i++) {
sum += params[i] * Math.pow(x, i);
}
return sum;
}
var initialCoeffs = [];
for (var i = 0; i <= polynomialDegree; i++) {
initialCoeffs.push(1.0);
}
var leastSquaresObjective = function (params) {
var total = 0.0;
for (var i = 0; i < angleTables.inAngles.length; i++) {
var result = polynomial(params, angleTables.inAngles[i]);
var delta = result - angleTables.outAngles[i];
total += (delta * delta);
}
return total;
}
var minimizer = numeric.uncmin(leastSquaresObjective, initialCoeffs);
var polynomialCoefficients = minimizer.solution;
Рендеринг результатов с простой геометрией
Метод показан на рисунке выше. Это дает возможные ответы, когда дело доходит до определения того, что должно быть нарисовано в месте красной точки на конечном изображении. Мы нашли то, что должно быть нарисовано от зеленой точки на исходном изображении.
Алгоритм следующий. Во-первых, нам нужно определить расстояние до плоскости изображения в «пиксельном пространстве» наблюдателя. Теперь, когда мы указали угол обзора, расстояние равно.
Теперь для заданных координат xy мы имеем, как мы определяем соответствующийШерстяная ткань? Ранее мы создали подходящую полиномиальную функцию. Вызовите эту функцию, чтобы получить. если этоЕсли он меньше угла черной дыры, мы нарисуем его черным цветом. Если он больше угла черной дыры, находим расстояние черной дыры в ней,. Значение расстояния может быть положительным или отрицательным, в зависимости отценность . Если отрицательный,появится в другом направлении от черной дыры. Координаты xy выхода можно рассчитать, умножив единичный вектор между точкой входа xy и черной дырой на расстояние на выходе..
Тогда есть координаты точек выхода.
OMG HTML5, WebGL и шейдеры!
Первая попытка нарисовать черную дыру в браузере была смесью радости и печали. Используя объекты HTML5 Canvas и ImageData, я могу быстро нарисовать статичную черную дыру в центре фонового изображения. Но когда я модифицировал плагин так, чтобы черная дыра могла следовать за моей мышью по фоновому изображению, программа начала зависать. Задержка между кадрами составляет от 300 до 400 мс. Пробовал несколько взломов HTML5, но ничего не помогло.
Вы знаете, сам процесс рендеринга на самом деле можно распараллелить. Начальный угол может генерировать выходной угол независимо, и между ними нет никакой зависимости. Графические процессоры должны прекрасно справляться с такими вещами. Я решил, что пришло время изучить шейдеры WebGL GLSL.
GLSL: OpenGL Shading Language (язык шейдинга OpenGL) — это язык, используемый для шейдерного программирования в OpenGL, то есть коротких пользовательских программ, написанных разработчиками, которые выполняются на GPU (Graphic Processor Unit) видеокарты. фиксированная часть конвейера рендеринга, позволяющая программировать различные уровни конвейера рендеринга. Например: преобразование просмотра, преобразование проекции и т. д. Код шейдера GLSL (GL Shading Language) делится на 2 части: Vertex Shader (вершинный шейдер) и Fragment (фрагментный шейдер), а иногда и Geometry Shader (геометрический шейдер). Именно вершинный шейдер отвечает за выполнение вершинного затенения. Он может получить текущее состояние в OpenGL и передать его с помощью встроенных переменных GLSL. GLSL использует язык C в качестве основного языка затенения высокого уровня, избегая сложности использования языка ассемблера или языка спецификации оборудования. --Энциклопедия Baidu
Как инженер, ориентированный на веб и мобильные устройства, использование WebGL для рисования требует написания большого количества шаблонного кода. Я хочу, чтобы код WebGL был максимально простым, и, надеюсь, с помощью любой библиотеки классов с открытым исходным кодом наша программа сможет сосредоточиться на интересном гравитационном линзировании. Я нашел очень хорошую библиотеку для использования в качестве базы. Библиотека glfx.js действительно классная. Его домашняя страница даже отображает эффект водоворота черной дыры в очевидном месте, который может следовать за движением мыши и имеет высокую частоту кадров. Это определенно правильное направление.
glfx.js предоставляет множество забавных и простых в использовании эффектов, от завихрений до выпуклых снимков, которые можно использовать на 2D-изображениях. Я довольно много пытался расширить его с помощью функции рендеринга черной дыры, но в итоге я изменил код glfx.js.
Эффект черной дыры создается с помощью шейдера GLSL. GLSL очень сложно отлаживать: нет console.log. Интересно столкнуться с таким ограничением, что разрешен только фиксированный целочисленный индекс или последовательный доступ к массиву в цикле for. В конечном счете, GLSL — это весело, а процесс разработки требует постоянных небольших настроек и наблюдения за тем, как тысячи пикселей падают и летают, пока не будет достигнут окончательный результат.
В GLSL вам нужно определить единые значения, которые содержат все пиксели для обработки графическим процессором. Для работы алгоритма мне нужно передать несколько важных значений: координаты xy черной дыры, угол, под которым максимум «попадает в черную дыру», расстояние от наблюдателя до плоскости изображения в "пиксельный мир" и, самое сложное, таблица полиномов углов. Большинство типов значений легко определить, в основном это либо число с плавающей запятой, либо двумерный вектор, вектор, содержащий два числа с плавающей запятой.
Однако передать таблицу угловых полиномов немного сложнее, так как она может содержать длину переменной. Вы можете сделать это, создав текстуру sampler2D с двумя линиями, одна для угла падения, а другая для угла выхода, и инициализируя ее круглосуточно, координируя объекты ImageData и Canvas. Насколько я знаю, есть только один способ импортировать объекты с динамическими размерами в шейдеры, и, по статистике, они замедляют работу GPU по сравнению с массивами фиксированного размера. Вы также можете импортировать таблицу угловых полиномов как массив чисел с плавающей запятой фиксированного размера, но каждый массив должен определять фиксированный размер во время компиляции. Мой шейдер черной дыры, как и другие шейдеры в glfx.js, на самом деле представляет собой записанную строку, передаваемую конструктору шейдера. Таким образом, я могу создать эту строку во время выполнения, вставить длину в строку, если это необходимо, и, наконец, передать ее объекту шейдера для компиляции.
Это все для шейдеров. Простите меня за использование здесь символа '\' для разделения одной и той же строки на несколько строк.
/**
* Gravitational lensing effect due to black hole.
*/
function blackHole(centerX, centerY, blackHoleAngleFn, fovAngle) {
var anglePolynomialCoefficients = blackHoleAngleFn.anglePolynomialCoefficients;
var numPolynomialCoefficients = anglePolynomialCoefficients.length;
gl.blackHoleShaders = gl.blackHoleShaders || {};
gl.blackHoleShaders[numPolynomialCoefficients] = gl.blackHoleShaders[numPolynomialCoefficients] || new Shader(null, '\
/* black hole vars */\
uniform float distanceFromViewerToImagePlane;\
uniform float maxInBlackHoleAngle;\
uniform vec2 blackHoleCenter;\
uniform float anglePolynomialCoefficients[' + numPolynomialCoefficients + '];\
/* texture vars */\
uniform sampler2D texture;\
uniform vec2 texSize;\
varying vec2 texCoord;\
\
float anglePolynomialFn(float inAngle) {\
float outAngle = 0.0;\
for (int i = 0; i < ' + numPolynomialCoefficients + '; i++) {\
outAngle += anglePolynomialCoefficients[i] * pow(inAngle, float(i));\
}\
return outAngle;\
}\
\
void main() {\
vec2 coord = texCoord * texSize;\
vec2 vecBetweenCoordAndCenter = coord - blackHoleCenter;\
float distanceFromCenter = length(vecBetweenCoordAndCenter);\
float inAngle = atan(distanceFromCenter, distanceFromViewerToImagePlane);\
/* Completely black if in black hole */\
if (inAngle <= maxInBlackHoleAngle) {\
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\
return;\
}\
float outAngle = anglePolynomialFn(inAngle);\
float outDistanceFromCenter = tan(outAngle) * distanceFromViewerToImagePlane;\
vec2 unitVectorBetweenCoordAndCenter = vecBetweenCoordAndCenter / distanceFromCenter;\
vec2 outCoord = blackHoleCenter + unitVectorBetweenCoordAndCenter * outDistanceFromCenter;\
outCoord = clamp(outCoord, vec2(0.0), texSize);\
gl_FragColor = texture2D(texture, outCoord / texSize);\
}');
var h = this.height;
var w = this.width;
var distanceFromViewerToImagePlane = Math.sqrt(h*h + w*w) / Math.tan(fovAngle);
simpleShader.call(this, gl.blackHoleShaders[numPolynomialCoefficients], {
anglePolynomialCoefficients: {
uniformVectorType: 'uniform1fv',
value: anglePolynomialCoefficients
},
distanceFromViewerToImagePlane: distanceFromViewerToImagePlane,
maxInBlackHoleAngle: blackHoleAngleFn.maxInBlackHoleAngle,
blackHoleCenter: [centerX, centerY],
texSize: [this.width, this.height]
});
return this;
}
После того, как я изменил glfx.js, окончательный код отрисовки сократился до следующих строк:
var blackHoleAngleFn = BlackHoleSolver
.computeBlackHoleAngleFunction(
distanceFromBlackHole,
polynomialDegree,
numAngleTableEntries,
fovAngleInDegrees);
// ... on mouse move
canvas.draw(texture)
.blackHole(x, y,
blackHoleAngleFn,
fovAngleInRadians)
.update();
Готово, все гравитационное линзирование! Даже мистер Маффинс.
Другой пример?
Аннотация: Из-за ограничения знания столбца вы можете перейти к исходному black-hole.js, чтобы просмотреть эффект.
Оригинал: http://cliffcrosland.tumblr.com/post/115981256393/black-hole-js
Иностранный журнал Джун рекомендовал к прочтению:
-
http://jasmcole.com/2014/10/04/what-do-black-holes-look-like/
-
Научно-популярная черная дыра червоточины, готовьтесь к Interstellar (Interstellar)