WebGL 3D 엔진 처음부터 10부분: 광택 조명
73337 단어 webgl3dvanillajscomputegraphics
예를 들어 여기에 금색 크리스마스 장식품의 예가 있다.
기존의 구체와 달리 점차적인 색뿐만 아니라 하이라이트도 있어 장면의 빛이 카메라로 반사됩니다.이것이 바로 거울면 반사 조명이 시뮬레이션을 시도한 것이다.
활용단어참조
가장 일반적인 방법은 Phong 조명을 사용하는 것입니다.그것은 우리가 만반사 조명을 사용할 때와 크게 다르지 않지만, 카메라 위치라는 추가 부품이 필요하다.따라서 우리는
getGlobalUniforms
에 우리의 제복을 설치할 것이다.const cameraPosition = this.cameras.default.getPosition();
const cameraLocation = this.context.getUniformLocation(program, "uCamera");
this.context.uniform3fv(cameraLocation, Float32Array.from(cameraPosition));
세그먼트 셰이더에서 액세스할 수 있습니다.uniform mediump vec3 uCamera;
그런 다음 위치를 색상으로 내보내는 테스트를 수행합니다.카메라가 2, 0, 0이면 객체가 빨간색이어야 합니다.다음은 우리가 해야 할 일이다.
1) 카메라의 방향(표준화) 벡터 가져오기
2) 라이트의 방향 찾기
3) 라이트의 대칭 이동(라이트에서)을 얻어 서피스 법선에 반사합니다.
4) 카메라의 방향을 반사 벡터로 표시하여 카메라를 향한 빛의 크기를 얻는다.
5) 색상 곱하기
첫 번째 단계와 두 번째 단계는 상당히 간단하니, 너는 약간의 감법만 하면 된다.
반성하려면 약간의 수학 지식이 필요하다.GLSL은 기꺼이 우리에게 이 기존의
reflect(vector, normal)
를 제공하지만, 만약 당신이 스스로 계산할 필요가 있다면 그것은 다음과 같다.export function reflect(vec, normal){
return [
vec[0] - 2 * dotVector(vec, normal) * normal[0],
vec[1] - 2 * dotVector(vec, normal) * normal[1],
vec[2] - 2 * dotVector(vec, normal) * normal[2],
];
}
나머지는 단도직입적으로 하겠습니다.기존의 픽셀당 셰이더를 사용하면 미러 반사를 추가하기 위해 수정할 수 있습니다.마지막으로 우리는 다음과 같은 착색기를 얻었다.
precision mediump float;
varying vec4 vColor;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vPosition;
uniform mat4 uLight1;
uniform sampler2D uSampler;
uniform vec3 uCamera;
void main() {
bool isPoint = uLight1[3][3] == 1.0;
vec3 normal = normalize(vNormal);
if(isPoint) {
//point light + color
vec3 toLight = normalize(uLight1[0].xyz - vPosition);
float light = dot(normal, toLight);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 reflectedLightDir = reflect(-toLight, normal);
float specularLight = dot(reflectedLightDir, toCameraDir);
gl_FragColor = vec4(specularLight, specularLight, specularLight, 1.0);
//gl_FragColor = vColor * uLight1[2] * vec4(light, light, light, 1);
} else {
//directional light + color
float light = dot(normal, uLight1[1].xyz);
gl_FragColor = vColor * uLight1[2] * vec4(light, light, light, 1);
}
}
전역 정밀도와 사전 규범화vNormal
도 추가하여 조작을 간소화하였음을 주의하십시오.만약 우리가 그것을 본다면, 그것은 우리가 그것을 보는 방식에 따라 변화하는 광점을 만들어 낼 것 같다.
광택도
비록 우리는 지금 거울 조명을 가지고 있지만, 우리는 그것을 약간 조절할 수 있기를 바란다.광택이나 매끄러운 정도를 정확하게 바꿀 수 있는 매개 변수가 필요하다는 것이다.전형적인 방법은 광택도치를 도입한 후에 거울면의 반사광의 크기를 이 출력으로 하는 것이다.규모가 더 지수적이기 때문일 것 같습니다.
gloss 파라미터를 가져오기 위해서
material.js
추가 파라미터를 도입하기 위해 변경할 것입니다.//material.js
export class Material {
#program;
#textures;
#uniforms;
constructor(material){
this.#program = material.program;
this.#textures = material.textures ?? [];
this.#uniforms = material.uniforms;
}
get program(){
return this.#program;
}
get textures(){
return this.#textures;
}
get uniforms(){
return this.#unforms;
}
}
그것은 실제로는 단지 하나의 인터페이스일 뿐이다.uniforms
는 단지 하나의 대상이 될 것이다. 그 키는 이름과 같고 값은 제복의 값과 같다.우리는 제복의 사이즈를 걱정할 필요가 없도록 그것도 설치할 것이다.이를 위해 다음과 같은 기능이 있습니다.export function autoBindUniform(context, program, uniformName, value){
const location = context.getUniformLocation(program, uniformName);
if (!location) return;
if (Array.isArray(value)) {
switch (value.length) {
case 1: {
this.context.uniform1fv(location, value);
}
case 2: {
this.context.uniform2fv(location, value);
}
case 3: {
this.context.uniform3fv(location, value);
}
case 4: {
this.context.uniform4fv(location, value);
}
default: {
console.error(`Invalid dimension for binding uniforms. ${uniformName} with value of length ${value.length}`);
}
}
} else {
context.uniform1f(location, value);
}
}
이것도 행렬로 확장할 수 있다.현재 bindMaterial
에서 우리는 모든 관련 통일된 매개 변수를 연결하는 새로운 줄을 추가하기만 하면 된다.if(material.uniforms){
Object.entries(material.uniforms).forEach(([uniformName, uniformValue]) => {
autoBindUniform(this.context, material.program,uniformName, uniformValue);
});
}
다음과 같이 자료를 업데이트할 수 있습니다.pixelShadedSpecular: new Material({
program: await loadProgram(this.context, "shaders/pixel-shaded-specular"),
uniforms: {
gloss: 4.0
}
}),
이것은 우리로 하여금 매우 쉽게 값을 바꾸게 할 것이다.마지막으로 착색기:precision mediump float;
varying vec4 vColor;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vPosition;
uniform mat4 uLight1;
uniform sampler2D uSampler;
uniform vec3 uCamera;
uniform float gloss;
void main() {
bool isPoint = uLight1[3][3] == 1.0;
vec3 normal = normalize(vNormal);
if(isPoint) {
//point light + color
vec3 toLight = normalize(uLight1[0].xyz - vPosition);
float light = dot(normal, toLight);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 reflectedLightDir = reflect(-toLight, normal);
float baseSpecular = clamp(dot(reflectedLightDir, toCameraDir), 0.0, 1.0);
float specularLight = pow(baseSpecular, gloss);
gl_FragColor = (uLight1[2] * vec4(specularLight, specularLight, specularLight, 1.0)) + (vColor * uLight1[2] * vec4(light, light, light, 1));
} else {
//directional light + color
float light = dot(normal, uLight1[1].xyz);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 reflectedLightDir = reflect(-uLight1[1].xyz, normal);
float baseSpecular = clamp(dot(reflectedLightDir, toCameraDir), 0.0, 1.0);
float specularLight = pow(baseSpecular, gloss);
gl_FragColor = (uLight1[2] * vec4(specularLight, specularLight, specularLight, 1.0)) + (vColor * uLight1[2] * vec4(light, light, light, 1));
}
}
clamp
에 baseSpecular
에 추가되었습니다. 이것이 없으면 빛이 마이너스인 상황에서 이상한 행동을 할 수 있습니다.최종 색 출력을 처리하기 위해서 우리는 두 개의 불빛을 함께 추가할 것이다.평행광에 대해 우리는 방향만 사용한다.너는 등불이 불어서 망가진 것을 볼 수 있다.이것은 구체와 다른 물체에 있어서는 이미 충분하지만 평면에 있어서는 그다지 좋지 않다.
이런 상황에서 튀어나온 부분은 여전히 원형이다.
풍보림
평면 문제 해결을 돕는 Phong 변형을 Blinn Phong이라고 합니다.그것의 계산 방식은 다소 다르고 더욱 간단하다.
1) 카메라의 방향(표준화) 벡터 가져오기
2) 반 벡터, 즉 toLight+toCamera(표준화) 얻기
3) 법선으로 반 벡터를 찍어 카메라를 향한 빛의 크기를 얻는다.
4) 색상 곱하기
나는 이것이 왜 효과가 있는지 잘 모르겠지만, 확실히 효과가 있다.다음은 코드입니다.
precision mediump float;
varying vec4 vColor;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vPosition;
uniform mat4 uLight1;
uniform sampler2D uSampler;
uniform vec3 uCamera;
uniform float gloss;
void main() {
bool isPoint = uLight1[3][3] == 1.0;
vec3 normal = normalize(vNormal);
if(isPoint) {
//point light + color
vec3 toLightDir = normalize(uLight1[0].xyz - vPosition);
float light = dot(normal, toLightDir);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 halfVector = normalize(toLightDir + toCameraDir);
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0);
float specularLight = pow(baseSpecular, gloss);
gl_FragColor = (uLight1[2] * vec4(specularLight, specularLight, specularLight, 1.0)) + (vColor * uLight1[2] * vec4(light, light, light, 1));
} else {
//directional light + color
float light = dot(normal, uLight1[1].xyz);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 halfVector = normalize(uLight1[1].xyz + toCameraDir);
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0);
float specularLight = pow(baseSpecular, gloss);
gl_FragColor = (uLight1[2] * vec4(specularLight, specularLight, specularLight, 1.0)) + (vColor * uLight1[2] * vec4(light, light, light, 1));
}
}
사시 각도에서 하이라이트의 형상이 약간 늘어나는 것이 바로 우리가 원하는 것이다.구체가 똑같아 보인다.
허물
Blinn Phong은 단점이 하나 있습니다.만약 우리가 구체를 충분히 멀리 회전한다면, 우리는 거울면 반사 고광선이 실제로 뒷면으로 잘라지는 것을 볼 수 있다.
광선 자체가 서피스를 등지고 있을 경우 광선 반사 광선의 값을 계산하지 않고 이 문제를 해결할 수 있습니다.
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0) * float(light > 0.0);
빛과 표면 사이의 점적이 음수라면 전체 물체가 0이 된다는 것이다.이것이 무엇인지 말하기는 어렵지만, 그것은 이전의 각도와 같지만, 이상한 결함은 없다.
거울면 반사
우리가 할 수 있는 또 다른 일은 재질 전체가 거울로 반사되는 값을 가질 필요가 없다는 것이다. 우리는 texel에 따라 할 수 있다.먼저 텍스쳐 세그먼트 셰이더의 복사본을 사용하여 입방체를 만들었습니다.
나는 Wolfenstien 3D에서 도출한 무늬 자체(내가 어떻게 이 점을 해냈는지 더 알고 싶다면 Triad 원본 포트를 구축하는 블로그를 읽을 수 있다:https://github.com/ndesmic/webrott/blob/master/part9/walls3.md).
이것은 복잡한 도구가 필요 없이 작고 수동으로 편집하기 쉬운 것을 원하기 때문이다.우리가 해야 할 일은 점액을 광택이 나고 촉촉하게 보이게 하는 것이다. 돌은 가벼운 광택을 낼 것이다.이것은 결코 정확한 과학이 아니다. 왜냐하면 우수한 화소 예술로서 색깔을 혼합하는 사용으로 인해 무엇이 무엇인지 정확하게 분별하기 어렵다.MS paint를 사용하여 수동으로 편집한 후 다음을 수행합니다.
그레이스케일 png
slimewall.specmap.png
.먼저 텍스쳐에 분산 조명을 추가합니다.precision mediump float;
varying vec4 vColor;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vPosition;
uniform mat4 uLight1;
uniform sampler2D uSampler;
void main() {
bool isPoint = uLight1[3][3] == 1.0;
if(isPoint) {
//point light + color
vec4 color = texture2D(uSampler, vUV);
mediump vec3 toLight = normalize(uLight1[0].xyz - vPosition);
mediump float light = dot(normalize(vNormal), toLight);
gl_FragColor = color * uLight1[2] * vec4(light, light, light, 1);
} else {
//directional light + color
vec4 color = texture2D(uSampler, vUV);
mediump float light = dot(normalize(vNormal), uLight1[1].xyz);
gl_FragColor = color * uLight1[2] * vec4(light, light, light, 1);
}
}
색상을 샘플링의 texel 색상으로만 바꿉니다.여러 텍스쳐 사용
여러 텍스쳐를 사용하려면 약간의 변경이 필요합니다.먼저 실제로 로드해야 합니다.
specMapped: new Material({
program: await loadProgram(this.context, "shaders/spec-mapped"),
textures: [
await loadTexture(this.context, "./img/slimewall.png"),
await loadTexture(this.context, "./img/slimewall.specmap.png")
]
})
주의해라, 이것은 병행화할 수 있지만, 나는 좀 게으르다.그런 다음 bindMaterial
에서 텍스쳐 바인딩을 수정합니다.bindMaterial(material){
this.context.useProgram(material.program);
material.textures.forEach((tex, index) => {
const location = this.context.getUniformLocation(material.program, `uSampler${index}`);
if(!location) return;
this.context.uniform1i(location, index);
this.context.activeTexture(this.context[`TEXTURE${index}`]);
this.context.bindTexture(this.context.TEXTURE_2D, tex);
});
if(material.uniforms){
Object.entries(material.uniforms).forEach(([uniformName, uniformValue]) => {
autoBindUniform(this.context, material.program, uniformName, uniformValue);
});
}
}
지금까지 샘플러를 위한 기본값을 사용해 왔습니다.이번에 우리는 실제로 제복을 묶을 것이다.다른 제복처럼.그것은 무늬의 단위를 나타내는 정수이다. 만약 무늬의 한 장의 내용을 기억하고 있다면, 최대 32개가 될 수 있다.그래서 우리가 한 것은 단지 샘플러가 어느 무늬조 인덱스에서 샘플을 채취해야 하는지(기본값은 0)를 말하는 것이다.착색기가 실제로 사용하지 않도록 하는 단축키도 있다. location == null
다음에 우리는 활동 무늬, 즉 귀속할 슬롯을 설정합니다.그것들은 TEXTURE0
과 TEXTURE1
라는 매거명이 있기 때문에 우리는 문자열 연결을 해서 그것들을 배열했다.그리고 우리는 귀속시켰다. 즉, "이 무늬를 현재 슬롯과 연결한다"는 것이다.이러한 모든 작업이 완료되면 uSamplerX
를 사용합니다. 여기서 X는 0과 32 사이의 숫자로 슬롯의 텍스쳐를 사용하고 재료에 지정된 순서대로 텍스쳐를 바인딩합니다.이것은 가장 의미 있는 방식은 아니지만, 그것은 매우 효과적이다.shader land로 돌아가면 두 개의 샘플러를 사용할 수 있습니다.
uniform sampler2D uSampler0;
uniform sampler2D uSampler1;
그것의 작업을 테스트하는 좋은 방법은 그것들 사이를 왔다 갔다 하는 것이다.uSampler0
는 이 예에서 슬림볼 무늬이고 uSampler1
는 거울면 반사 스티커이다.현재 우리는 모든 texel에 새로운 정보를 추가할 수 있기 때문에 다양한 멋진 일을 할 수 있다.이 경우 미러 반사 맵에는 텍스쳐의 미러 반사도 값(즉, 얼마나 많은 미러 반사가 적용되는지)이 포함됩니다.거울 반사 스티커는 그레이스케일 png입니다. 이것은 우리가 여전히 4개의 색 채널을 가지고 있지만, 모두 중복된다는 것을 의미합니다.효율은 그리 높지 않지만, 지금은 괜찮다.간단하게 보기 위해서 우리는 빨간색 구성 요소만 사용할 수 있다.
precision mediump float;
varying vec4 vColor;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vPosition;
uniform mat4 uLight1;
uniform vec3 uCamera;
uniform float gloss;
uniform sampler2D uSampler0;
uniform sampler2D uSampler1;
void main() {
bool isPoint = uLight1[3][3] == 1.0;
vec3 normal = normalize(vNormal);
if(isPoint) {
//point light + color
vec4 color = texture2D(uSampler0, vUV);
float specularity = texture2D(uSampler1, vUV).r;
vec3 toLightDir = normalize(uLight1[0].xyz - vPosition);
float diffuseMagnitude = dot(normal, toLightDir);
vec4 diffuseLight = color * uLight1[2] * vec4(diffuseMagnitude, diffuseMagnitude, diffuseMagnitude, 1);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 halfVector = normalize(toLightDir + toCameraDir);
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0) * float(clamp(diffuseMagnitude, 0.0, 1.0) > 0.0);
float specularMagnitude = pow(baseSpecular, gloss);
vec4 specularLight = uLight1[2] * specularity * vec4(specularMagnitude, specularMagnitude, specularMagnitude, 1.0);
gl_FragColor = diffuseLight + specularLight;
} else {
//directional light + color
vec4 color = texture2D(uSampler0, vUV);
float specularity = texture2D(uSampler1, vUV).r;
float diffuseMagnitude = dot(normal, uLight1[1].xyz);
vec4 diffuseLight = color * uLight1[2] * vec4(diffuseMagnitude, diffuseMagnitude, diffuseMagnitude, 1);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 halfVector = normalize(uLight1[1].xyz + toCameraDir);
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0);
float specularMagnitude = pow(baseSpecular, gloss);
vec4 specularLight = uLight1[2] * specularity * vec4(specularMagnitude, specularMagnitude, specularMagnitude, 1.0);
gl_FragColor = specularLight + diffuseLight;
}
}
내가 specularLight
결정할 때, 색깔에 이 양을 곱하면 거울 반사 스티커에서 나온다는 것을 알 수 있을 것이다.즉, 어두운 영역0
에는 미러 반사가 적용되지 않고 밝은 영역에는 미러 반사가 적용됩니다.응, 이건 완전히 내가 기대한 건 아니지만, 좀 멋있어.습점액이라기보다는kintsugi이다.
텍스쳐 방향 고정
또 하나 주의해야 할 것은 무늬가 위로 향하는 것이다.이것은 웹과 OpenGL 사이의 이미지가 아래에서 위로 올라가는지 위에서 아래로 내려가는지에 대한 불일치입니다.정상적인 네트워크 모드가 위에서 아래로 향하기 때문에
bootGpu
에 다음과 같은 내용을 추가할 수 있습니다.this.context.pixelStorei(this.context.UNPACK_FLIP_Y_WEBGL, true);
텍스쳐를 다른 방향으로 정위합니다.Glossmaps
Glossmaps는 거울 반사 스티커와 똑같습니다. 거울 반사 지수 데이터만 포함합니다. (우리는 줄곧
gloss
라고 불렀습니다.이것도 조잡도 스티커라고 할 수 있는데 조잡도는 광택도와 상반된다(거울면 반사 고광선이 더욱 분산된다).이 점에 대해, 나는 0.0에서 1.0까지의 선형 점차적인 그래디언트인glossmap을 가진 컬러 사각형을 시도할 것이다.
착색기:
precision mediump float;
varying vec4 vColor;
varying vec2 vUV;
varying vec3 vNormal;
varying vec3 vPosition;
uniform mat4 uLight1;
uniform vec3 uCamera;
uniform float specularity;
uniform sampler2D uSampler0;
void main() {
bool isPoint = uLight1[3][3] == 1.0;
vec3 normal = normalize(vNormal);
if(isPoint) {
//point light + color
float gloss = exp2(texture2D(uSampler0, vUV).r * 6.0) + 2.0;
vec3 toLightDir = normalize(uLight1[0].xyz - vPosition);
float diffuseMagnitude = dot(normal, toLightDir);
vec4 diffuseLight = vColor * uLight1[2] * vec4(diffuseMagnitude, diffuseMagnitude, diffuseMagnitude, 1);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 halfVector = normalize(toLightDir + toCameraDir);
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0) * float(clamp(diffuseMagnitude, 0.0, 1.0) > 0.0);
float specularMagnitude = pow(baseSpecular, gloss);
vec4 specularLight = uLight1[2] * specularity * vec4(specularMagnitude, specularMagnitude, specularMagnitude, 1.0);
gl_FragColor = diffuseLight + specularLight;
} else {
//directional light + color
float gloss = exp2(texture2D(uSampler0, vUV).r * 6.0) + 2.0;
float diffuseMagnitude = dot(normal, uLight1[1].xyz);
vec4 diffuseLight = vColor * uLight1[2] * vec4(diffuseMagnitude, diffuseMagnitude, diffuseMagnitude, 1);
vec3 toCameraDir = normalize(uCamera - vPosition);
vec3 halfVector = normalize(uLight1[1].xyz + toCameraDir);
float baseSpecular = clamp(dot(halfVector, normal), 0.0, 1.0);
float specularMagnitude = pow(baseSpecular, gloss);
vec4 specularLight = uLight1[2] * specularity * vec4(specularMagnitude, specularMagnitude, specularMagnitude, 1.0);
gl_FragColor = specularLight + diffuseLight;
}
}
이것은 거의 거울면 반사 착색기와 같지만 광택은 이미 거울면 반사로 바뀌었다.내가 한 특별한 일은 광택치를 다시 비추는 것이다.gloss는 하나의 지수이기 때문에 우리가 원하는 것은 일반적인 0-1 범위이기 때문에 이것은 지수가 비치는 것이다.내가 사용한 정확한 공식은 프레아 호르머에서 나온 것이다. 이를 어떻게 할 것인가에 대한 건의로.이런 간단한 테스트에서 정확한 효과는 직각적으로 판단하기 쉽지 않지만 효과적이다.너는 오른쪽이 더욱 매끄럽다는 것을 알아차릴 것이다.
물론 같은 착색기에서 거울면 반사 스티커와glossmaps를 조합할 수 있다.'만반사 스티커'라고 불리는 일반적인 무늬도 볼 수 있습니다. 왜냐하면 그들은 만반사 조명의 색깔을 매우 직접적으로 결정하기 때문입니다.
이것은 내가 예상한 것보다 훨씬 많다.도움이 되었으면 좋겠습니다. 다음에 또 만나요!여기서 코드를 찾을 수 있습니다: https://github.com/ndesmic/geogl/tree/v7
Reference
이 문제에 관하여(WebGL 3D 엔진 처음부터 10부분: 광택 조명), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/ndesmic/webgl-3d-engine-from-scratch-part-10-specular-lighting-1gl8텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)