Laya自定义Shader实现Planar阴影(一)

最近有个需求,需要把物体的影子投影在某一个平面上,类似这样的效果:
在这里插入图片描述
打开光照阴影之后,发现在小米测试机上只有45帧,于是决定使用自定义shader,增加一个pass通道渲染阴影。在实现前,有两点需要事先说明的的:
1、这里预设接受投影得平面是已知的,并且平行于xz平面,因此阴影的y坐标是已知的。如果想了解投影到任意平面的办法,可以参考https://zhuanlan.zhihu.com/p/97267754,本文不做讨论。
2、上图中,超出平面的阴影是不显示的。但因为laya目前不支持Stencil(Stencil实现方法:Laya实现Stencil模板测试),所以“不渲染超出平面部分的阴影”这个功能在laya中无法实现。
接下来开始分析实现原理,并给出完整的shader。
在这里插入图片描述

如上图,P点投影到平面上的点为S,已知P点和光线方向以及 S y S_{y} Sy(因为该平面平行于x轴和z轴组成的平面),求 S x S_{x} Sx S z S_{z} Sz。我们可以在xy平面和zy平面分别求出 S x S_{x} Sx S z S_{z} Sz
在这里插入图片描述

上图是在xy平面上P与S的关系,L是光线方向(注意,既然是方向,就表示L是单位向量)。由此可得等式: P + d L = S P+dL=S P+dL=S
其中d是长度。已知 S y S_{y} Sy,只要计算出d,就能得到 S x S_{x} Sx
可以跟据: P y + d ⋅ L y = S y P_{y}+d·L_{y}=S_{y} Py+dLy=Sy求得:
d = ( S y − P y ) / L y d=(S_{y}-P_{y})/L_{y} d=(SyPy)/Ly
所以:
S x = P y + d ⋅ L x = P x + ( S y − P y ) ⋅ L x / L y S_{x}=P_{y}+d·L_{x}=P_{x}+(S_{y}-P_{y})·L_{x}/L_{y} Sx=Py+dLx=Px+(SyPy)Lx/Ly
同理可得:
S z = P y + d ⋅ L z = P z + ( S y − P y ) ⋅ L z / L y S_{z}=P_{y}+d·L_{z}=P_{z}+(S_{y}-P_{y})·L_{z}/L_{y} Sz=Py+dLz=Pz+(SyPy)Lz/Ly
接下来构建场景:
在这里插入图片描述

接着定义材质以及shader:
PlanarShadowMaterial继承UnlitMaterial,即物体本身使用UnlitMaterial渲染,也可以跟据需求继承BlinnPhongMaterial或其它材质。但如果继承材质基类Material,则需要自己实现贴图等材质属性的赋值,会比较麻烦。

import { UnlitMaterial } from "laya/d3/core/material/UnlitMaterial";
import { VertexMesh } from "laya/d3/graphics/Vertex/VertexMesh";
import { Shader3D } from "laya/d3/shader/Shader3D";
import { SubShader } from "laya/d3/shader/SubShader";
import UnlitVS from "../../../../../libs/laya/d3/shader/files/Unlit.vs";
import UnlitPS from "../../../../../libs/laya/d3/shader/files/Unlit.fs";

export default class PlanarShadowMaterial extends UnlitMaterial {

    static PLANE_HEIGHT = Shader3D.propertyNameToID("u_Plane_Height");

    constructor() {
        super();
        this.setShaderName("PlanarShadow");
    }

    public set planeHeight(val: number) {
        this.shaderData.setNumber(PlanarShadowMaterial.PLANE_HEIGHT, val);
    }

    public get planeHeight(): number {
        return this.shaderData.getNumber(PlanarShadowMaterial.PLANE_HEIGHT);
    }

    public static __init__() {
        var vs = `
            #if defined(GL_FRAGMENT_PRECISION_HIGH)
                precision highp float;
            #else
                precision mediump float;
            #endif
            #include "Lighting.glsl";
            
            attribute vec4 a_Position;
            uniform mat4 u_WorldMat;
            uniform mat4 u_ViewProjection;
            uniform float u_Plane_Height;
            uniform DirectionLight u_SunLight;
            
            #ifdef BONE
                const int c_MaxBoneCount = 24;
                attribute vec4 a_BoneIndices;
                attribute vec4 a_BoneWeights;
                uniform mat4 u_Bones[c_MaxBoneCount];
            #endif
            
            void main() {
                vec4 position;
                #ifdef BONE
                    mat4 skinTransform;
                    #ifdef SIMPLEBONE
                        float currentPixelPos;
                        #ifdef GPU_INSTANCE
                            currentPixelPos = a_SimpleTextureParams.x+a_SimpleTextureParams.y;
                        #else
                            currentPixelPos = u_SimpleAnimatorParams.x+u_SimpleAnimatorParams.y;
                        #endif
                        float offset = 1.0/u_SimpleAnimatorTextureSize;
                        skinTransform =  loadMatFromTexture(currentPixelPos,int(a_BoneIndices.x),offset) * a_BoneWeights.x;
                        skinTransform += loadMatFromTexture(currentPixelPos,int(a_BoneIndices.y),offset) * a_BoneWeights.y;
                        skinTransform += loadMatFromTexture(currentPixelPos,int(a_BoneIndices.z),offset) * a_BoneWeights.z;
                        skinTransform += loadMatFromTexture(currentPixelPos,int(a_BoneIndices.w),offset) * a_BoneWeights.w;
                    #else
                        skinTransform =  u_Bones[int(a_BoneIndices.x)] * a_BoneWeights.x;
                        skinTransform += u_Bones[int(a_BoneIndices.y)] * a_BoneWeights.y;
                        skinTransform += u_Bones[int(a_BoneIndices.z)] * a_BoneWeights.z;
                        skinTransform += u_Bones[int(a_BoneIndices.w)] * a_BoneWeights.w;
                    #endif
                    position=skinTransform*a_Position;
                #else
                    position=a_Position;
                #endif

                vec3 lightDir = normalize(u_SunLight.direction);
                //计算顶点的世界坐标
                vec4 shadowPos = u_WorldMat * position;
                //跟据之前的公式计算Sx和Sz
                shadowPos.xz = shadowPos.xz + lightDir.xz * (u_Plane_Height - shadowPos.y) / lightDir.y;
                //给Sy赋值并加上一个偏移量,保证阴影在平面上方
                shadowPos.y = u_Plane_Height + 0.0001;
                //世界坐标转换为裁剪空间坐标
                gl_Position = u_ViewProjection * shadowPos;
                gl_Position=remapGLPositionZ(gl_Position);
            }
        `;

        var fs = `
            void main()
            {
                gl_FragColor = vec4(0, 0, 0, 0.3);
            }
        `;

        var attributeMap = {
            'a_Position': VertexMesh.MESH_POSITION0,
            'a_Color': VertexMesh.MESH_COLOR0,
            'a_Texcoord0': VertexMesh.MESH_TEXTURECOORDINATE0,
            'a_BoneWeights': VertexMesh.MESH_BLENDWEIGHT0,
            'a_BoneIndices': VertexMesh.MESH_BLENDINDICES0,
            'a_MvpMatrix': VertexMesh.MESH_MVPMATRIX_ROW0
        };
        var uniformMap = {
            'u_Bones': Shader3D.PERIOD_CUSTOM,
            'u_AlbedoTexture': Shader3D.PERIOD_MATERIAL,
            'u_AlbedoColor': Shader3D.PERIOD_MATERIAL,
            'u_TilingOffset': Shader3D.PERIOD_MATERIAL,
            'u_AlphaTestValue': Shader3D.PERIOD_MATERIAL,
            'u_MvpMatrix': Shader3D.PERIOD_SPRITE,
            'u_WorldMat': Shader3D.PERIOD_SPRITE,
            'u_ViewProjection': Shader3D.PERIOD_CAMERA,
            'u_Plane_Height': Shader3D.PERIOD_MATERIAL,     //自定义的uniformMap必须使用Shader3D.PERIOD_MATERIAL类型
            'u_SunLights': Shader3D.PERIOD_SCENE,

            'u_SimpleAnimatorTexture': Shader3D.PERIOD_SPRITE,
            'u_SimpleAnimatorParams': Shader3D.PERIOD_SPRITE,
            'u_SimpleAnimatorTextureSize': Shader3D.PERIOD_SPRITE,

            'u_FogStart': Shader3D.PERIOD_SCENE,
            'u_FogRange': Shader3D.PERIOD_SCENE,
            'u_FogColor': Shader3D.PERIOD_SCENE
        };
        var stateMap = {
            's_Cull': Shader3D.RENDER_STATE_CULL,
            's_Blend': Shader3D.RENDER_STATE_BLEND,
            's_BlendSrc': Shader3D.RENDER_STATE_BLEND_SRC,
            's_BlendDst': Shader3D.RENDER_STATE_BLEND_DST,
            's_DepthTest': Shader3D.RENDER_STATE_DEPTH_TEST,
            's_DepthWrite': Shader3D.RENDER_STATE_DEPTH_WRITE
        }
        var shader = Shader3D.add("PlanarShadow", null, null, true);
        var subShader = new SubShader(attributeMap, uniformMap);
        subShader.addShaderPass(vs, fs, stateMap);
        subShader.addShaderPass(UnlitVS, UnlitPS, stateMap);
        shader.addSubShader(subShader);
    }

}

调用方法:

PlanarShadowMaterial.__init__();
let cube = this.view.getChildByName("Cube") as MeshSprite3D;
let material = new PlanarShadowMaterial();
material.planeHeight = (this.view.getChildByName("Plane") as Sprite3D).transform.position.y;
material.renderMode = UnlitMaterial.RENDERMODE_TRANSPARENT;
material.depthWrite = true;
cube.meshRenderer.material = material;

在这里插入图片描述
下一篇的章节展现遇到的问题以及如何修复。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值