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:
- Reasearch how to Modify the source code
- Modifying blenders source code
- 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:
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:
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.
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:
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:
Then we right click on the blender icon and go to contents.
Then we go to scripts folder in all of these folders.
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”:
Now we have the file so what do we add:
1# SPDX-License-Identifier: Apache-2.02# Copyright 2018-2021 The glTF-Blender-IO authors.34import numpy as np5import bpy67from . import gltf2_blender_export_keys8from io_scene_gltf2.io.com import gltf2_io9from io_scene_gltf2.io.com import gltf2_io_constants10from io_scene_gltf2.io.com import gltf2_io_debug11from io_scene_gltf2.io.exp import gltf2_io_binary_data121314def gather_primitive_attributes(blender_primitive, export_settings):15 """16 Gathers the attributes, such as POSITION, NORMAL, TANGENT from a blender primitive.1718 :return: a dictionary of attributes19 """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 attributes293031def 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)3435 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)4243 assert array.dtype == dtype44 assert array.shape[1] == num_elems4546 amax = None47 amin = None48 if include_max_and_min:49 amax = np.amax(array, axis=0).tolist()50 amin = np.amin(array, axis=0).tolist()5152 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 )6667def __gather_cheese(blender_primitive, export_settings):68 # position = blender_primitive["attributes"]["POSITION"]69 depsgraph = bpy.context.evaluated_depsgraph_get()7071 obj = bpy.context.active_object.evaluated_get(depsgraph)7273 me = obj.data74 n = len(me.attributes['cheese'].data)75 vals = [0.] * n76 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=True91 )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()23obj = 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 Validator2void checkAttributeSemanticName(String semantic) {3 // Skip on custom semantics4 if (semantic.isNotEmpty && semantic.codeUnitAt(0) == 95 /*underscore*/) {5 return;6 }78 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];2122 if (!ATTRIBUTE_SEMANTIC_ARRAY_MEMBERS.contains(arraySemantic) ||23 semParts.length != 2) {24 context.addIssue(SemanticError.meshPrimitiveInvalidAttribute,25 name: semantic);26 break;27 }2829 var index = 0;30 var valid = true;31 final codeUnits = semParts[1].codeUnits;3233 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 }5051 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!