Примечание редактора: И мы продолжаем прославлять сообщество Three.js, отсчитывая время до первой конференции Three.js в Париже в сентябре этого года. В этом новом уроке Доминик Фойчик делится небольшим волшебным трюком, позволяющим оживить 2D-изображения с помощью глубины, света и тени с помощью Three.js, TSL и WebGPU.
🇫🇷 Вы думаете о Париже? В рамках нашего партнерства с первой конференцией Three.js читатели Codrops могут использовать код CODROPS получить скидка 15%. Получите свой билет →
Я видел много крутых графических эффектов в сети, но большинство из них остаются на поверхности. Что, если бы вы могли пойти дальше и заставить эффект погрузиться в изображение? Что-то, что действительно проникает внутрь изображения, как свет.
Ключом к этому является карта глубины. То, что раньше казалось волшебством, теперь стало возможным благодаря моделям оценки глубины, которые стали действительно хороши в оценке глубины по 2D-изображениям.
Карта глубины
Это основной ингредиент нашего эффекта. Чтобы получить его, нам нужно снабдить наше изображение моделью оценки глубины и использовать ее для создания карты глубины. Вы можете установить такую модель, как Depth Anything 3, самостоятельно или использовать Инструмент генерации глубины Я создал для этой статьи.


У необработанной карты глубины есть проблема, которую вы не можете увидеть, но свет может. Это 8-битное изображение, поэтому оно имеет только 256 возможных значений глубины, а гладкие поверхности квантуются в плоские ступени. Освещение определяет наклон по этой карте, и на каждом шагу склон внезапно резко возрастает, поэтому тень, которая должна течь по гладкому камню, распадается на песчаный, пятнистый шум. Чтобы это исправить, мы преобразуем карту глубины в плавающую и размываем ее, чтобы сгладить эти шаги.


Сначала мы конвертируем 8-битные значения глубины в значения с плавающей запятой, размываем их, чтобы удалить видимые ступени, а затем сохраняем результат как данные с полуплавной точкой, чтобы мы могли сохранить более плавную информацию о глубине.
const { data } = context.getImageData(0, 0, width, height)
const values = new Float32Array(width * height)
for (let i = 0; i
Имитация поверхности с помощью карты нормалей
Как заставить свет вести себя так, будто наше изображение трехмерное? Ответ нормальный карта.
На самом деле освещение не заботится о самой форме, оно заботится о нормальные люди: направление, в котором обращена каждая точка на поверхности. Точка, обращенная к свету, яркая, точка, наклоненная в сторону, темная. Вот и вся хитрость этого эффекта.
Таким образом, вместо построения реальной геометрии мы можем присвоить каждому пикселю нормаль, благодаря которой плоскость будет казаться трехмерной для света.
Чтобы создать карту нормалей, мы будем использовать нашу карту глубины. depthGradient функция производит выборку карта глубины текстуру и вычисляет наклон поверхности, который мы можем использовать для создания нашей карты нормалей.
const depthGradient = Fn(([vUv, step]) => {
const left = smoothDepthNode.sample(vUv.sub(alongX)).r
const right = smoothDepthNode.sample(vUv.add(alongX)).r
const bottom = smoothDepthNode.sample(vUv.sub(alongY)).r
const top = smoothDepthNode.sample(vUv.add(alongY)).r
return vec2(right.sub(left), top.sub(bottom)).mul(0.5)
})
При визуализации в виде цветов карта нормалей выглядит следующим образом:

Чтобы добавить еще больше деталей, я проделываю тот же трюк с самой фотографией, используя ее яркость вместо глубины. Это обман — темная полоса наклоняет нормаль так же, как настоящая канавка, — но под движущимся светом она воспринимается как деталь поверхности. Два градиента просто складываются вместе:
const shape = vec3(slope.x.negate(), slope.y.negate(), float(1))
return shape.add(vec3(detail.x.negate(), detail.y.negate(), 0)).normalize()
Вот обычное изображение до и после добавления деталей.


Тени
Нормали заставляют изображение реагировать на свет, как трехмерную поверхность, но они не могут заставить одну часть отбрасывать тень на другую. Чтобы найти тени, каждый пиксель проводит линию к свету через карту глубины. Если на этом пути обнаруживается неровность, пиксель находится в тени.
Карта глубины дискретизируется в нескольких точках вдоль этого пути, и величина окклюзии накапливается для создания мягкой тени.
const occlusion = float(0).toVar()
Loop(SHADOW_STEPS, ({ i }) => {
const travel = float(i).add(1).div(SHADOW_STEPS)
const rayDepth = surfaceDepth.add(headroom.mul(travel))
const blockerDepth = smoothDepthNode.sample(vUv.add(sweep.mul(travel))).r
const softness = uShadowSoftness.mul(travel.mul(SOFTNESS_GROWTH).add(1))
const blocked = blockerDepth.sub(rayDepth).div(softness).clamp(0, 1)
occlusion.assign(occlusion.max(blocked))
})
Материал
Все три ингредиента передаются MeshPhongNodeMaterialкоторый выполняет за нас фактический расчет освещения:
const material = new MeshPhongNodeMaterial({ specular: 0x000000 })
material.colorNode = diffuseNode(vUv, depth) // our image
material.normalNode = normalNode(vUv) // fake normals from depth-map
material.aoNode = shadowNode(vUv, depth) // shadows
Здесь изображение становится цветом материала, наши сгенерированные нормали контролируют реакцию поверхности на свет, а тени на основе глубины добавляются в качестве фоновой окклюзии.
Заключительные слова
Я надеюсь, что этот маленький волшебный трюк вдохновит вас на создание собственных интересных эффектов с помощью карт глубины. В этой области еще есть много неисследованных возможностей.
Желаем вам всего наилучшего на предстоящей конференции Three.js! В этом году мне не удалось приехать, но, надеюсь, в следующий раз!