今天继续学习webgl一个重要功能:skinning(蒙皮),内容来自学习网站webglfundamentals,这里仅供学习交流,原文链接:https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-skinning.html。文章并非原创!如果转载请标明原文章出处!

前一篇我们学习的《WebGL蒙皮(上)》就是蒙皮的基础知识。写呈现蒙皮网格的代码并不困难。更困难的部分实际上是获取数据。你通常需要一些3D软件像blender/maya/3d studio max,然后要么写你自己的导出器或者找到一个导出器提供所有你需要的数据。你会看到像我们介绍的一样加载蒙皮相较于展示它会有10倍多的代码,这还不包括大约20-30倍多的代码从3D程序中导出的导出器。题外话这部分通常是人们写他们的3D引擎通常忽略的。

让我们尝试加载一个glTF文件。 glTF是为WebGL而设计的。在网上我找到了这个虎鲸文件是Junskie Pastilan制作的。

glTF有两种格式。.gltf格式是一个JSON文件通常引用一个 .bin文件,这是一个二进制文件通常只包含几何体,可能包含动画数据。另一种格式是.glb二进制格式。通常是JSON和其他文件连接到一个二进制文件内,每个连接部分之间有一个短头和一个大小/类型描述。对于JavaScript,我认为.gltf格式稍微容易上手,所以让我们尝试加载它。首先我下载了.blend文件,安装blender,安装gltf导出器,blender中加载文件并导出。导出之后我用文本编辑器打开.gltf文件并浏览了一下。我用这个表来弄清楚格式。我想说明下面的代码不是一个完美的gltf加载器,只是足以展示鲸鱼的代码。我怀疑如果我们尝试不同的文件,我们会遇到需要更改的区域。首先要做的事情是加载文件。简单起见,我们使用JavaScript的async/await。首先我们写一些代码来加载.gltf 文件和它引用的文件。

  • async function loadGLTF(url) {

  • const gltf = await loadJSON(url);

  • // 加载所有gltf文件相关连的文件

  • const baseURL = new URL(url, location.href);

  • gltf.buffers = await Promise.all(gltf.buffers.map((buffer) => {

  • const url = new URL(buffer.uri, baseURL.href);

  • return loadBinary(url.href);

  • }));

  • ...

  • async function loadFile(url, typeFunc) {

  • const response = await fetch(url);

  • if (!response.ok) {

  • throw new Error(`could not load: ${url}`);

  • }

  • return await response[typeFunc]();

  • }

  • async function loadBinary(url) {

  • return loadFile(url, 'arrayBuffer');

  • }

  • async function loadJSON(url) {

  • return loadFile(url, 'json');

  • }

现在我们需要遍历数据将其连接起来。首先让我们着手于glTF如何定义一个网格。网格是图元的集合。图元实际上是渲染所需的缓冲和属性。让我们使用码少趣多文章中实现的webglUtils。我们将遍历网格,为每个网格创建一个传递给webglUtils.setBuffersAndAttributesBufferInfo。回忆 BufferInfo实际上只是属性信息,及下标如果有的话,和传递给gl.drawXXX的元素数量。举个例子一个只有位置和法线的立方体会具有如下结构的BufferInfo

  • const cubeBufferInfo = {

  • attribs: {

  • 'a_POSITION': { buffer: WebGLBuffer, type: gl.FLOAT, numComponents: 3, },

  • 'a_NORMAL': { buffer: WebGLBuffer, type: gl.FLOAT, numComponents: 3, },

  • },

  • numElements: 24,

  • indices: WebGLBuffer,

  • elementType: gl.UNSIGNED_SHORT,

  • }

所以我们将遍历每个图元生成一个像这样的BufferInfo。图元有一组属性,每个属性引用一个访问器。访问器描述是哪种数据,例如VEC3/gl.FLOAT并引用一个视图缓冲。给定一个访问器下标,我们可以编写一些代码来返回一个WebGLBuffer,其中包含加载的数据,访问器和,缓冲视图的stride。

  • // 给定一个访问器下标返回一个访问器, WebGLBuffer和一个stride

  • function getAccessorAndWebGLBuffer(gl, gltf, accessorIndex) {

  • const accessor = gltf.accessors[accessorIndex];

  • const bufferView = gltf.bufferViews[accessor.bufferView];

  • if (!bufferView.webglBuffer) {

  • const buffer = gl.createBuffer();

  • const target = bufferView.target || gl.ARRAY_BUFFER;

  • const arrayBuffer = gltf.buffers[bufferView.buffer];

  • const data = new Uint8Array(arrayBuffer, bufferView.byteOffset, bufferView.byteLength);

  • gl.bindBuffer(target, buffer);

  • gl.bufferData(target, data, gl.STATIC_DRAW);

  • bufferView.webglBuffer = buffer;

  • }

  • return {

  • accessor,

  • buffer: bufferView.webglBuffer,

  • stride: bufferView.stride || 0,

  • };

  • }

我们也需要一个将glTF访问器的type类型转换为数字的方法

  • function throwNoKey(key) {

  • throw new Error(`no key: ${key}`);

  • }

  • const accessorTypeToNumComponentsMap = {

  • 'SCALAR': 1,

  • 'VEC2': 2,

  • 'VEC3': 3,

  • 'VEC4': 4,

  • 'MAT2': 4,

  • 'MAT3': 9,

  • 'MAT4': 16,

  • };

  • function accessorTypeToNumComponents(type) {

  • return accessorTypeToNumComponentsMap[type] || throwNoKey(type);

  • }

现在我们已经创建了这些函数,我们可以使用他们来设置网格注意:glTF文件可以定义材质,但是导出器并没有导出任何材质到文件内,即使已经勾选了导出材质的选项。我只能猜测在blender中导出器不处理任何材质。我们会使用默认材质如果文件中没有材质的话。因为这个文件中没有任何材质,这里没有使用glTF材质的代码。

  • const defaultMaterial = {

  • uniforms: {

  • u_diffuse: [.5, .8, 1, 1],

  • },

  • };

  • // 设置网格

  • gltf.meshes.forEach((mesh) => {

  • mesh.primitives.forEach((primitive) => {

  • const attribs = {};

  • let numElements;

  • for (const [attribName, index] of Object.entries(primitive.attributes)) {

  • const {accessor, buffer, stride} = getAccessorAndWebGLBuffer(gl, gltf, index);

  • numElements = accessor.count;

  • attribs[`a_${attribName}`] = {

  • buffer,

  • type: accessor.componentType,

  • numComponents: accessorTypeToNumComponents(accessor.type),

  • stride,

  • offset: accessor.byteOffset | 0,

  • };

  • }

  • const bufferInfo = {

  • attribs,

  • numElements,

  • };

  • if (primitive.indices !== undefined) {

  • const {accessor, buffer} = getAccessorAndWebGLBuffer(gl, gltf, primitive.indices);

  • bufferInfo.numElements = accessor.count;

  • bufferInfo.indices = buffer;

  • bufferInfo.elementType = accessor.componentType;

  • }

  • primitive.bufferInfo = bufferInfo;

  • // 存储图元的材质信息

  • primitive.material = gltf.materials && gltf.materials[primitive.material] || defaultMaterial;

  • });

  • });

现在每个图元都有一个bufferInfo和一个material属性。对于蒙皮,我们通常需要某种场景图。我们在场景图的文章中创建了一个场景图,所以让我们使用那个。

  • class TRS {

  • constructor(position = [0, 0, 0], rotation = [0, 0, 0, 1], scale = [1, 1, 1]) {

  • this.position = position;

  • this.rotation = rotation;

  • this.scale = scale;

  • }

  • getMatrix(dst) {

  • dst = dst || new Float32Array(16);

  • m4.compose(this.position, this.rotation, this.scale, dst);

  • return dst;

  • }

  • }

  • class Node {

  • constructor(source, name) {

  • this.name = name;

  • this.source = source;

  • this.parent = null;

  • this.children = [];

  • this.localMatrix = m4.identity();

  • this.worldMatrix = m4.identity();

  • this.drawables = [];

  • }

  • setParent(parent) {

  • if (this.parent) {

  • this.parent._removeChild(this);

  • this.parent = null;

  • }

  • if (parent) {

  • parent._addChild(this);

  • this.parent = parent;

  • }

  • }

  • updateWorldMatrix(parentWorldMatrix) {

  • const source = this.source;

  • if (source) {

  • source.getMatrix(this.localMatrix);

  • }

  • if (parentWorldMatrix) {

  • // 一个矩阵传入,所以做数学运算

  • m4.multiply(parentWorldMatrix, this.localMatrix, this.worldMatrix);

  • } else {

  • // 没有矩阵传入,所以只是拷贝局部矩阵到世界矩阵

  • m4.copy(this.localMatrix, this.worldMatrix);

  • }

  • // 现在处理所有子

  • const worldMatrix = this.worldMatrix;

  • for (const child of this.children) {

  • child.updateWorldMatrix(worldMatrix);

  • }

  • }

  • traverse(fn) {

  • fn(this);

  • for (const child of this.children) {

  • child.traverse(fn);

  • }

  • }

  • _addChild(child) {

  • this.children.push(child);

  • }

  • _removeChild(child) {

  • const ndx = this.children.indexOf(child);

  • this.children.splice(ndx, 1);

  • }

  • }

相较于场景图文章中的代码有一些值的注意的变化。

  • 此代码使用ES6的class特性。

    使用class语法比定义类的旧方法要好得多。

  • 我们给Node添加了要绘制的数组

    这将列出从此节点要绘制的的物体。我们会用类的实例实际上来绘制。这个方法我们通常可以用不同的类绘制不同的物体。

    注意:我不确定在Node里添加一个要绘制的数组是最好的方法。我觉得场景图本身应该可能不包含要绘制的物体。需要绘制的东西可改为图中节点的引用来获取数据。要绘制物体的方法比较常见所以让我们开始使用。

  • 我们增加了一个traverse方法。

    它用当前节点调用传入的函数,并对子节点递归执行。

  • TRS类使用四元数进行旋转

    我们并没有介绍过四元数,说实话我不认为我非常理解足以解释他们。幸运的是,我们用它们并不需要知道他们如何工作。我们只是从gltf文件中取出数据,调用一个函数它通过这些数据创建一个矩阵,使用该矩阵。

glTF文件中的节点数据存储为数组。我们会转换glTF文件中的节点数据为Node实例。我们存储节点数据的旧数组为origNodes,我们稍后会需要用它。

  • const origNodes = gltf.nodes;

  • gltf.nodes = gltf.nodes.map((n) => {

  • const {name, skin, mesh, translation, rotation, scale} = n;

  • const trs = new TRS(translation, rotation, scale);

  • const node = new Node(trs, name);

  • const realMesh = gltf.meshes[mesh];

  • if (realMesh) {

  • node.drawables.push(new MeshRenderer(realMesh));

  • }

  • return node;

  • });

上面我们为每个节点创建一个TRS实例,一个Node实例,我们查找之前设置的网格数据,如果有mesh属性的话,创建一个 MeshRenderer来绘制它。让我们来创建MeshRenderer。它只是码少趣多文章中渲染单个模型代码的封装。它所做的就是存一个对于网格的引用,然后为每个图元设置程序,属性和全局变量,最终通过webglUtils.drawBufferInfo调用gl.drawArrays或者 gl.drawElements;

  • class MeshRenderer {

  • constructor(mesh) {

  • this.mesh = mesh;

  • }

  • render(node, projection, view, sharedUniforms) {

  • const {mesh} = this;

  • gl.useProgram(meshProgramInfo.program);

  • for (const primitive of mesh.primitives) {

  • webglUtils.setBuffersAndAttributes(gl, meshProgramInfo, primitive.bufferInfo);

  • webglUtils.setUniforms(meshProgramInfo, {

  • u_projection: projection,

  • u_view: view,

  • u_world: node.worldMatrix,

  • });

  • webglUtils.setUniforms(skinProgramInfo, primitive.material.uniforms);

  • webglUtils.setUniforms(skinProgramInfo, sharedUniforms);

  • webglUtils.drawBufferInfo(gl, primitive.bufferInfo);

  • }

  • }

  • }

我们已经创建了节点,现在我们需要将它们实际安排到场景图中。在glTF中2步完成。首先,每个节点有一个可选的children属性,为子节点的下标数组,所以我们可以遍历所有节点为它们的子节点设定父节点。

  • function addChildren(nodes, node, childIndices) {

  • childIndices.forEach((childNdx) => {

  • const child = nodes[childNdx];

  • child.setParent(node);

  • });

  • }

  • // 将节点加入场景图

  • gltf.nodes.forEach((node, ndx) => {

  • const children = origNodes[ndx].children;

  • if (children) {

  • addChildren(gltf.nodes, node, children);

  • }

  • });

然后有一个场景的数组。一个场景有一个场景底部节点在nodes数组中下标的数组来引用这些节点。我不是很清楚为什么不简单地从单个根节点开始,但是无论如何这就是glTF文件中地内容。所以我们创建一个根节点,作为所有场景子节点的父节点。

  • // 设置场景

  • for (const scene of gltf.scenes) {

  • scene.root = new Node(new TRS(), scene.name);

  • addChildren(gltf.nodes, scene.root, scene.nodes);

  • }

  • return gltf;

  • }

我们已经完成了加载,至少只是网格部分。让我们将主函数标记为async 所以我们能使用await关键字。

  • async function main() {

我们可以像这样加载gltf文件

  • const gltf = await loadGLTF('resources/models/killer_whale/whale.CYCLES.gltf');

我们需要一个与gltf文件中的数据匹配的着色器。让我们看看gltf文件中的图元数据。

  • {

  • "name" : "orca",

  • "primitives" : [

  • {

  • "attributes" : {

  • "JOINTS_0" : 5,

  • "NORMAL" : 2,

  • "POSITION" : 1,

  • "TANGENT" : 3,

  • "TEXCOORD_0" : 4,

  • "WEIGHTS_0" : 6

  • },

  • "indices" : 0

  • }

  • ]

  • }

看一下,我们只使用NORMALPOSITION来渲染。我们在每个属性前添加了a_,因此像这样的顶点着色器应该可以工作。

  • attribute vec4 a_POSITION;

  • attribute vec3 a_NORMAL;

  • uniform mat4 u_projection;

  • uniform mat4 u_view;

  • uniform mat4 u_world;

  • varying vec3 v_normal;

  • void main() {

  • gl_Position = u_projection * u_view * u_world * a_POSITION;

  • v_normal = mat3(u_world) * a_NORMAL;

  • }

片断着色器中我们使用一个简单的平行光

  • precision mediump float;

  • varying vec3 v_normal;

  • uniform vec4 u_diffuse;

  • uniform vec3 u_lightDirection;

  • void main () {

  • vec3 normal = normalize(v_normal);

  • float light = dot(u_lightDirection, normal) * .5 + .5;

  • gl_FragColor = vec4(u_diffuse.rgb * light, u_diffuse.a);

  • }

注意我们使用了平行光文章中提到的点乘,但与此不同,这里点乘结果乘以.5并加上.5。正常平行光照,当直接面向光源时,表面100%照亮,减弱到0%当表面方向和光照垂直。这意味着远离光线的模型的1/2是黑的。通过乘以.5并加上.5,我们将点乘从-1 1转换到0 1,这意味着当完全反方向时才会是黑色。这为简单测试提供了简单并很好的照明。所以,我们需要编译和连接着色器

  • // 编译和连接着色器,查找属性和全局变量的位置

  • const meshProgramInfo = webglUtils.createProgramInfo(gl, ["meshVS", "fs"]);

接着渲染,所有和之前不同的地方是

  • const sharedUniforms = {

  • u_lightDirection: m4.normalize([-1, 3, 5]),

  • };

  • function renderDrawables(node) {

  • for(const drawable of node.drawables) {

  • drawable.render(node, projection, view, sharedUniforms);

  • }

  • }

  • for (const scene of gltf.scenes) {

  • // 更新场景中的世界矩阵。

  • scene.root.updateWorldMatrix();

  • // 遍历场景并渲染所有renderables

  • scene.root.traverse(renderDrawables);

  • }

之前遗留下来的(未在上面显示)是用于计算投影矩阵,相机矩阵,和视图矩阵的代码。接下来我们遍历每个场景,调用scene.root.updateWorldMatrix会更新场景图中所有节点的矩阵。然后我们为renderDrawables调用scene.root.traverserenderDrawables调用该节点上所有绘制对象的渲染方法,传入投影,视图矩阵,sharedUniforms包含的光照信息。打开网页看效果:http://39.106.0.97:8090/lesson/11Techniques/02-3D/04Skinning-03.html现在,这是我们处理蒙皮的工作。首先让我们创建一个代表蒙皮的类。它将管理关节列表,关节是应用于蒙皮的场景图中节点的另一个名字。它还会有绑定矩阵的逆矩阵。它会管理我们放入关节矩阵的材质。

  • class Skin {

  • constructor(joints, inverseBindMatrixData) {

  • this.joints = joints;

  • this.inverseBindMatrices = [];

  • this.jointMatrices = [];

  • // 为每个关节矩阵分配足够的空间

  • this.jointData = new Float32Array(joints.length * 16);

  • // 为每个关节和绑定逆矩阵创建视图

  • for (let i = 0; i < joints.length; ++i) {

  • this.inverseBindMatrices.push(new Float32Array(

  • inverseBindMatrixData.buffer,

  • inverseBindMatrixData.byteOffset + Float32Array.BYTES_PER_ELEMENT * 16 * i,

  • 16));

  • this.jointMatrices.push(new Float32Array(

  • this.jointData.buffer,

  • Float32Array.BYTES_PER_ELEMENT * 16 * i,

  • 16));

  • }

  • // 创建存储关节矩阵的纹理

  • this.jointTexture = gl.createTexture();

  • gl.bindTexture(gl.TEXTURE_2D, this.jointTexture);

  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);

  • gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  • }

  • update(node) {

  • const globalWorldInverse = m4.inverse(node.worldMatrix);

  • // 遍历每个关节获得当前世界矩阵

  • // 来计算绑定矩阵的逆

  • // 并在纹理中存储整个结果

  • for (let j = 0; j < this.joints.length; ++j) {

  • const joint = this.joints[j];

  • const dst = this.jointMatrices[j];

  • m4.multiply(globalWorldInverse, joint.worldMatrix, dst);

  • m4.multiply(dst, this.inverseBindMatrices[j], dst);

  • }

  • gl.bindTexture(gl.TEXTURE_2D, this.jointTexture);

  • gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 4, this.joints.length, 0,

  • gl.RGBA, gl.FLOAT, this.jointData);

  • }

  • }

MeshRenderer一样,我们制作SkinRenderer,来用Skin来渲染蒙皮网格。

  • class SkinRenderer {

  • constructor(mesh, skin) {

  • this.mesh = mesh;

  • this.skin = skin;

  • }

  • render(node, projection, view, sharedUniforms) {

  • const {skin, mesh} = this;

  • skin.update(node);

  • gl.useProgram(skinProgramInfo.program);

  • for (const primitive of mesh.primitives) {

  • webglUtils.setBuffersAndAttributes(gl, skinProgramInfo, primitive.bufferInfo);

  • webglUtils.setUniforms(skinProgramInfo, {

  • u_projection: projection,

  • u_view: view,

  • u_world: node.worldMatrix,

  • u_jointTexture: skin.jointTexture,

  • u_numJoints: skin.joints.length,

  • });

  • webglUtils.setUniforms(skinProgramInfo, primitive.material.uniforms);

  • webglUtils.setUniforms(skinProgramInfo, sharedUniforms);

  • webglUtils.drawBufferInfo(gl, primitive.bufferInfo);

  • }

  • }

  • }

你可以看到和 MeshRenderer非常类似。它有一个Skin的引用来更新所有渲染需要的矩阵。然后它后跟了渲染的标准模式,使用程序,设置属性,用webglUtils.setUniforms设置全局变量,也绑定纹理,然后渲染。我们也需要一个支持蒙皮的顶点着色器这与我们之前介绍的蒙皮着色器几乎相同。我们重命名了属性值来匹配gltf文件中的内容。最大的不同是我们生成了一个 skinMatrix。在我们之前的蒙皮着色器,我们将位置和每一个关节/骨骼矩阵相乘,并乘以每个关节的影响权重。在这个例子中,我们代替的将矩阵和权重相乘并相加,只乘以一次位置。这产生相同的结果,但是我们可以使用skinMatrix和法线相乘,我们需要这样做否则法线会和蒙皮不匹配。还要注意在这里我们用u_world相乘。我们在Skin.update里减去了它。

  • const globalWorldInverse = m4.inverse(node.worldMatrix);

  • // 遍历每个关节,获得它当前的世界矩阵

  • // 来计算绑定矩阵的逆

  • // 并在纹理中存储整个结果

  • for (let j = 0; j < this.joints.length; ++j) {

  • const joint = this.joints[j];

  • const dst = this.jointMatrices[j];

  • m4.multiply(globalWorldInverse, joint.worldMatrix, dst);

无论你是否这样做取决于你。这样做的原因是它允许你实例化蒙皮。换句话说你可以在相同帧中在不同的地方渲染蒙皮网格。如果有很多的关节,对于一个蒙皮网格做所有的矩阵数学是非常慢的,所以你做一遍数学操作,然后你可以通过一个不同的世界矩阵将蒙皮网格重渲染在任何地方。这对于显示一群角色是有效的。不幸的是所有的角色都会是相同的姿势,所以我并不清楚这是否有用。这种情况通常出现的频率是多少? 你可以在Skin中移除乘以世界矩阵的逆并在着色器中移除乘以u_world,结果是一样的,你仅仅不能实例化 那个蒙皮网格。当然你可以多次渲染不同姿势的同一蒙皮网格。你会需要一个不同的Skin对象指向其他方向的不同节点。回到我们的加载代码,当我们创建Node实例时,如果有skin属性,我们记录它,为了能为它创建一个Skin

  • const skinNodes = [];

  • const origNodes = gltf.nodes;

  • gltf.nodes = gltf.nodes.map((n) => {

  • const {name, skin, mesh, translation, rotation, scale} = n;

  • const trs = new TRS(translation, rotation, scale);

  • const node = new Node(trs, name);

  • const realMesh = gltf.meshes[mesh];

  • if (skin !== undefined) {

  • skinNodes.push({node, mesh: realMesh, skinNdx: skin});

  • } else if (realMesh) {

  • node.drawables.push(new MeshRenderer(realMesh));

  • }

  • return node;

  • });

创建Node之后我们需要创建Skin。蒙皮通过joints数组引用节点,该数组是为关节提供矩阵的节点下标数组。蒙皮也引用一个访问器,访问器引用了保存在文件中的反向绑定姿势矩阵。

  • // 设置蒙皮

  • gltf.skins = gltf.skins.map((skin) => {

  • const joints = skin.joints.map(ndx => gltf.nodes[ndx]);

  • const {stride, array} = getAccessorTypedArrayAndStride(gl, gltf, skin.inverseBindMatrices);

  • return new Skin(joints, array);

  • });

上面的代码给定一个访问器下标,调用了getAccessorTypedArrayAndStride。我们需要提供这部分的代码。给定一个访问器,我们会返回类型化数组的正确类型视图以访问缓冲中的数据。

  • const glTypeToTypedArrayMap = {

  • '5120': Int8Array, // gl.BYTE

  • '5121': Uint8Array, // gl.UNSIGNED_BYTE

  • '5122': Int16Array, // gl.SHORT

  • '5123': Uint16Array, // gl.UNSIGNED_SHORT

  • '5124': Int32Array, // gl.INT

  • '5125': Uint32Array, // gl.UNSIGNED_INT

  • '5126': Float32Array, // gl.FLOAT

  • }

  • // 给定一个GL类型返回需要的类型

  • function glTypeToTypedArray(type) {

  • return glTypeToTypedArrayMap[type] || throwNoKey(type);

  • }

  • // 给定一个访问器下标返回访问器

  • // 和缓冲正确部分的类型化数组

  • function getAccessorTypedArrayAndStride(gl, gltf, accessorIndex) {

  • const accessor = gltf.accessors[accessorIndex];

  • const bufferView = gltf.bufferViews[accessor.bufferView];

  • const TypedArray = glTypeToTypedArray(accessor.componentType);

  • const buffer = gltf.buffers[bufferView.buffer];

  • return {

  • accessor,

  • array: new TypedArray(

  • buffer,

  • bufferView.byteOffset + (accessor.byteOffset || 0),

  • accessor.count * accessorTypeToNumComponents(accessor.type)),

  • stride: bufferView.byteStride || 0,

  • };

  • }

需要注意的是上面的代码我们用硬编码的WebGL常量制作了一个表。这是我们第一次这样做。常量不会改变,所以这是安全的。现在我们有了蒙皮,我们可以返回并将它门添加到引用它们的节点。

  • // 给蒙皮节点添加SkinRenderers

  • for (const {node, mesh, skinNdx} of skinNodes) {

  • node.drawables.push(new SkinRenderer(mesh, gltf.skins[skinNdx]));

  • }

如果我们这样渲染我们看不出任何不同。我们需要让一些节点动起来。让我们遍历Skin中的每个节点,换句话说每个关节,并在本地x轴上旋转一点点。为此,我们会存每个关节的原始本地矩阵。我们会每帧旋转一些本地矩阵,使用一个特殊的方法m4.decompose,会转换矩阵为关节的位置,旋转量,缩放量。

  • const origMatrix = new Map();

  • function animSkin(skin, a) {

  • for(let i = 0; i < skin.joints.length; ++i) {

  • const joint = skin.joints[i];

  • // 如果这个关节并没有存储矩阵

  • if (!origMatrix.has(joint)) {

  • // 为关节存储一个矩阵

  • origMatrix.set(joint, joint.source.getMatrix());

  • }

  • // 获取原始的矩阵

  • const origMatrix = origRotations.get(joint);

  • // 旋转它

  • const m = m4.xRotate(origMatrix, a);

  • // 分解回关节的位置,旋转量,缩放量

  • m4.decompose(m, joint.source.position, joint.source.rotation, joint.source.scale);

  • }

  • }

然后在渲染之前我们会调用它

  • animSkin(gltf.skins[0], Math.sin(time) * .5);

注意animSkin不是通常的做法。理想情况下,我们会加载一些艺术家制作的动画或者我们知道我们想要以某种方式在代码中操作某个特定关节。在这个例子里,我们只是看看蒙皮是否有效,这似乎是一种简单的方法。打开网页看效果:http://39.106.0.97:8090/lesson/11Techniques/02-3D/04Skinning-04.html在我们继续之前一些注意事项当我第一次尝试让它工作时,就像大多数程序一样,屏幕上没有显示的东西。所以,首先做的是在蒙皮着色器的末尾添加这一行

  • gl_Position = u_projection * u_view * a_POSITION;

在片断着色器中,我改变了它,仅仅在末尾添加这个来画纯色的

  • gl_FragColor = vec4(1, 0, 0, 1);

这将删除所有蒙皮,仅仅在原点绘制网格。我调整相机位置直到我有了一个好的视角。

  • const cameraPosition = [5, 0, 5];

  • const target = [0, 0, 0];

这显示了虎鲸的轮廓,所以我知道至少有一些数据正在发挥作用

接下来我让片断着色器显示法线

  • gl_FragColor = vec4(normalize(v_normal) * .5 + .5, 1);

法线从-1 到 1,所以 * .5 + .5调整它们到0 到 1来观察颜色。

回到顶点着色器我仅仅传递法线

  • v_normal = a_NORMAL;

我可以看到这样

我并没有觉得法线会出错,但是从我认为有效的开始,并确认它确实是有效的是很好的方法。接下来我想我应该检查权重。所有我需要做的就是像法线一样从顶点着色器传递权重

  • v_normal = a_WEIGHTS_0.xyz * 2. - 1.;

权重从0到1,但是因为片断着色器需要法线,我仅仅改变权重从-1到1这最初产生了一种混乱的颜色。一旦我发现了这个bug,我得到了这样的图像

它并不完全明显是正确的,但确实有道理。你希望每个骨骼最近的颜色有强烈的颜色,并且你希望在骨骼周围看到色环,因为那个区域的权重可能是1.0或者至少全部相似。由于原始图像太乱了,我也尝试显示骨骼下标

  • v_normal = a_JOINTS_0.xyz / (u_numJoints - 1.) * 2. - 1.;

下标从 0 到 骨骼数量- 1,所以上边的代码会得到-1到1的结果。当正常工作时,我得到了这样的图像

又一次得到了乱七八糟的颜色。上图是修复后的样子。这就是你期望看到的虎鲸的权重。每个骨骼周围的色环。这个bug和webgl.createBufferInfoFromArrays如何计算组件数量有关。有些情况下它被忽略了特定的那个,试图猜测,并猜错了。修复bug后我移除了着色器的改动。注意如果你想使用它们,我在注释中保留它们。我想说清楚上面的代码是为了帮助说明蒙皮。它并不意味是一个成熟的蒙皮引擎。我想如果我们试图做一个可使用的引擎,我们会遇到许多我们可能需要改变的事情,但我希望这个例子可以帮助轻微揭开蒙皮的神秘面纱。

再次声明,文章并非原创!这里仅供学习交流,原文链接:https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-skinning.html。如果转载请标明原文章出处!

学习交流小伙伴公众号giserYZ2SS直接留言。

中webgl解析json_WebGL蒙皮(下)相关推荐

  1. 深入解析 Raft 模块在 KaiwuDB 中的优化改造(下)

    导读 KaiwuDB 是由浪潮开源的一款 NewSQL 分布式数据库,具备 HTAP 特性,拥有强一致.高可用的分布式架构.其中,KaiwuDB 各方面的强一致性都依靠 Raft 算法实现.我们在上一 ...

  2. JAVA方法调用中的解析与分派

    JAVA方法调用中的解析与分派 本文算是<深入理解JVM>的读书笔记,参考书中的相关代码示例,从字节码指令角度看看解析与分派的区别. 方法调用,其实就是要回答一个问题:JVM在执行一个方法 ...

  3. iOS中XML解析汇总

    在时间上TBXML占优,libxml2支持了边下载边解析. 来源:http://www.codeios.com/forum.php?mod=viewthread&tid=9880&hi ...

  4. Python中对象名称前单下划线和双下划线有啥区别

    单下划线 在一个类中的方法或属性用单下划线开头就是告诉别的程序这个属性或方法是私有的.然而对于这个名字来说并没有什么特别的. 引自PEP-8: 单下划线:"内部使用"的弱指示器.比 ...

  5. linux源码文件名,Linux中文件名解析处理源码分析

    Linux中文件名解析处理源码分析 前言 Linux中对一个文件进行操作的时候,一件很重要的事情是对文件名进行解析处理,并且找到对应文件的inode对象,然后创建表示文件的file对象.在此,对文件名 ...

  6. iOS中XML解析 (二) libxml2(实例:打印xml内容及存储到数组)

    关联:iOS中XML解析 (一) TBXML (实例:打印xml内容及存储到数组) 关于libxml库的基本使用,在http://xmlsoft.org/网上有文档. 准备工作: project=&g ...

  7. iOS中XML解析 (一) TBXML (实例:打印xml内容及存储到数组)

    关联:iOS中XML解析 (二) libxml2(实例:打印xml内容及存储到数组) 在时间上TBXML占优,libxml2支持了边下载边解析. 来源:http://www.codeios.com/f ...

  8. VS中无法解析的外部命令的解决办法

    VS中无法解析的外部命令的解决办法 报错LNK2005外部符号 报错LNK2019外部符号 报错LNK1120外部符号 解决办法1: 检查自己报错的代码里,是否有类里声明的函数没有对应的实现.比如在p ...

  9. 网络直播电视之M3U8解析篇 (下)

    网络直播电视之M3U8解析篇 (下) 标签: c++C++directshowDirectshowDirectShow网络直播视频 2012-12-26 13:04 43004人阅读 评论(8) 收藏 ...

最新文章

  1. 计算机设备采购申请,办公室采购电脑请示报告
  2. 分布式项目启动时报错:Duplicate spring bean id XXX
  3. 信息学奥赛一本通(1397:简单算术表达式求值)
  4. 搭建、使用与维护私有PyPi仓库
  5. oracle 设置会话的编码,在Oracle中使用登录触发器初始化用户会话
  6. Journal of BitcoinJ 从clone开始
  7. STM32驱动SPI LCD屏幕
  8. web前端 vue 面试题(一)
  9. kindle3使用技巧
  10. Wretch超雅虎奇摩成台湾省第一大网站
  11. SD--定价过程的16个字段的作用说明
  12. 自制Linux功能板
  13. tp5.1 出现Class 'Qcloud\Sms\SmsSingleSender' not found(mac和windows没有,linux出现)
  14. python批量生成word_实例5:用Python批量生成word版邀请函
  15. 第六讲 Keras实现手写字体识别分类
  16. linkerd2 php 微服务,在 Linkerd2 中进行流量拆分
  17. 阿里巴巴矢量图库批量下载的方法
  18. 计算机打字大赛策划书,打字比赛策划书
  19. java ftp输出流_java输出流实现文件下载
  20. An assembly specified in the application dependencies manifest (…) was not found

热门文章

  1. java 深入了解DTO及如何使用DTO
  2. 学成在线--19.新增课程(数据字典)
  3. android fragment fragmenttransaction,Android FragmentTransaction 常用方法总结
  4. 星星排序python_python中怎么实现星星排列
  5. C语言变量的类型和存储位置
  6. mysql 停止服务内存_服务器莫名的内存高占用 导致 MySQL 停止运行问题
  7. 贪心算法设计作业调度c语言,贪心算法 - 数据结构与算法教程 - C语言网
  8. 链表(单链表、双链表、内核链表)
  9. springaop----springaop的使用(一)
  10. 单词的理解 —— 词义的变化(翻译)