FDNavigate back to the homepage

Exporting Per Vertex Attributes from Blender into a GLTF Model

Rick
September 23rd, 2022 · 4 min read

This article was going to be about a directional sin wave per vertex, basically enabling breaking waves on a beach.

However this will have to be a later write up as to my suprise blender doesnt export per vertex attributes or import them for that matter.

So this article will be about exporting per vertex attributes from blender via modifiying blenders source code.

The general idea:

  1. Reasearch how to Modify the source code
  2. Modifying blenders source code
  3. Formatting the name of the attribute in the source code and the GLTF Validator

Reasearch how to Modify the source code

First of I looked at these pages:

  • three.js forum
  • GLTF Github issue
  • Blender issue

And came to the conclusion this wouldnt be easy 😂

So we know that the blender peeps are looking into this but sometimes if your in a tight spot you need some kind of solution before the next release!

So my secondary search sent me to this page:

Page about vertex colours and sRGB color management

My initial thoughts were to store xyz values in colors, and currently the only thing which might export is an unsigned byte color attribute using store named attribute in geometry nodes.

So the below wouldnt work, as the blender GLTF exporter doesnt support custom attributes.

Named attribute in blender

However I know for a fact GLTF2 format does support custom attributes!

If we try and convert xyz values of a position into colors mapped between 0-1 blender will store these colors in sRGB color space (or other color spaces) and mofifies the values with gamma correction or various other calculations..

I couldnt figure out why the values were slightly off from being converted to a color in blender and then being read out of the vertex color attribute +/- 0.0004 error roughly.

But when you think of the error it is actually quite big. Especially when you look at this article and how we convert numbers to be 0-1 values and saved as color.

So then this lead me to think can we use the source code of blender and append another primitive attribute to the GLTF model via the exporter?

And sure as hell.. we can!

⚠️I use blender through steam and get access to various versions here

Modifying blenders source code

How on gos green earth would we modify blenders source code. I dont know about you but it looks very very complicated. Dont let this intimidate you!

This article got me started on the road of the correct modification.

Now Im not saying this is bullet proof or extensible in any way. But if your in a tight spot and need a custom attribute you just created via blenders geometry nodes then keep reading..

Im going to go through how to find the file in question we need to modify.

Go to edit —> preferences in blender.

Search for gltf in the addons section.

You should see this:

GTLF2 Exported in addons section of preferences in blender

So this is my path:

/Users/rickthompson/Library/Application Support/Steam/steamapps/common/blender...

So we right click the finder on macos and select go to folder:

Go to folder with path we just got from blender addon preference screen for GLTF2 addon

Then we right click on the blender icon and go to contents.

Go to content by right clicking on the blender app in steamapps folder in finder

Then we go to scripts folder in all of these folders.

Scripts folder in blender steam folder

Now as you can see I just dragged this onto my quick folders section so when we try and open in vsCode its easier to access 🙂

Now if we go to vsCode and open this scripts folder we are set to start modifying!

I found the file by searching for “POSITION”:

Searching for position attribute in scripts folder of steam blender install

Now we have the file so what do we add:

1# SPDX-License-Identifier: Apache-2.0
2# Copyright 2018-2021 The glTF-Blender-IO authors.
3
4import numpy as np
5import bpy
6
7from . import gltf2_blender_export_keys
8from io_scene_gltf2.io.com import gltf2_io
9from io_scene_gltf2.io.com import gltf2_io_constants
10from io_scene_gltf2.io.com import gltf2_io_debug
11from io_scene_gltf2.io.exp import gltf2_io_binary_data
12
13
14def gather_primitive_attributes(blender_primitive, export_settings):
15 """
16 Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.
17
18 :return: a dictionary of attributes
19 """
20 attributes = {}
21 attributes.update(__gather_position(blender_primitive, export_settings))
22 attributes.update(__gather_normal(blender_primitive, export_settings))
23 attributes.update(__gather_tangent(blender_primitive, export_settings))
24 attributes.update(__gather_texcoord(blender_primitive, export_settings))
25 attributes.update(__gather_colors(blender_primitive, export_settings))
26 attributes.update(__gather_skins(blender_primitive, export_settings))
27 attributes.update(__gather_cheese(blender_primitive, export_settings))
28 return attributes
29
30
31def array_to_accessor(array, component_type, data_type, include_max_and_min=False):
32 dtype = gltf2_io_constants.ComponentType.to_numpy_dtype(component_type)
33 num_elems = gltf2_io_constants.DataType.num_elements(data_type)
34
35 if type(array) is not np.ndarray:
36 array = np.array(array, dtype=dtype)
37 print('array :', array)
38 print(' num_elems: ', num_elems)
39 print('shape: ', array.shape)
40 # if len(array.shape) == 1:
41 array = array.reshape(len(array) // num_elems, num_elems)
42
43 assert array.dtype == dtype
44 assert array.shape[1] == num_elems
45
46 amax = None
47 amin = None
48 if include_max_and_min:
49 amax = np.amax(array, axis=0).tolist()
50 amin = np.amin(array, axis=0).tolist()
51
52 return gltf2_io.Accessor(
53 buffer_view=gltf2_io_binary_data.BinaryData(array.tobytes()),
54 byte_offset=None,
55 component_type=component_type,
56 count=len(array),
57 extensions=None,
58 extras=None,
59 max=amax,
60 min=amin,
61 name=None,
62 normalized=None,
63 sparse=None,
64 type=data_type,
65 )
66
67def __gather_cheese(blender_primitive, export_settings):
68 # position = blender_primitive["attributes"]["POSITION"]
69 depsgraph = bpy.context.evaluated_depsgraph_get()
70
71 obj = bpy.context.active_object.evaluated_get(depsgraph)
72
73 me = obj.data
74 n = len(me.attributes['cheese'].data)
75 vals = [0.] * n
76 n = me.attributes['cheese'].data.values()
77 # m = n.foreach_get("vector", vals)
78 f = []
79 print('length: ', len(n))
80 for i in range(len(n)):
81 for j in range(3):
82 f.extend([n[i].vector[0], n[i].vector[1], n[i].vector[2]])
83 # data = map(lambda vector: n['vector'], n)
84 print('f after: ', len(f))
85 return {
86 "_cheese": array_to_accessor(
87 f,
88 component_type=gltf2_io_constants.ComponentType.Float,
89 data_type=gltf2_io_constants.DataType.Vec3,
90 include_max_and_min=True
91 )
92 }

This is the top of the file with my addition of gather cheese function.

This utlizes array_to_accessor function which we just mimic what we do for other attributes like POSITION.

I figured out we need to repeat each vertex 3 times which is why we do this:

1for i in range(len(n)):
2 for j in range(3):
3 f.extend([n[i].vector[0], n[i].vector[1], n[i].vector[2]])

We cant access the attribute as we need to evaluate it first as we have generated this attribute!

So we do this to evaluate it:

1depsgraph = bpy.context.evaluated_depsgraph_get()
2
3obj = bpy.context.active_object.evaluated_get(depsgraph)

With this we can now manipulate it into a format that can be stored as an attribute.

I would like to reiterate that this is not extensible and is hard coding a attribute to be stored in a GLTF Model. But name what ever attribute you like of type vec3.

Formatting the name of the attribute in the source code and the GLTF Validator

The one last thing you need to be aware of is you have to preappend the custom attribute in this file addons/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitive_attributes.py to have a underscore _.

The GLTF models have quite a comprehensive validator found here.

This file contains logic which will fail the name of the attribute if it doesnt get preappended with _ and ultimately make the model un useable:

1# GLTF Validator
2void checkAttributeSemanticName(String semantic) {
3 // Skip on custom semantics
4 if (semantic.isNotEmpty && semantic.codeUnitAt(0) == 95 /*underscore*/) {
5 return;
6 }
7
8 switch (semantic) {
9 case POSITION:
10 hasPosition = true;
11 break;
12 case NORMAL:
13 hasNormal = true;
14 break;
15 case TANGENT:
16 hasTangent = true;
17 break;
18 default:
19 final semParts = semantic.split('_');
20 final arraySemantic = semParts[0];
21
22 if (!ATTRIBUTE_SEMANTIC_ARRAY_MEMBERS.contains(arraySemantic) ||
23 semParts.length != 2) {
24 context.addIssue(SemanticError.meshPrimitiveInvalidAttribute,
25 name: semantic);
26 break;
27 }
28
29 var index = 0;
30 var valid = true;
31 final codeUnits = semParts[1].codeUnits;
32
33 if (codeUnits.isEmpty) {
34 valid = false;
35 } else if (codeUnits.length == 1) {
36 index = codeUnits[0] - 0x30 /* 0 */;
37 if (index < 0 || index > 9) {
38 valid = false;
39 }
40 } else {
41 for (var i = 0; i < codeUnits.length; ++i) {
42 final digit = codeUnits[i] - 0x30;
43 if (digit > 9 || digit < 0 || i == 0 && digit == 0) {
44 valid = false;
45 break;
46 }
47 index = 10 * index + digit;
48 }
49 }
50
51 if (valid) {
52 switch (arraySemantic) {
53 case COLOR_:
54 colorCount++;
55 maxColor = index > maxColor ? index : maxColor;
56 break;
57 case JOINTS_:
58 jointsCount++;
59 maxJoints = index > maxJoints ? index : maxJoints;
60 break;
61 case TEXCOORD_:
62 texCoordCount++;
63 maxTexcoord = index > maxTexcoord ? index : maxTexcoord;
64 break;
65 case WEIGHTS_:
66 weightsCount++;
67 maxWeights = index > maxWeights ? index : maxWeights;
68 break;
69 }
70 } else {
71 context.addIssue(SemanticError.meshPrimitiveInvalidAttribute,
72 name: semantic);
73 }
74 }
75}

Here is also a stackoverflow answerre-affirming this.

And checkout the console.log in this codesandbox geomtry —> attributes for the cube:

Final thoughts

If your in a tight spot and need an attribute to be exported from blender then this will definately help.

Foe example you can store any per vertex value like float or vec3 into an attribute and utilise in a shader. Wether this be a distance between two vectors or several noise values or even pre computed fmb noise.

Dont forget to rveert your changes in the files to not cause any future issues when done.

Many Many possibilities!

Next tile Im going to use this in example of how to do wave crashing on a shore line.

Stay safe and try it out!

More articles from theFrontDev

Creating Water Trails with Vertex Displacement on a GLTF Model

How to do water trails by displacing verticies and integrating a custom shader with a built in threejs shader, taking advantage of hdr images in a custom shader.

September 19th, 2022 · 5 min read

Creating light shafts from blender to a r3f project

A 101 on how to use geometry nodes and opacity masks in blender to generate some pretty cool looking light shafts for a threejs / react three fiber project.

September 4th, 2022 · 5 min read
© 2021–2024 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/