Open7

PlayCanvasでカスタムシェーダーをエンティティに追加する

はがはが

custom-shader.js

// custom-shader.js
const CustomShader = pc.createScript('customshader');

CustomShader.attributes.add('vs', {
    type: 'asset',
    assetType: 'shader',
    title: 'Vertex Shader'
});

CustomShader.attributes.add('fs', {
    type: 'asset',
    assetType: 'shader',
    title: 'Fragment Shader'
});

CustomShader.prototype.initialize = function() {
		// アトリビュートからバーテックスシェーダを取得
    const vertexShader = this.vs.resource;
		// アトリビュートからフラグメントシェーダーを取得
    const fragmentShader = this.fs.resource;
    const shaderDefinition = {
        attributes: {
            aVertexPosition: pc.SEMANTIC_POSITION, // aVertexPosition バーテックスシェーダーで使う値
        },
        vshader: vertexShader,
        fshader: fragmentShader
    };

    // フラグメントシェーダーとバーテックスシェーダーを渡す
    this.shader = new pc.Shader(this.app.graphicsDevice, shaderDefinition);

    // あとは作ってマテリアルをシェーダーをマテリアルに設定するだけ
    this.material = new pc.Material();
    this.material.setShader(this.shader);
    const render = this.entity.render;
    render.meshInstances[0].material = this.material;

    this.timer = 0; // 追加
	this.material.setParameter("uTime", this.timer);
};

CustomShader.prototype.update = function(dt) {
   this.timer += dt;
   if(this.timer > 1) this.timer = 0;
   this.material.setParameter("uTime", this.timer);
};


はがはが

フラグメントシェーダー
fragment shader (custom-shader.frag)

precision highp float;

uniform float uTime;
void main(void)
{
    gl_FragColor = vec4(0, 0, uTime, 1.0);
}

vertex shader(custom-shader.vert)

attribute vec3 aVertexPosition;

uniform mat4 matrix_model;
uniform mat4 matrix_viewProjection;


void main(void)
{
    gl_Position = matrix_viewProjection * matrix_model * vec4(aVertexPosition, 1.0);
}

はがはが

Uniformの一覧(シェーダーで利用できる値)の確認方法について

// this.app.graphicsDevice.scope.variables
pc.app.graphicsDevice.scope.variables

実行結果

uniform value
textureBias 0
source null
areaLightsLutTex1 Texture
areaLightsLutTex2 Texture
shadowAtlasTexture Texture
shadowAtlasParams vec2
cookieAtlasTexture Texture
polygonOffset null
pixelOffset null
weight[0] null
light_radius null
matrix_projection mat4
matrix_projectionSkybox mat4
matrix_view mat4
matrix_view3 mat4
matrix_viewInverse mat4
matrix_viewProjection mat4
projectionFlipY float
view_position vec3
camera_near float
camera_far float
camera_params vec4
tbnBasis 1
fog_color null
fog_start null
fog_end null
fog_density null
matrix_model mat4
matrix_normal mat3
matrix_pose[0] null
texture_poseMap null
texture_poseMapSize null
morph_weights_a null
morph_weights_b null
morphPositionTex null
morphNormalTex null
morph_tex_params null
alpha_ref 0
texture_opacityMap null
light_globalAmbient vec3
exposure 1
skyboxIntensity null
uScreenSize vec4
twoSidedLightingNegScaleFactor null
lightsTexture8 Texture
lightsTextureFloat Texture
lightsTextureInvSize vec4
clusterWorldTexture Texture
clusterPixelsPerCell float
clusterTextureSize vec3
clusterBoundsMin vec3
clusterBoundsDelta vec3
clusterCellsCountByBoundsSize vec3
clusterCellsDot vec3
clusterCellsMax vec3
clusterCompressionLimit0 vec2
POSITION null
TEXCOORD0 null
light0_color vec3
light0_direction vec3
light0_shadowMap Texture
light0_shadowMatrix mat4
light0_shadowParams vec3
light0_shadowIntensity float
light0_radius null
light0_position null
light0_halfWidth null
light0_halfHeight null
light0_innerConeAngle null
light0_outerConeAngle null
light0_cookie null
light0_cookieIntensity null
light0_cookieMatrix null
light0_cookieOffset null
light0_shadowMatrixPalette[0] mat8
light0_shadowCascadeDistances[0] vec4
light0_shadowCascadeCount int
materia_diffuse vec3
material_specular vec3
material_shininess float
material_emissive vec3
material_opacity float
material_occludeSpecularIntensity float
material_reflectivity float
はがはが

Shader Chunk

https://playcanvas.com/editor/scene/492248

pc.script.attribute('materials', 'asset', [], {
    type: 'material'
});
pc.script.attribute('shader', 'asset', [], {
    type: 'shader',
    max: 1
});

var Plasma = pc.createScript('plasma');

Plasma.attributes.add('materials', {type: 'asset', array: true});
Plasma.attributes.add('shader', {type: 'asset'});

Plasma.prototype.initialize = function () {
    // get the shader asset
    var fs = this.shader.resource;

    // update all the materials with the chunk
    for (var i = 0; i < this.materials.length; i++) {
        var material = this.materials[i].resource;
        material.chunks.APIVersion = pc.CHUNKAPI_1_55;
        material.chunks.emissivePS = fs;
        // Force the shader generator to generate UV processing code
        material.diffuseMap = new pc.Texture(this.app.graphicsDevice, {
            width: 1,
            height: 1,
            format: pc.PIXELFORMAT_R8_G8_B8
        });
        material.setParameter('iGlobalTime', 0);
        material.update();
    }

    this.time = 0;
};

Plasma.prototype.update = function (dt) {
    // update the time uniform in the new shader chunk
    this.time += dt;
    for (var i = 0; i < this.materials.length; i++) {
        var material = this.materials[i].resource;
        material.setParameter('iGlobalTime', this.time);
    }
};

fragment shader

uniform float iGlobalTime;

void getEmission() {
    vec2 p = -1.0 + 2.0 * vUv0;

    // main code, *original shader by: 'Plasma' by Viktor Korsun (2011)
    float x = p.x;
    float y = p.y;
    float mov0 = x+y+cos(sin(iGlobalTime)*2.0)*100.+sin(x/100.)*1000.;
    float mov1 = y / 0.9 +  iGlobalTime;
    float mov2 = x / 0.2;
    float c1 = abs(sin(mov1+iGlobalTime)/2.+mov2/2.-mov1-mov2+iGlobalTime);
    float c2 = abs(sin(c1+sin(mov0/1000.+iGlobalTime)+sin(y/40.+iGlobalTime)+sin((x+y)/100.)*3.));
    float c3 = abs(sin(c2+cos(mov1+mov2+c2)+cos(mov2)+sin(x/1000.)));
    dEmission = vec3(c1, c2, c3);
}
はがはが

カスタムポストエフェクトを作成する


class PostEffectRenderer extends pc.PostEffect {
  constructor(props: {
    graphicsDevice: pc.GraphicsDevice;
    fshader: string;
    vshader: string;
  }) {
    const { graphicsDevice, fshader, vshader } = props;

    super(graphicsDevice);
    var attributes = {
      aVertexPosition: pc.SEMANTIC_POSITION,
    };

    this.customShader = new pc.Shader(graphicsDevice, {
      attributes: attributes,
      vshader: vshader,
      fshader: fshader,
    });
  }
  render(inputTarget, outputTarget, rect) {
    var device = this.device;
    var scope = device.scope;

    scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer);
    pc.drawFullscreenQuad(
      device,
      outputTarget,
      this.vertexBuffer,
      this.customShader,
      rect
    );
  }
}

export class CustomPostEffect extends pc.ScriptType {
  initialize() {
    const vshader = this.vert;
    const fshader = this.frag;
    this.time = 0;
    this.effect = new PostEffectRenderer({
      graphicsDevice: this.app.graphicsDevice,
      vshader,
      fshader,
    });
    const queue = this.entity.camera.postEffects;
    queue.addEffect(this.effect);

    this.app.mouse.on(pc.EVENT_MOUSEMOVE, (e) => {
      this.app.graphicsDevice.scope.resolve("iMouse").setValue([e.x, e.y]);
    });

    this.on("attr:frag", () => {
      queue.removeEffect(this.effect);
      console.log(this.vert, this.frag);
      this.effect = new PostEffectRenderer({
        graphicsDevice: this.app.graphicsDevice,
        vshader: this.vert,
        fshader: this.frag,
      });
      queue.addEffect(this.effect);
    });

    this.on("state", function (enabled) {
      if (enabled) {
        queue.addEffect(this.effect);
      } else {
        queue.removeEffect(this.effect);
      }
    });
    this.on("destroy", function () {
      queue.removeEffect(this.effect);
    });
  }
  update(dt) {
    this.time += dt;
    this.app.graphicsDevice.scope.resolve("iTime").setValue(this.time);
    this.app.graphicsDevice.scope.resolve("iTimeDelta").setValue(dt);
  }
}

CustomPostEffect.attributes.add("frag", { type: "string" });
CustomPostEffect.attributes.add("vert", { type: "string" });

v1.58.0〜

class PostEffectRenderer extends pc.PostEffect {
  constructor(props: {
    graphicsDevice: pc.GraphicsDevice;
    fshader: string;
    vshader: string;
  }) {
    const { graphicsDevice, fshader, vshader } = props;

    super(graphicsDevice);
    var attributes = {
      aVertexPosition: pc.SEMANTIC_POSITION,
    };

    this.customShader = new pc.Shader(graphicsDevice, {
      attributes: attributes,
      vshader: vshader,
      fshader: fshader,
    });
  }
  // @ts-ignore
  render(inputTarget, outputTarget, rect) {
    pc.drawFullscreenQuad(
      this.device,
      outputTarget,
      this.vertexBuffer,
      this.customShader,
      rect
    );
  }
}

export class CustomPostEffect extends pc.ScriptType {
  initialize() {
    const vshader = this.vert;
    const fshader = this.frag;
    this.time = 0;
    this.effect = new PostEffectRenderer({
      graphicsDevice: this.app.graphicsDevice,
      vshader,
      fshader,
    });
    const queue = this.entity.camera.postEffects;
    queue.addEffect(this.effect);

    this.app.mouse.on(pc.EVENT_MOUSEMOVE, (e) => {
      this.app.graphicsDevice.scope.resolve("iMouse").setValue([e.x, e.y]);
    });

    this.on("attr:frag", () => {
      queue.removeEffect(this.effect);
      console.log(this.vert, this.frag);
      this.effect = new PostEffectRenderer({
        graphicsDevice: this.app.graphicsDevice,
        vshader: this.vert,
        fshader: this.frag,
      });
      queue.addEffect(this.effect);
    });

    this.on("state", function (enabled) {
      if (enabled) {
        queue.addEffect(this.effect);
      } else {
        queue.removeEffect(this.effect);
      }
    });
    this.on("destroy", function () {
      queue.removeEffect(this.effect);
    });
  }
  update(dt) {
    this.time += dt;
    this.app.graphicsDevice.scope.resolve("iTime").setValue(this.time);
    this.app.graphicsDevice.scope.resolve("iTimeDelta").setValue(dt);
  }
}
pc.registerScript(CustomPostEffect)
CustomPostEffect.attributes.add("frag", { type: "string" });
CustomPostEffect.attributes.add("vert", { type: "string" });
はがはが

シェーダーチャンクを設定できるスクリプト

export class ShaderChunk extends pc.ScriptType {
  initialize() {
    this.time = 0;
    if (!this.fs || !this.chunk) return;
    const material = this.entity.render.material;
    material.chunks[this.chunk] = this.fs;
    if (this.isGenerateDiffuseMap) {
      this.app.assets.loadFromUrl( "/images/bg-rect-512.png", "texture", (err, asset) => {
        const texture = asset?.resource;
        material.diffuseMap = texture
        material.update();
      } );
      //material.diffuseMap = new pc.Texture(this.app.graphicsDevice, {
      //   width: 1,
      //   height: 1,
      //   format: pc.PIXELFORMAT_R8_G8_B8,
      // });
    }
    material.update();
  }

  update(dt) {
    this.time += dt;
    const material = this.entity.render?.material;
    material.setParameter("iTime", this.time);
    material.setParameter("iTimeDelta", dt);
  }
}
ShaderChunk.attributes.add("fs", { type: "string" });
ShaderChunk.attributes.add("isGenerateDiffuseMap", {
  type: "boolean",
  default: true,
});

ShaderChunk.attributes.add("chunk", {
  type: "string",
  enum: [
    { emissivePS: "emissivePS" },
    { diffusePS: "diffusePS" },
    { normalMapPS: "normalMapPS" },
  ],
  default: "emissivePS",
});

pc.registerScript(ShaderChunk);