웹 어셈블리 드로잉2: WebGL

마지막 부분에서 우리는 SVG를 이용하여 산점도와 선도를 그렸다.만약 이것이 약간 보잘것없어 보인다면, 우리는 이번에 웹GL을 사용하려고 시도할 것이다.이 글은 WebGL을 사용하는 모든 세부 사항을 포함하지 않을 것이며, 나는 다른 글도 사용할 수 있지만, 우리는 가능한 한 그것을 최소화할 것이다.

본보기


나는 기본적으로 SVG 그래프의 렌더링 방법을 삭제할 뿐이지만, 모든 같은 속성을 보존한다.지금 나는 원조의'형상'부분을 제거할 것이다. 왜냐하면 이것은 내가 원하는 것보다 좀 복잡하기 때문이다. 그러나 우리는 잠시 후에 다시 연구할 수 있다.
function hyphenCaseToCamelCase(text) {
    return text.replace(/-([a-z])/g, g => g[1].toUpperCase());
}

class WcGraphGl extends HTMLElement {
    #points = [];
    #width = 320;
    #height = 240;
    #xmax = 100;
    #xmin = -100;
    #ymax = 100;
    #ymin = -100;
    #func;
    #step = 1;
    #thickness = 1;
    #continuous = false;

    #defaultSize = 2;
    #defaultColor = "#F00"

    static observedAttributes = ["points", "func", "step", "width", "height", "xmin", "xmax", "ymin", "ymax", "default-size", "default-color", "continuous", "thickness"];
    constructor() {
        super();
        this.bind(this);
    }
    bind(element) {
        element.attachEvents.bind(element);
    }
    render() {
        if (!this.shadowRoot) {
            this.attachShadow({ mode: "open" });
        }

    }
    attachEvents() {

    }
    connectedCallback() {
        this.render();
        this.attachEvents();
    }
    attributeChangedCallback(name, oldValue, newValue) {
        this[hyphenCaseToCamelCase(name)] = newValue;
    }
    set points(value) {
        if (typeof (value) === "string") {
            value = JSON.parse(value);
        }

        value = value.map(p => ({
            x: p[0],
            y: p[1],
            color: p[2] ?? this.#defaultColor,
            size: p[3] ?? this.#defaultSize
        }));

        this.#points = value;

        this.render();
    }
    get points() {
        return this.#points;
    }
    set width(value) {
        this.#width = parseFloat(value);
    }
    get width() {
        return this.#width;
    }
    set height(value) {
        this.#height = parseFloat(value);
    }
    get height() {
        return this.#height;
    }
    set xmax(value) {
        this.#xmax = parseFloat(value);
    }
    get xmax() {
        return this.#xmax;
    }
    set xmin(value) {
        this.#xmin = parseFloat(value);
    }
    get xmin() {
        return this.#xmin;
    }
    set ymax(value) {
        this.#ymax = parseFloat(value);
    }
    get ymax() {
        return this.#ymax;
    }
    set ymin(value) {
        this.#ymin = parseFloat(value);
    }
    get ymin() {
        return this.#ymin;
    }
    set func(value) {
        this.#func = new Function(["x"], value);
        this.render();
    }
    set step(value) {
        this.#step = parseFloat(value);
    }
    set defaultSize(value) {
        this.#defaultSize = parseFloat(value);
    }
    set defaultColor(value) {
        this.#defaultColor = value;
    }
    set continuous(value) {
        this.#continuous = value !== undefined;
    }
    set thickness(value) {
        this.#thickness = parseFloat(value);
    }
}

customElements.define("wc-graph-gl", WcGraphGl);

캔버스를 설치하다


connectedCallback(){
    this.attachShadow({ mode: "open" });
    this.canvas = document.createElement("canvas");
    this.shadowRoot.appendChild(this.canvas);
    this.canvas.height = this.#height;
    this.canvas.width = this.#width;
    this.context = this.canvas.getContext("webgl2");
    this.render();
    this.attachEvents();
}
별거 아니야.우리는 나중에 다시 사용할 수 있도록 connected Callback에 모든 내용을 설정했습니다.나도 웹GL2를 사용하고 있는데, 지금은 모든 브라우저가 그것을 지원해야 한다.

셰이더 템플릿


WebGL에는 많은 샘플이 있으니 시작합시다.
function compileShader(context, text, type) {
    const shader = context.createShader(type);
    context.shaderSource(shader, text);
    context.compileShader(shader);

    if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
        throw new Error(`Failed to compile shader: ${context.getShaderInfoLog(shader)}`);
    }
    return shader;
}

function compileProgram(context, vertexShader, fragmentShader) {
    const program = context.createProgram();
    context.attachShader(program, vertexShader);
    context.attachShader(program, fragmentShader);
    context.linkProgram(program);

    if (!context.getProgramParameter(program, context.LINK_STATUS)) {
        throw new Error(`Failed to compile WebGL program: ${context.getProgramInfoLog(program)}`);
    }

    return program;
}

render(){
  if(!this.context) return;

  const vertexShader = compileShader(this.context, `
    attribute vec2 aVertexPosition;
    void main(){
        gl_Position = vec4(aVertexPosition, 1.0, 1.0);
                gl_PointSize = 10.0;
    }
`, this.context.VERTEX_SHADER);
  const fragmentShader = compileShader(this.context, `
    void main() {
        gl_FragColor = vec4(1.0, 0, 0, 1);
    }
`, this.context.FRAGMENT_SHADER);
  const program = compileProgram(this.context, vertexShader, 
  fragmentShader)
  this.context.useProgram(program);
}

컨텍스트가 없는 경우(DOM에 첨부하기 전에 속성 변경이 렌더링을 트리거하는 경우) 중단합니다.우리는 계속해서 render에서 상하문을 사용하고 기본적인 정점과 부분 착색기를 설정하여 그것들을 컴파일하고 연결하고 컴파일러를 만들었다.우리는 여기서 세부 사항을 토론하지 않겠지만, 이것은 우리가 시작해야 할 최소한의 것이다.여기에 재미있는 새로운 것이 하나 있다gl_PointSize.이렇게 하면 그려진 점의 크기가 조절됩니다.

정점


//setup vertices
const positionBuffer = this.context.createBuffer();
this.context.bindBuffer(this.context.ARRAY_BUFFER, positionBuffer);
const positions = new Float32Array(this.#points.flat());
this.context.bufferData(this.context.ARRAY_BUFFER, positions, this.context.STATIC_DRAW);
const positionLocation = this.context.getAttribLocation(program, "aVertexPosition");
this.context.enableVertexAttribArray(positionLocation);
this.context.vertexAttribPointer(positionLocation, 3, this.context.FLOAT, false, 0, 0);
우리는 점으로 버퍼를 만들고 속성 aVertexPosition 과 연결합니다.또 많은 노래와 춤이 있지만, 우리는 이 모든 것이 필요하다.

그림 그리기


그림을 그리는 것이 적어도 더 쉽다.우리는 화면을 지우고 모든 것을 점으로 그립니다.
this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
this.context.drawArrays(this.context.POINTS, this.#points.length, this.context.UNSIGNED_SHORT, 0);
그러나 몇 가지 이유로 이런 방법은 통하지 않는다.하드코딩this.#points
[
-1.0, -1.0,
1.0, -1.0,
1.0, 1.0,
-1.0, 1.0
]
그리고 길이drawArrays를 4(최종 매개 변수)로 설정하면 캔버스 구석에 4개의 붉은 점이 보인다.이것은 지금까지 당신이 어떤 문법 오류를 범했는지 충분히 알 수 있다.

정점 색상


우선, 우리의 점은 정확한 형상이 아니다.우리는 표시점의 구조 대상 수조가 하나 있지만, 실제로 우리가 필요로 하는 것은 일련의 벡터이다.set points에서 우리는 이 점을 쉽게 바꿀 수 있지만 색깔 문제에 부딪혔다.SVG 그래프에서, 우리는 DOM 색상 (예를 들어 '파란색') 을 사용할 수 있다. 왜냐하면 DOM을 사용해서 그것을 나타낼 수 있기 때문이다.WebGL에서 우리는 이런 것이 없고 Vec4의 부동만 있다.DOM 색상을 RGB로 변환하는 것은 실제로 당신이 상상한 것보다 훨씬 어렵고 보통 해킹을 의미하기 때문에 우리는 이 점을 포기해야 한다.우리가 할 수 있는 것은 일련의 부동점수를 사용하거나 16진수에서 그것을 바꾸는 것이다. (다른 형식에서 바꾸는 것은 매우 중요하다.)이것은 매우 간단하기 때문에 우리는 일련의 부동으로부터 시작합시다.
그래서 { x, y, color, size }[x,y,r,g,b,size]로 바뀌었어요.
하지만 우리에게는 또 다른 문제가 있다.WebGL에 전달할 수 있는 최대 벡터는vec4입니다. 6개의 구성 요소가 있습니다.우리는 그것을 두 개의 벡터로 나누어야 한다.색상과 x, y, 크기.우리도 크기를 나눌 수 있지만, 이것은 더 많은 비용이 발생할 뿐이기 때문에, 나는 그것을 기존의 버퍼에 압축할 것이다. 왜냐하면 우리는 추가 공간이 있기 때문이다.
복구set points:
set points(value) {
    if (typeof (value) === "string") {
        value = JSON.parse(value);
    }
    this.#points = value.map(p => [
        p[0],
        p[1],
        p[6] ?? this.#defaultSize
    ]);
    this.#colors = value.map(p => p.length > 2 ? [
        p[2],
        p[3],
        p[4],
        p[5]
    ] : this.#defaultColor);
    this.render();
}
물건을 입력할 수 있는 다른 방법도 있지만, 나는 평면수 그룹이 가장 좋다고 생각한다.아마도 너는 색깔 그룹을 끼워 넣을 수 있을 것이다.어떤 상황에서도, 우리는 x, y, 크기를 한 그룹에 추출하고, 색을 다른 그룹에 추출하거나, 입력이 색이 없으면 기본값을 사용합니다.이것은 defaultColor도 반드시 4치 그룹이어야 하기 때문에 반드시 그것을 업데이트해야 한다는 것을 의미한다.그리고 알파를 처리해야 하기 때문에 네 번째 요소는 1이어야 한다. 그렇지 않으면 일이 일어나지 않을 때 낙담할 수도 있다.여기에서 많은 검증이 발생할 수 있습니다. 코드 크기에 대한 검증은 하지 않겠지만, 원하신다면 이렇게 할 수 있습니다.
그런 다음 붙여넣기 정점 코드를 복사하여 사용하도록 업데이트해야 합니다this.#colors.
//setup color
const colorBuffer = this.context.createBuffer();
this.context.bindBuffer(this.context.ARRAY_BUFFER, colorBuffer);
const colorsArray = new Float32Array(this.#colors.flat());
this.context.bufferData(this.context.ARRAY_BUFFER, colorsArray, this.context.STATIC_DRAW);
const colorLocation = this.context.getAttribLocation(program, "aVertexColor");
this.context.enableVertexAttribArray(colorLocation);
this.context.vertexAttribPointer(colorLocation, 4, this.context.FLOAT, false, 0, 0);
버퍼가 평평한 계열이기 때문에 this.#colors.flat()this.#points.flat()를 사용하려면 업데이트가 필요합니다.속성을 지정할 때, 색상이 크기 4를 사용하는지 확인하고, 이름을 aVertextColor (또는 부르고 싶은 이름) 로 업데이트하고 aVertexPosition 크기 3으로 업데이트합니다.
그런 다음 셰이더를 업데이트할 수 있습니다.
//Setup WebGL shaders
const vertexShader = compileShader(this.context, `
    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;
    varying mediump vec4 vColor;
    void main(){
        gl_Position = vec4(aVertexPosition.xy, 1.0, 1.0);
        gl_PointSize = aVertexPosition.z;
        vColor = aVertexColor;
    }
`, this.context.VERTEX_SHADER);
const fragmentShader = compileShader(this.context, `
    varying mediump vec4 vColor;
    void main() {
        gl_FragColor = vColor;
    }
`, this.context.FRAGMENT_SHADER);
우리는 3치 위치 (x, y,size) 와 4치가 정점 색깔로 되어 있다.위치의 앞의 두 부분은 4치 최종 위치의 x, y가 되고 세 번째 값z은 점 크기가 된다.그리고 우리는 varying 변수를 사용하여 색을 세그먼트 착색기로 아래로 전송하기만 하면 된다.세션 착색기에서, 우리는 직접 그것을 사용한다.지금은 크기와 색깔이 포인트에 적용되어야 합니다.
그러나 이것은 화면 공간의 좌표이기 때문에 -1:1 범위를 넘어서는 어떤 것도 볼 수 없다.우리는 적응하기 위해 우리의 점을 축소해야 하지만, 다행히도, 이것은 정점 착색기에서 직접 완성할 수 있다.

줌 정점


우리는 경계를 설정해야 한다.이것은 버퍼 설정과 유사합니다.
//setup bounds
const bounds = new Float32Array([this.#xmin, this.#xmax, this.#ymin, this.#ymax]);
const boundsLocation = this.context.getUniformLocation(program, "uBounds");
this.context.uniform4fv(boundsLocation, bounds);
나는 그것들을 네 개의 표량이 아니라 네 개의 벡터에 놓았다. 왜냐하면 이것은 더욱 의미가 있기 때문이다.
이제 정점 착색기의 마력을 살펴보자.
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
uniform vec4 uBounds;
varying mediump vec4 vColor;
float inverseLerp(float a, float b, float v){
    return (v-a)/(b-a);
}
void main(){
    gl_PointSize = aVertexPosition.z;
    gl_Position = vec4(mix(-1.0,1.0,inverseLerp(uBounds.x, uBounds.y, aVertexPosition.x)), mix(-1.0,1.0,inverseLerp(uBounds.z, uBounds.w, aVertexPosition.y)), 1.
0, 1.0);
    vColor = aVertexPosition;
}
가장 멋있는 일 중 하나는 모든 변환이 GPU의 정점 착색기에서 이루어질 수 있지만, 당신이 무엇을 하는지 알 수 있는 데는 시간이 좀 걸린다는 것이다.나는 이미 함수inverseLerp를 소개했는데, 이것은lerp의 역함수이다. 만약 네가 지난번에 내가 말한 windowValue 함수가 바로 이렇게 한 것을 기억한다면.놀랍게도 GLSL에는 역lerp 함수가 존재하지 않기 때문에 반드시 그것을 작성해야 한다.나도 그것inverseLerp이라고 부른다. 왜냐하면 이곳의 도형 환경에서 이해하기 쉽기 때문이다.보시다시피 우리가 사용합니다.x、 ,.y、 ,.z、 ,.w는 부품을 가리킨다.이것은 간략하게 쓰이지만 필요하면 색인 기호[0], [1], [2], [3]를 사용할 수 있다.하지만 더 많아요. 저희도 있어요mix.mix는 내장 함수, 즉lerp 함수이다.우리가 이걸 필요로 하는 이유는 화면 공간이 어떻게 작동하는지 때문이다.이전에 우리는 두 축에 반방향lerp를 만들고 최대 길이를 곱했다.이것은 좌표가 0에서 최대 길이이기 때문이다.그러나 WebGL 화면 공간에서는 -1대 1이므로 다시 줌해야 합니다.기본적으로, 우리는 원시 좌표를 얻어 그것을 규격화한 다음에, 이러한 규격화된 좌표를 사용하여 다시 화포를 투영한다.나는 반드시 간소화해야 한다고 확신한다. 특히 한 걸음에 벡터 곱셈을 완성해야 한다. 그러므로 만약 당신이 발견한다면 나에게 알려주십시오.나머지는 모두 똑같습니다. 우리는 색깔과 사용z 좌표의 크기를 통과합니다.

SVG와 비슷해 보입니다.픽셀 크기는 SVG의 1/2입니다.DPI 비례 인자가 무엇인지 확실하지는 않지만, 같은 네트워크 크기를 얻으려면 2배가 더 필요하다.
마지막으로 함수 그림 그리기 유틸리티를 추가합니다.기본 색상이 올바른지 확인하는 것부터 시작할 수 있습니다.
set defaultColor(value) {
    if(typeof(value) === "string"){
        this.#defaultColor = JSON.parse(value);
    } else {
        this.#defaultColor = value;
    }
}
우리는 그것이 문자열인지 아닌지를 분석합니다. 그렇지 않으면 사용자가 전달과 그룹으로 무엇을 하는지 알고 있다고 가정합니다.그런 다음 코드를 추가하여 함수를 점으로 렌더링합니다.
let points;
let colors;
if(this.#func){
    points = [];
    colors = [];
    for (let x = this.#xmin; x < this.#xmax; x += this.#step) {
        const y = this.#func(x);
        points.push([x, y, this.#defaultSize]);
        colors.push(this.#defaultColor);
    }
} else {
    points = this.#points;
    colors = this.#colors;
}
우리는 버퍼를 연결할 때 colorsthis.#colors 의 데이터가 아닌 points 의 변수를 조정해야 한다.

연속선


이 부분은 매우 간단하다. 우리는 단지 두 번, 한 번은 선을 사용하고 한 번은 점을 쓴다.
//draw
this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
if(this.#continuous){
        this.context.lineWidth(this.#thickness);
    this.context.drawArrays(this.context.LINE_STRIP, 0, points.length);
}
this.context.drawArrays(this.context.POINTS, 0, points.length);
유감스럽게도 선가중치를 설정할 수 있지만 플랫폼에서 실제로 지원할 수 없습니다.보아하니 WebGL은 선폭을 존중할 필요가 없다. 적어도 내 기계에는 필요없다. 나는 항상 1.0 두께의 선을 얻을 수 있다.지원 여부를 조회할 수 있습니다.
console.log(this.context.getParameter(this.context.ALIASED_LINE_WIDTH_RANGE));
이것은 내가 지지하는 최소 선폭과 최대 선폭this.#points을 얻을 것이다.이것은 아주 엉망이다. 그것을 복원하기 위해서, 우리는 스스로 울타리화선을 연구해야 할지도 모르지만, 이것은 다른 날의 일이다.
또 다른 것은 이 선들의 행동이 달라질 수 있다는 것이다.지난 글에서 연속선을 단일색으로 설정하는 것을 어떻게 결정했는지 기억하십니까? 그러면 모든 내용에 선분을 사용하지 않습니까?WebGL에서 우리는 상반된 문제에 부딪혔다.기본적으로 선은 점에 따라 색상을 전환합니다. 그러나 실선을 사용하려면 모든 선의 색상이 동일하도록 새 색상 버퍼를 만들어야 합니다. 또는 "이것이 선이므로 이 색상으로 설정하기만 하면 됩니다"라는 일관된 매개 변수를 셰이더 프로그램에 추가해야 합니다.나는 그렇게 하지 않을 것이다. 그러나 너는 이렇게 할 수 있다.

드로잉 설명서


그래서 우리는 SVG 실현에 대한 무용지물이 있는데 이것은 WebGL에서 재미있는 문제이다.보아하니, 우리는 이 버퍼 구역, 제복, 착색기 프로그램을 모두 설정했는데, 단지 하나의 물건, 즉 점을 과장하기 위해서였다.그런데 우리는 어떻게 다른 것을 첨가합니까?기본적으로 우리는 반드시 다시 한 번 해야 한다.하지만 우리는 적어도 지름길을 갈 수 있다.새 착색기 프로그램을 만들어서 참고선을 그리는 것이 아니라 기존의 착색기 프로그램을 반복해서 사용할 수 있습니다. 왜냐하면 이것은 기본적인 선 착색기일 뿐입니다.
//draw guides
{
    const positionBuffer = this.context.createBuffer();
    this.context.bindBuffer(this.context.ARRAY_BUFFER, positionBuffer);
    const positions = new Float32Array([
        (this.#xmax + this.#xmin) / 2, this.#ymin, 10,
        (this.#xmax + this.#xmin) / 2, this.#ymax, 10,
        this.#xmin, (this.#ymax + this.#ymin) / 2, 10,
        this.#xmax, (this.#ymax + this.#ymin) / 2, 10
    ]);
    this.context.bufferData(this.context.ARRAY_BUFFER, positions, this.context.STATIC_D
    const positionLocation = this.context.getAttribLocation(program, "aVertexPosition")
    this.context.enableVertexAttribArray(positionLocation);
    this.context.vertexAttribPointer(positionLocation, 3, this.context.FLOAT, false, 0,
    const colorBuffer = this.context.createBuffer();
    this.context.bindBuffer(this.context.ARRAY_BUFFER, colorBuffer);
    const colorsArray = new Float32Array([
        0,0,0,1,
        0,0,0,1,
        0,0,0,1,
        0,0,0,1
    ]);
    this.context.bufferData(this.context.ARRAY_BUFFER, colorsArray, this.context.STATIC
    const colorLocation = this.context.getAttribLocation(program, "aVertexColor");
    this.context.enableVertexAttribArray(colorLocation);
    this.context.vertexAttribPointer(colorLocation, 4, this.context.FLOAT, false, 0, 0)
    const bounds = new Float32Array([this.#xmin, this.#xmax, this.#ymin, this.#ymax]);
    const boundsLocation = this.context.getUniformLocation(program, "uBounds");
    this.context.uniform4fv(boundsLocation, bounds);
    this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
    this.context.drawArrays(this.context.LINES, 0, points.length);
}

우리는 [1.0, 1.0]를 설정한 후에 점의 속성을 설정하기 전에 그것을 놓을 수 있다.곱슬곱슬한 블록은 고의적인 것이기 때문에 나는 변수명을 다시 쓸 수 있다.우리는 각 축척의 중간점에 4개의 점을 설정하고 정점에 4가지 색을 제공한다(이 예는 검은색이다).그리고 우리는 useProgram로 그림을 그릴 수 있는데, 그것은 gl.LINES와 비슷하지만, 그것은 연속적이지 않다.버퍼 gl.LINE_STRIP 를 이동할 방법이 필요합니다. 그렇지 않으면 버퍼를 그릴 때 버퍼를 지우지 않고 그릴 것입니다.
이제 SVG 그래픽과 거의 비슷하게 시작하겠습니다.

결론


이번 코드는 더욱 크고, 더욱 복잡하며, 심지어는 획의 너비와 형상 등 일부 특성이 부족하다.우리는 여전히 그것들을 추가할 수 있지만, 이것은 더 많은 일이 될 것이다.다른 한편, 모든 무거운 짐이 GPU로 옮겨져 속도가 매우 빠르다.이것은 정말 네가 어떻게 계속할 것인지에 달려 있지만, 너의 틀이 너를 위해 결정하게 하지 마라.

좋은 웹페이지 즐겨찾기