Skip to content

Commit ad5c7ab

Browse files
authored
GPU Morph Targets (playcanvas#2084)
* GPU Morph Targets * Simplification in example * Workaround for BoundingBox keeping reference to Vec3 parameters - to avoid override of Vec3.ZERO values * Morph weights parameter typo fix * Remove unrelated changes to different PR * Minimize object allocations when collective active targets
1 parent 0e89ffb commit ad5c7ab

21 files changed

+958
-905
lines changed

build/dependencies.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@
8080
../src/scene/mesh-instance.js
8181
../src/scene/skin.js
8282
../src/scene/skin-partition.js
83+
../src/scene/morph-target.js
8384
../src/scene/morph.js
85+
../src/scene/morph-instance.js
8486
../src/scene/model.js
8587
../src/scene/particle-system/particle-emitter.js
8688
../src/scene/particle-system/cpu-updater.js

examples/examples.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var categories = [
2626
"mesh-decals",
2727
"mesh-deformation",
2828
"mesh-generation",
29+
"mesh-morph",
2930
"model-asset",
3031
"model-box",
3132
"model-outline",

examples/graphics/mesh-morph.html

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>PlayCanvas Mesh Morph</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link rel="icon" type="image/png" href="../playcanvas-favicon.png" />
8+
<script src="../../build/output/playcanvas.js"></script>
9+
<script src="../../build/output/playcanvas-extras.js"></script>
10+
<style>
11+
body {
12+
margin: 0;
13+
overflow-y: hidden;
14+
}
15+
</style>
16+
</head>
17+
18+
<body>
19+
<!-- The canvas element -->
20+
<canvas id="application-canvas"></canvas>
21+
22+
<!-- The script -->
23+
<script>
24+
var canvas = document.getElementById("application-canvas");
25+
26+
// Create the application and start the update loop
27+
var app = new pc.Application(canvas);
28+
app.start();
29+
30+
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
31+
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
32+
app.setCanvasResolution(pc.RESOLUTION_AUTO);
33+
34+
window.addEventListener("resize", function () {
35+
app.resizeCanvas(canvas.width, canvas.height);
36+
});
37+
38+
var miniStats = new pc.MiniStats(app);
39+
40+
// Create an entity with a directional light component
41+
var light = new pc.Entity();
42+
light.addComponent("light", {
43+
type: "directional"
44+
});
45+
app.root.addChild(light);
46+
light.setLocalEulerAngles(45, 30, 0);
47+
48+
// Create an entity with a camera component
49+
var camera = new pc.Entity();
50+
camera.addComponent("camera", {
51+
clearColor: new pc.Color(0.1, 0.1, 0.1)
52+
});
53+
app.root.addChild(camera);
54+
55+
// helper function to return the shortest distance from point [x, y, z] to a plane defined by [a, b, c] normal
56+
var shortestDistance = function (x, y, z, a, b, c)
57+
{
58+
var d = Math.abs((a * x + b * y + c * z));
59+
var e = Math.sqrt(a * a + b * b + c * c);
60+
return d / e;
61+
}
62+
63+
// helper function that creates a morph target from original positions, normals and indices, and a plane normal [nx, ny, nz]
64+
var createMorphTarget = function (positions, normals, indices, nx, ny, nz) {
65+
66+
// modify vertices to separate array
67+
var modifiedPositions = new Float32Array(positions.length);
68+
var dist, i, displacement;
69+
var limit = 0.2;
70+
for (i = 0; i < positions.length; i += 3) {
71+
// distance of the point to the specified plane
72+
dist = shortestDistance(positions[i], positions[i + 1], positions[i + 2], nx, ny, nz);
73+
74+
// modify distance to displacement amoint - displace nearby points more than distant points
75+
displacement = pc.math.clamp(dist, 0, limit);
76+
displacement = pc.math.smoothstep(0, limit, dist);
77+
displacement = 1 - displacement;
78+
79+
// generate new position by extruding vertex along normal by displacement
80+
modifiedPositions[i] = positions[i] + normals[i] * displacement;
81+
modifiedPositions[i + 1] = positions[i + 1] + normals[i + 1] * displacement;
82+
modifiedPositions[i + 2] = positions[i + 2] + normals[i + 2] * displacement;
83+
}
84+
85+
// generate normals based on modified positions and indices
86+
var modifiedNormals = new Float32Array(pc.calculateNormals(modifiedPositions, indices));
87+
88+
// generate delta positions and normals - as morph targets store delta between base position / normal and modified position / normal
89+
for (i = 0; i < modifiedNormals.length; i++) {
90+
modifiedPositions[i] -= positions[i];
91+
modifiedNormals[i] -= normals[i];
92+
}
93+
94+
// create a morph target
95+
return new pc.MorphTarget(app.graphicsDevice, {
96+
deltaPositions: modifiedPositions,
97+
deltaNormals: modifiedNormals
98+
});
99+
};
100+
101+
// create the base mesh - a sphere, with higher amount of vertices / triangles
102+
var mesh = pc.createSphere(app.graphicsDevice, { latitudeBands: 200, longitudeBands: 200 });
103+
104+
// obtain base mesh vertex / index data
105+
var srcPositions = [], srcNormals = [], indices = [];
106+
mesh.getPositions(srcPositions);
107+
mesh.getNormals(srcNormals);
108+
mesh.getIndices(indices);
109+
110+
// build 3 targets by expanding a part of sphere along 3 planes, specified by the normal
111+
var targets = [];
112+
targets.push(createMorphTarget(srcPositions, srcNormals, indices, 1, 0, 0));
113+
targets.push(createMorphTarget(srcPositions, srcNormals, indices, 0, 1, 0));
114+
targets.push(createMorphTarget(srcPositions, srcNormals, indices, 0, 0, 1));
115+
116+
// create a morph using these 3 targets
117+
mesh.morph = new pc.Morph(targets);
118+
119+
// Create the mesh instance
120+
var node = new pc.GraphNode();
121+
var material = new pc.StandardMaterial();
122+
this.meshInstance = new pc.MeshInstance(node, mesh, material);
123+
124+
// Create a model and add the mesh instance to it
125+
var model = new pc.Model();
126+
model.graph = node;
127+
model.meshInstances = [ this.meshInstance ];
128+
129+
// add morph instance - this is where currently set weights are stored
130+
var morphInstance = new pc.MorphInstance(mesh.morph);
131+
meshInstance.morphInstance = morphInstance;
132+
model.morphInstances.push(morphInstance);
133+
134+
// Create Entity and add it to the scene
135+
this.entity = new pc.Entity();
136+
entity.setLocalPosition(0, 0, 0);
137+
app.root.addChild(this.entity);
138+
139+
// Add a model compoonent
140+
app.systems.model.addComponent(this.entity, {
141+
type: 'asset'
142+
});
143+
this.entity.model.model = model;
144+
145+
// update function called once per frame
146+
var self = this;
147+
var time = 0;
148+
app.on("update", function (dt) {
149+
time += dt;
150+
151+
// modify weights of all 3 morph targets along some sin curve with different frequency
152+
morphInstance.setWeight(0, Math.abs(Math.sin(time)));
153+
morphInstance.setWeight(1, Math.abs(Math.sin(time * 0.3)));
154+
morphInstance.setWeight(2, Math.abs(Math.sin(time * 0.7)));
155+
156+
// orbit camera around
157+
camera.setLocalPosition(6 * Math.sin(time * 0.2), 4, 6 * Math.cos(time * 0.2));
158+
camera.lookAt(pc.Vec3.ZERO);
159+
});
160+
161+
</script>
162+
</body>
163+
</html>

src/graphics/device.js

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,38 +2058,46 @@ Object.assign(pc, function () {
20582058
if (element !== null) {
20592059
// Retrieve the vertex buffer that contains this element
20602060
vertexBuffer = this.vertexBuffers[element.stream];
2061-
vbOffset = this.vbOffsets[element.stream] || 0;
2062-
2063-
// Set the active vertex buffer object
2064-
bufferId = vertexBuffer.bufferId;
2065-
if (this.boundBuffer !== bufferId) {
2066-
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
2067-
this.boundBuffer = bufferId;
2068-
}
2061+
if (vertexBuffer) {
2062+
vbOffset = this.vbOffsets[element.stream] || 0;
2063+
2064+
// Set the active vertex buffer object
2065+
bufferId = vertexBuffer.bufferId;
2066+
if (this.boundBuffer !== bufferId) {
2067+
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
2068+
this.boundBuffer = bufferId;
2069+
}
20692070

2070-
// Hook the vertex buffer to the shader program
2071-
locationId = attribute.locationId;
2072-
if (!this.enabledAttributes[locationId]) {
2073-
gl.enableVertexAttribArray(locationId);
2074-
this.enabledAttributes[locationId] = true;
2075-
}
2076-
gl.vertexAttribPointer(
2077-
locationId,
2078-
element.numComponents,
2079-
this.glType[element.dataType],
2080-
element.normalize,
2081-
element.stride,
2082-
element.offset + vbOffset
2083-
);
2071+
// Hook the vertex buffer to the shader program
2072+
locationId = attribute.locationId;
2073+
if (!this.enabledAttributes[locationId]) {
2074+
gl.enableVertexAttribArray(locationId);
2075+
this.enabledAttributes[locationId] = true;
2076+
}
2077+
gl.vertexAttribPointer(
2078+
locationId,
2079+
element.numComponents,
2080+
this.glType[element.dataType],
2081+
element.normalize,
2082+
element.stride,
2083+
element.offset + vbOffset
2084+
);
20842085

2085-
if (element.stream === 1 && numInstances > 0) {
2086-
if (!this.instancedAttribs[locationId]) {
2087-
gl.vertexAttribDivisor(locationId, 1);
2088-
this.instancedAttribs[locationId] = true;
2086+
if (element.stream === 1 && numInstances > 0) {
2087+
if (!this.instancedAttribs[locationId]) {
2088+
gl.vertexAttribDivisor(locationId, 1);
2089+
this.instancedAttribs[locationId] = true;
2090+
}
2091+
} else if (this.instancedAttribs[locationId]) {
2092+
gl.vertexAttribDivisor(locationId, 0);
2093+
this.instancedAttribs[locationId] = false;
20892094
}
2090-
} else if (this.instancedAttribs[locationId]) {
2091-
gl.vertexAttribDivisor(locationId, 0);
2092-
this.instancedAttribs[locationId] = false;
2095+
}
2096+
} else {
2097+
// disable the attribute (shader will get default value 0, 0, 0, 1)
2098+
if (this.enabledAttributes[attribute.locationId]) {
2099+
gl.disableVertexAttribArray(attribute.locationId);
2100+
this.enabledAttributes[attribute.locationId] = false;
20932101
}
20942102
}
20952103
}
@@ -2972,20 +2980,36 @@ Object.assign(pc, function () {
29722980
this.vbOffsets[stream] = vbOffset;
29732981

29742982
// Push each vertex element in scope
2975-
var vertexFormat = vertexBuffer.getFormat();
2976-
var i = 0;
2977-
var elements = vertexFormat.elements;
2978-
var numElements = elements.length;
2979-
while (i < numElements) {
2980-
var vertexElement = elements[i++];
2981-
vertexElement.stream = stream;
2982-
vertexElement.scopeId.setValue(vertexElement);
2983+
if (vertexBuffer) {
2984+
var vertexFormat = vertexBuffer.getFormat();
2985+
var i = 0;
2986+
var elements = vertexFormat.elements;
2987+
var numElements = elements.length;
2988+
while (i < numElements) {
2989+
var vertexElement = elements[i++];
2990+
vertexElement.stream = stream;
2991+
vertexElement.scopeId.setValue(vertexElement);
2992+
}
29832993
}
29842994

29852995
this.attributesInvalidated = true;
29862996
}
29872997
},
29882998

2999+
// Function to disable vertex elements coming from vertex buffer and using constant default value instead (0,0,0,1)
3000+
// this is similar to setVertexBuffer with null vertex buffer, where access to vertexFormat is replaced by providing list of element semantics
3001+
disableVertexBufferElements: function (elementNames) {
3002+
3003+
for (var i = 0; i < elementNames.length; i++) {
3004+
// Resolve the ScopeId for the attribute name
3005+
var scopeId = this.scope.resolve(elementNames[i]);
3006+
if (scopeId.value) {
3007+
this.attributesInvalidated = true;
3008+
scopeId.setValue(null);
3009+
}
3010+
}
3011+
},
3012+
29893013
compileShaderSource: function (src, isVertexShader) {
29903014
var gl = this.gl;
29913015

src/graphics/graphics.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,10 @@
823823
* @description Vertex attribute to be treated as a texture coordinate (set 7).
824824
*/
825825
SEMANTIC_TEXCOORD7: "TEXCOORD7",
826+
827+
// private semantic used for programatic construction of individual attr semantics
828+
SEMANTIC_ATTR: "ATTR",
829+
826830
/**
827831
* @constant
828832
* @name pc.SEMANTIC_ATTR0
@@ -1044,6 +1048,17 @@
10441048
pc.typedArrayTypes = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array];
10451049
pc.typedArrayTypesByteSize = [1, 1, 2, 2, 4, 4, 4];
10461050

1051+
// map of typed array to engine pc.TYPE_***
1052+
pc.typedArrayToType = {
1053+
"Int8Array": pc.TYPE_INT8,
1054+
"Uint8Array": pc.TYPE_UINT8,
1055+
"Int16Array": pc.TYPE_INT16,
1056+
"Uint16Array": pc.TYPE_UINT16,
1057+
"Int32Array": pc.TYPE_INT32,
1058+
"Uint32Array": pc.TYPE_UINT32,
1059+
"Float32Array": pc.TYPE_FLOAT32
1060+
};
1061+
10471062
// map of engine pc.INDEXFORMAT_*** to their corresponding typed array constructors and byte sizes
10481063
pc.typedArrayIndexFormats = [Uint8Array, Uint16Array, Uint32Array];
10491064
pc.typedArrayIndexFormatsByteSize = [1, 2, 4];

src/graphics/program-lib/chunks/normal.vert

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@ vec3 getNormal() {
66
#else
77
dNormalMatrix = matrix_normal;
88
#endif
9-
return normalize(dNormalMatrix * vertex_normal);
9+
10+
vec3 tempNormal = vertex_normal;
11+
12+
#ifdef MORPHING
13+
#ifdef MORPHING_NRM03
14+
tempNormal += morph_weights_a[0] * morph_nrm0;
15+
tempNormal += morph_weights_a[1] * morph_nrm1;
16+
tempNormal += morph_weights_a[2] * morph_nrm2;
17+
tempNormal += morph_weights_a[3] * morph_nrm3;
18+
#endif
19+
#ifdef MORPHING_NRM47
20+
tempNormal += morph_weights_b[0] * morph_nrm4;
21+
tempNormal += morph_weights_b[1] * morph_nrm5;
22+
tempNormal += morph_weights_b[2] * morph_nrm6;
23+
tempNormal += morph_weights_b[3] * morph_nrm7;
24+
#endif
25+
#endif
26+
27+
return normalize(dNormalMatrix * tempNormal);
1028
}
1129

src/graphics/program-lib/chunks/transform.vert

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
uniform vec4 uScreenSize;
33
#endif
44

5+
#ifdef MORPHING
6+
uniform vec4 morph_weights_a;
7+
uniform vec4 morph_weights_b;
8+
#endif
9+
510
mat4 getModelMatrix() {
611
#ifdef DYNAMICBATCH
712
return getBoneMatrix(vertex_boneIndices);
@@ -37,6 +42,21 @@ vec4 getPosition() {
3742
localPos = localPos.xzy;
3843
#endif
3944

45+
#ifdef MORPHING
46+
#ifdef MORPHING_POS03
47+
localPos.xyz += morph_weights_a[0] * morph_pos0;
48+
localPos.xyz += morph_weights_a[1] * morph_pos1;
49+
localPos.xyz += morph_weights_a[2] * morph_pos2;
50+
localPos.xyz += morph_weights_a[3] * morph_pos3;
51+
#endif
52+
#ifdef MORPHING_POS47
53+
localPos.xyz += morph_weights_b[0] * morph_pos4;
54+
localPos.xyz += morph_weights_b[1] * morph_pos5;
55+
localPos.xyz += morph_weights_b[2] * morph_pos6;
56+
localPos.xyz += morph_weights_b[3] * morph_pos7;
57+
#endif
58+
#endif
59+
4060
vec4 posW = dModelMatrix * vec4(localPos, 1.0);
4161
#ifdef SCREENSPACE
4262
posW.zw = vec2(0.0, 1.0);

0 commit comments

Comments
 (0)