предисловие
Всем привет, это волшебник CSS - alphardex.
В этой статье мы будем использовать three.js для реализации классного оптического эффекта — капель росы. Мы знаем, что липкий эффект возникает, когда роса капает с поверхности. В 2D-плоскости этот липкий эффект может быть легко достигнут с помощью фильтров CSS. Но когда дело доходит до 3D-мира, все не так просто, в настоящее время для его достижения нам приходится полагаться на освещение, что включает в себя ключевой алгоритм — Ray Marching. Ниже приведен окончательный рендер реализации
Са, маршрут Хадзима!
Готов к работе
авторышаблон three.js: Нажмите вилку в правом нижнем углу, чтобы скопировать копию.
положительный
полноэкранная камера
Сначала измените камеру на ортогональную, затем отрегулируйте длину плоскости до 2, чтобы она заполнила экран.
class RayMarching extends Base {
constructor(sel: string, debug: boolean) {
super(sel, debug);
this.clock = new THREE.Clock();
this.cameraPosition = new THREE.Vector3(0, 0, 0);
this.orthographicCameraParams = {
left: -1,
right: 1,
top: 1,
bottom: -1,
near: 0,
far: 1,
zoom: 1
};
}
// 初始化
init() {
this.createScene();
this.createOrthographicCamera();
this.createRenderer();
this.createRayMarchingMaterial();
this.createPlane();
this.createLight();
this.trackMousePos();
this.addListeners();
this.setLoop();
}
// 创建平面
createPlane() {
const geometry = new THREE.PlaneBufferGeometry(2, 2, 100, 100);
const material = this.rayMarchingMaterial;
this.createMesh({
geometry,
material
});
}
}
Создать материал
Создайте материал шейдера, который определяет все параметры, которые будут переданы шейдеру.
const matcapTextureUrl = "https://i.loli.net/2021/02/27/7zhBySIYxEqUFW3.png";
class RayMarching extends Base {
// 创建光线追踪材质
createRayMarchingMaterial() {
const loader = new THREE.TextureLoader();
const texture = loader.load(matcapTextureUrl);
const rayMarchingMaterial = new THREE.ShaderMaterial({
vertexShader: rayMarchingVertexShader,
fragmentShader: rayMarchingFragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTime: {
value: 0
},
uMouse: {
value: new THREE.Vector2(0, 0)
},
uResolution: {
value: new THREE.Vector2(window.innerWidth, window.innerHeight)
},
uTexture: {
value: texture
},
uProgress: {
value: 1
},
uVelocityBox: {
value: 0.25
},
uVelocitySphere: {
value: 0.5
},
uAngle: {
value: 1.5
},
uDistance: {
value: 1.2
}
}
});
this.rayMarchingMaterial = rayMarchingMaterial;
}
}
вершинный шейдерrayMarchingVertexShader
, это нужно только в готовом виде с шаблоном
Основное внимание уделяется фрагментному шейдеру.rayMarchingFragmentShader
фрагментный шейдер
задний план
В качестве разминки сначала создайте сияющий фон.
varying vec2 vUv;
vec3 background(vec2 uv){
float dist=length(uv-vec2(.5));
vec3 bg=mix(vec3(.3),vec3(.0),dist);
return bg;
}
void main(){
vec3 bg=background(vUv);
vec3 color=bg;
gl_FragColor=vec4(color,1.);
}
sdf
Как создавать объекты в модели освещения? Нам нужен СДФ.
sdf означает функцию расстояния со знаком: если она передается координате в функциональном пространстве, она возвращает кратчайшее расстояние между этой точкой и некоторыми плоскостями.Символ возвращаемого значения указывает, находится ли точка внутри или снаружи плоскости, поэтому она называется расстоянием со знаком.функция.
Если мы хотим создать мяч, мы должны создать его с помощью sdf мяча. Уравнение сферы можно представить следующим кодом gsl
float sdSphere(vec3 p,float r)
{
return length(p)-r;
}
Код для блока выглядит следующим образом
float sdBox(vec3 p,vec3 b)
{
vec3 q=abs(p)-b;
return length(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.);
}
Не могу понять, что делать? Не беда, за границей уже есть большие коровыЧасто используемые формулы sdfвсе улажено
Создать блок в SDF
float sdf(vec3 p){
float box=sdBox(p,vec3(.3));
return box;
}
Экран по-прежнему пустой, потому что наш гость, свет, еще не вошел.
легкий шаг
Далее идет персонаж номер один этой статьи - легкий шаг. Прежде чем представить ее, давайте взглянем на трассировку лучей ее лучшего друга.
Во-первых, нам нужно знать, как работает трассировка лучей: задайте положение камеры.eye
, поставить сетку впереди и выстрелить лучом из положения камерыray
, попали в объект через сетку, и каждый пиксель сформированного изображения соответствует каждой точке сетки.
В ray stepping вся сцена определяется серией углов sdf. Чтобы найти границу между сценой и лучом зрения, мы начинаем с положения камеры и перемещаем каждую точку по лучу понемногу, каждый шаг будет определять, находится ли точка внутри определенной поверхности сцены, и если да, то затем Done, что означает, что луч попал во что-то, если нет, то луч продолжает шагать.
На изображении выше p0 — это положение камеры, а синяя линия представляет собой луч. Видно, что первый шаг света p0p1 очень велик, и он также оказывается кратчайшим расстоянием от источника света до поверхности в это время. Хотя точка на поверхности является кратчайшим расстоянием, она не следует направлению линии взгляда, поэтому продолжайте обнаруживать точку p4.
На шейдертое есть одинИнтерактивный пример
Ниже приведена реализация кода GLSL для пошагового луча.
const float EPSILON=.0001;
float rayMarch(vec3 eye,vec3 ray,float end,int maxIter){
float depth=0.;
for(int i=0;i<maxIter;i++){
vec3 pos=eye+depth*ray;
float dist=sdf(pos);
depth+=dist;
if(dist<EPSILON||dist>=end){
break;
}
}
return depth;
}
Создайте луч в основной функции и передайте его алгоритму пошагового луча, чтобы получить кратчайшее расстояние от луча до поверхности.
void main(){
...
vec3 eye=vec3(0.,0.,2.5);
vec3 ray=normalize(vec3(vUv,-eye.z));
float end=5.;
int maxIter=256;
float depth=rayMarch(eye,ray,end,maxIter);
if(depth<end){
vec3 pos=eye+depth*ray;
color=pos;
}
...
}
Соблазненные легкой поступью, появляются дикие блоки!
центральный материал
Текущий блок имеет 2 проблемы: 1. Не центрирован 2. Растянут в направлении оси x
Центрирование + качество растяжки 2 последовательных прогулки
vec2 centerUv(vec2 uv){
uv=2.*uv-1.;
float aspect=uResolution.x/uResolution.y;
uv.x*=aspect;
return uv;
}
void main(){
...
vec2 cUv=centerUv(vUv);
vec3 ray=normalize(vec3(cUv,-eye.z));
...
}
Квадрат мгновенно поплыл к центру экрана, но в это время у нее не было цвета.
Вычислить нормали поверхности
В модели освещения нам нужноВычислить нормаль к поверхности, для придания цвета материалу
vec3 calcNormal(in vec3 p)
{
const float eps=.0001;
const vec2 h=vec2(eps,0);
return normalize(vec3(sdf(p+h.xyy)-sdf(p-h.xyy),
sdf(p+h.yxy)-sdf(p-h.yxy),
sdf(p+h.yyx)-sdf(p-h.yyx)));
}
void main(){
...
if(depth<end){
vec3 pos=eye+depth*ray;
vec3 normal=calcNormal(pos);
color=normal;
}
...
}
В этот момент квадрат окрашивается в синий цвет, но мы еще не можем сказать, что это сплошная фигура.
пошевеливайся
Пусть блок вращается на 360 °, функция 3D-вращения находится непосредственно вgistПросто найди это
uniform float uVelocityBox;
mat4 rotationMatrix(vec3 axis,float angle){
axis=normalize(axis);
float s=sin(angle);
float c=cos(angle);
float oc=1.-c;
return mat4(oc*axis.x*axis.x+c,oc*axis.x*axis.y-axis.z*s,oc*axis.z*axis.x+axis.y*s,0.,
oc*axis.x*axis.y+axis.z*s,oc*axis.y*axis.y+c,oc*axis.y*axis.z-axis.x*s,0.,
oc*axis.z*axis.x-axis.y*s,oc*axis.y*axis.z+axis.x*s,oc*axis.z*axis.z+c,0.,
0.,0.,0.,1.);
}
vec3 rotate(vec3 v,vec3 axis,float angle){
mat4 m=rotationMatrix(axis,angle);
return(m*vec4(v,1.)).xyz;
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(.3));
return box;
}
эффект слияния
Один блок слишком одинок, создайте шар, чтобы сопровождать ее
Как склеить шарик и кубик, нужноsminэта функция
uniform float uProgress;
float smin(float a,float b,float k)
{
float h=clamp(.5+.5*(b-a)/k,0.,1.);
return mix(b,a,h)-k*h*(1.-h);
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(.3));
float sphere=sdSphere(p,.3);
float sBox=smin(box,sphere,.3);
float mixedBox=mix(sBox,box,uProgress);
return mixedBox;
}
ПучокuProgress
значение установлено в 0, они успешно склеиваются
ПучокuProgress
Значение возвращается к 1, и они снова разделяются
Динамическое слияние
Следующим шагом является реализация анимации капания росы, на самом деле, преобразование смещения применяется к графике Fusion
uniform float uAngle;
uniform float uDistance;
uniform float uVelocitySphere;
const float PI=3.14159265359;
float movingSphere(vec3 p,float shape){
float rad=uAngle*PI;
vec3 pos=vec3(cos(rad),sin(rad),0.)*uDistance;
vec3 displacement=pos*fract(uTime*uVelocitySphere);
float gotoCenter=sdSphere(p-displacement,.1);
return smin(shape,gotoCenter,.3);
}
float sdf(vec3 p){
vec3 p1=rotate(p,vec3(1.),uTime*uVelocityBox);
float box=sdBox(p1,vec3(.3));
float sphere=sdSphere(p,.3);
float sBox=smin(box,sphere,.3);
float mixedBox=mix(sBox,box,uProgress);
mixedBox=movingSphere(p,mixedBox);
return mixedBox;
}
текстура маткапа
Текстура по умолчанию слишком землистая? У нас есть крутые стикеры matcap, чтобы помочь
uniform sampler2D uTexture;
vec2 matcap(vec3 eye,vec3 normal){
vec3 reflected=reflect(eye,normal);
float m=2.8284271247461903*sqrt(reflected.z+1.);
return reflected.xy/m+.5;
}
float fresnel(float bias,float scale,float power,vec3 I,vec3 N)
{
return bias+scale*pow(1.+dot(I,N),power);
}
void main(){
...
if(depth<end){
vec3 pos=eye+depth*ray;
vec3 normal=calcNormal(pos);
vec2 matcapUv=matcap(ray,normal);
color=texture2D(uTexture,matcapUv).rgb;
float F=fresnel(0.,.4,3.2,ray,normal);
color=mix(color,bg,F);
}
...
}
После расстановки маткапа и формулы Френеля мгновенно становится круто, не так ли? !
адрес проекта
Эта статья участвует в «Весенней рекрутинговой кампании Nuggets 2021», нажмите, чтобы просмотретьсведения о деятельности