└── addons ├── .DS_Store └── so_fluffy ├── plugin.cfg ├── thickness_default.tres ├── turbulence_default.tres ├── so_fluffy_plugin.gd ├── density_default.tres ├── icon.svg ├── icon.svg.import ├── LICENSE.txt ├── shell_material.tres ├── so_fluffy.gdshader ├── README.md └── so_fluffy.gd /addons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxvolumedev/sofluffy/HEAD/addons/.DS_Store -------------------------------------------------------------------------------- /addons/so_fluffy/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="SoFluffy" 4 | description="" 5 | author="maxvolume" 6 | version="" 7 | script="so_fluffy_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/so_fluffy/thickness_default.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Curve" format=3 uid="uid://y6u8h4gp2j7v"] 2 | 3 | [resource] 4 | _data = [Vector2(0, 0), 0.0, 1.4, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] 5 | point_count = 2 6 | -------------------------------------------------------------------------------- /addons/so_fluffy/turbulence_default.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="NoiseTexture2D" load_steps=2 format=3 uid="uid://b04osmohfuo3"] 2 | 3 | [sub_resource type="FastNoiseLite" id="FastNoiseLite_30hqb"] 4 | frequency = 0.0095 5 | 6 | [resource] 7 | as_normal_map = true 8 | noise = SubResource("FastNoiseLite_30hqb") 9 | -------------------------------------------------------------------------------- /addons/so_fluffy/so_fluffy_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | # Initialization of the plugin goes here. 7 | add_custom_type("Fur", "Node", preload("so_fluffy.gd"), preload("icon.svg")) 8 | 9 | 10 | 11 | func _exit_tree(): 12 | # Clean-up of the plugin goes here. 13 | pass 14 | -------------------------------------------------------------------------------- /addons/so_fluffy/density_default.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="NoiseTexture2D" load_steps=3 format=3 uid="uid://c6yee2nx1q476"] 2 | 3 | [sub_resource type="Gradient" id="Gradient_k0srd"] 4 | offsets = PackedFloat32Array(0, 0.666368) 5 | colors = PackedColorArray(0.497474, 0.497474, 0.497474, 1, 1, 1, 1, 1) 6 | 7 | [sub_resource type="FastNoiseLite" id="FastNoiseLite_fnqm7"] 8 | noise_type = 2 9 | frequency = 0.0033 10 | 11 | [resource] 12 | color_ramp = SubResource("Gradient_k0srd") 13 | noise = SubResource("FastNoiseLite_fnqm7") 14 | -------------------------------------------------------------------------------- /addons/so_fluffy/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/so_fluffy/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bq8xlvd1em08i" 6 | path="res://.godot/imported/icon.svg-ee462722aacfe672b96520b23eca2882.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/so_fluffy/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-ee462722aacfe672b96520b23eca2882.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/so_fluffy/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Max Muermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /addons/so_fluffy/shell_material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://d23fcf8nvkobb"] 2 | 3 | [ext_resource type="Shader" path="res://addons/so_fluffy/so_fluffy.gdshader" id="1_4cy67"] 4 | 5 | [resource] 6 | render_priority = 0 7 | shader = ExtResource("1_4cy67") 8 | shader_parameter/height = 1.0 9 | shader_parameter/normal_strength = 0.5 10 | shader_parameter/static_direction_local = Vector3(0, 0, 0) 11 | shader_parameter/static_direction_world = Vector3(0, 0, 0) 12 | shader_parameter/h = 2.08165e-12 13 | shader_parameter/density = 0.14 14 | shader_parameter/seed = 0 15 | shader_parameter/scruffiness = 0.5 16 | shader_parameter/use_heightmap_texture = false 17 | shader_parameter/turbulence_strength = 0.5 18 | shader_parameter/jitter_strength = 0.0 19 | shader_parameter/color = null 20 | shader_parameter/use_height_gradient = false 21 | shader_parameter/scale_height_gradient = true 22 | shader_parameter/use_albedo_texture = false 23 | shader_parameter/use_emission = false 24 | shader_parameter/emission_color = null 25 | shader_parameter/emission_energy_multiplier = null 26 | shader_parameter/use_emission_texture = false 27 | shader_parameter/use_thickness_curve = false 28 | shader_parameter/thickness_scale = 1.0 29 | shader_parameter/physics_pos_offset = Vector3(0, -1, 0) 30 | shader_parameter/physics_rot_offset = null 31 | metadata/is_fur = true 32 | -------------------------------------------------------------------------------- /addons/so_fluffy/so_fluffy.gdshader: -------------------------------------------------------------------------------- 1 | shader_type spatial; 2 | 3 | render_mode cull_disabled; //, unshaded 4 | 5 | uniform float height = 0; 6 | uniform float normal_strength = 1.0; 7 | uniform vec3 static_direction_local = vec3(0.0,0.0,0.0); 8 | uniform vec3 static_direction_world = vec3(0.0,0.0,0.0); 9 | uniform float h = 0; 10 | uniform float density = 1.0; 11 | uniform bool render_skin = true; 12 | uniform int seed = 0; 13 | uniform float scruffiness = 0.5; 14 | uniform sampler2D heightmap_texture : source_color, filter_nearest; 15 | uniform bool use_heightmap_texture = false; 16 | uniform sampler2D turbulence_texture : filter_linear; 17 | uniform float turbulence_strength = 0.5; 18 | uniform sampler2D jitter_texture : filter_linear; 19 | uniform float jitter_strength = 0; 20 | uniform bool curls_enabled = true; 21 | uniform float curls_twist = 24.0; 22 | uniform float curls_fill = 0.7853981634; 23 | 24 | 25 | 26 | uniform vec3 color: source_color; 27 | uniform sampler2D height_gradient: source_color, filter_nearest; 28 | uniform bool use_height_gradient = false; 29 | uniform bool scale_height_gradient = true; 30 | uniform bool use_albedo_texture = false; 31 | uniform sampler2D albedo_texture : source_color; 32 | 33 | uniform bool use_emission = false; 34 | uniform vec3 emission_color: source_color; 35 | uniform float emission_energy_multiplier; 36 | uniform bool use_emission_texture = false; 37 | uniform sampler2D emission_texture : source_color; 38 | 39 | 40 | uniform sampler2D thickness_curve; 41 | uniform bool use_thickness_curve = false; 42 | uniform float thickness_scale = 1.0; 43 | 44 | // physics 45 | uniform vec3 physics_pos_offset = vec3(0.0, 0.0, 0.0); 46 | uniform mat3 physics_rot_offset; 47 | 48 | // Gold Noise ©2015 dcerisano@standard3d.com 49 | // - based on the Golden Ratio 50 | // - uniform normalized distribution 51 | // - fastest static noise generator function (also runs at low precision) 52 | // - use with indicated fractional seeding method. 53 | // @see https://www.shadertoy.com/view/ltB3zD 54 | float gold_noise(vec2 p) { 55 | p = p + vec2(float(seed), float(seed)); 56 | vec3 p3 = fract(vec3(p.xyx) * .1031); 57 | p3 += dot(p3, p3.yzx + 33.33); 58 | return fract((p3.x + p3.y) * p3.z); 59 | } 60 | 61 | float atan2(in float y, in float x) { 62 | bool s = (abs(x) > abs(y)); 63 | return mix(PI/2.0 - atan(x,y), atan(y,x), float(s)); 64 | } 65 | 66 | // fast atan2 approximation, returns an angle in the range -PI..PI 67 | // @see https://www.shadertoy.com/view/WtyyWD 68 | float atan2_approximation2( float x, float y ) { 69 | if ( x == 0.0f ) { 70 | if ( y > 0.0f ) return PI/2.0; 71 | if ( y == 0.0f ) return 0.0f; 72 | return -PI/2.0; 73 | } 74 | float atan; 75 | float z = y/x; 76 | if ( abs( z ) < 1.0f ) { 77 | atan = z/(1.0f + 0.28f*z*z); 78 | if ( x < 0.0f ) { 79 | if ( y < 0.0f ) return atan - PI; 80 | return atan + PI; 81 | } 82 | } 83 | else { 84 | atan = PI/2.0 - z/(z*z + 0.28f); 85 | if ( y < 0.0f ) return atan - PI; 86 | } 87 | return atan; 88 | } 89 | 90 | void vertex() { 91 | vec3 physics_pos_offset_world = (vec4(physics_pos_offset, 0.0) * MODEL_MATRIX).xyz * h * 0.2; 92 | vec3 direction = static_direction_local + (vec4(static_direction_world, 0.0) * MODEL_MATRIX).xyz; 93 | VERTEX += (NORMAL * normal_strength + direction * (1.0 - normal_strength)) * height * h + physics_pos_offset_world; 94 | VERTEX = VERTEX * physics_rot_offset; 95 | } 96 | 97 | void fragment() { 98 | vec2 uv = UV * density; // current pixel UV, scaled by density 99 | 100 | float ts = 1024.0; 101 | vec2 xy = uv; 102 | 103 | if(turbulence_strength > 0.0) { 104 | vec2 turbulence = texture(turbulence_texture, uv).xy - vec2(0.5, 0.5); 105 | 106 | vec2 displacement = turbulence * turbulence_strength * 0.1; 107 | xy = xy + displacement * pow((1.0 - h * 0.5), 1.0 * turbulence_strength) * 1.0; 108 | } 109 | 110 | xy = xy + (texture(jitter_texture, UV).xy - vec2(0.5, 0.5)) * jitter_strength / 200.0; 111 | 112 | // density noise 113 | float noise = 0.0; 114 | 115 | if (scruffiness > 0.0 || curls_enabled) { 116 | noise = gold_noise(floor(xy * 1024.0)); 117 | } 118 | float len = scruffiness <= 0.0 ? 1.0 : pow(noise, scruffiness); 119 | 120 | if(use_heightmap_texture) { 121 | vec3 val = texture(heightmap_texture, UV).rgb; // sample heightmap length texture at original UV - we don't care how far the strand has been displaced 122 | len *= val.r; 123 | } 124 | 125 | // calculate distance of current pixel from center of texture pixel 126 | vec2 center = vec2(0.5, 0.5); 127 | vec2 center_offset = fract(xy * ts) - center; 128 | float relDist = length(center_offset) / thickness_scale * 2.0; 129 | 130 | float thickness; 131 | if(use_thickness_curve) { 132 | thickness = texture(thickness_curve, vec2(clamp(h / len, 0.0, 0.99), 0.0)).r * len; 133 | } else { 134 | thickness = h; 135 | } 136 | 137 | // calculate curls - circle segments and angle offsets 138 | bool curl = true; 139 | if (curls_enabled) { 140 | float a = mod(atan2_approximation2(center_offset.y, center_offset.x) + h * curls_twist + noise * 1024.0, 2.0 * PI); 141 | curl = a > 0.0 && a < curls_fill; 142 | } 143 | 144 | if(curl && (len - thickness >= relDist && h < len || (render_skin && h == 0.0))) { 145 | if(!FRONT_FACING) NORMAL = -NORMAL; 146 | vec3 col = color; 147 | 148 | // sample height gradient 149 | float grad_h = h; 150 | if(scale_height_gradient) { 151 | grad_h /= len; 152 | } 153 | 154 | vec3 height_col; 155 | if(use_height_gradient) { 156 | height_col = texture(height_gradient, vec2(grad_h * 0.999, 0.0)).rgb; 157 | } else { 158 | float hg = pow(h, 0.9); 159 | height_col = vec3(hg,hg,hg); 160 | } 161 | col *= height_col; 162 | 163 | if(use_albedo_texture) { 164 | col *= texture(albedo_texture, UV).rgb; 165 | } 166 | ALBEDO = col; 167 | if(use_emission) { 168 | vec3 emission = emission_color; 169 | if(use_emission_texture) { 170 | emission += texture(emission_texture, UV).rgb; 171 | } 172 | EMISSION = emission * emission_energy_multiplier; 173 | } 174 | } else { 175 | discard; 176 | } 177 | } 178 | 179 | void light() { 180 | // LAMBERT 181 | //DIFFUSE_LIGHT += clamp(dot(NORMAL, LIGHT), 0.0, 1.0) * ATTENUATION * LIGHT_COLOR; 182 | 183 | // HALF-LAMBERT 184 | float d = dot(NORMAL, LIGHT) / 2.0 + 0.5; 185 | DIFFUSE_LIGHT += d * d * ATTENUATION * LIGHT_COLOR; 186 | } 187 | -------------------------------------------------------------------------------- /addons/so_fluffy/README.md: -------------------------------------------------------------------------------- 1 | ![SO FLUFFY](screenshots/title.png) 2 | 3 | # SO FLUFFY! 4 | 5 | High-quality, endlessly configurable fur for Godot 4 with bouncy physics. 6 | 7 | # What is it? 8 | 9 | SO FLUFFY is a shell fur rendering system for Godot 4. Shell rendering involves rendering multiple copies of an object's geometry, scaling each one up a little more, thus creating a series or "shells" of increasing size surrounding the object. 10 | 11 | A shader is then used to render cross-sections of fur strands on each shell. 12 | 13 | ![LOD demo](screenshots/bee.png) 14 | 15 | # Features 16 | 17 | - Performance 18 | - Material-based shell generation, no geometry is duplicated. SO FLUFFY uses a cascade of next_pass materials for subsequent shells and performs all geometry operations in its vertex shader 19 | - dynamic LODs through disabling shells based on camera distance 20 | - Control over strand growth: 21 | - fur density (strands per area) 22 | - scruffiness (length distribution of strands) 23 | - heightmap texture for precise control over where strands can grow 24 | - turbulence and jitter - overlay noise for displacing strands for a more organic look, or to model things like cowlicks 25 | - thickness profile - control the thickness of strands over their length to produce finer or thicker hair, or other organic shapes like moss or fungus 26 | - Fur can grow along surface normals, in a fixed direction relative to the object, or in a fixed direction in world space, or any combination of those 27 | - Twist strands into curls 28 | - Control over appearance: 29 | - color gradient applied along the length of each strand, usefule for simulating self-shadowing or other effects (see demos) 30 | - albedo color - solid color or texture 31 | - emission - solid color or texture 32 | - Physics 33 | - linear spring physics for satisfying bounciness on linear movement 34 | - rotational spring physics for satisfying swishiness on rotation 35 | - rotational physics effects can be scaled independently 36 | 37 | # Quick start 38 | 39 | 1. install the plugin by placing the so_fluffy folder inside the "addons" folder in your project 40 | 2. open Godot "Project Settings->Plugins" and enable the SoFluffy plugin 41 | 3. Add some geometry to your scene - any subclass of GeometryInstance3D can be used to grow fur. 42 | 4. Add a Fur node as a child of your geometry and tweak parameters to your liking. 43 | 44 | # Caveats 45 | 46 | ## Performance 47 | 48 | Shell rendering is not exactly cheap. The major driver of rendering cost is the number of shells - the cost of rendering is O(N). Some features incur additional performance cost - any time a texture is used (height map, turbulence, albedo, emission, thickness curve, height gradient), more texture samples are required, which places extra load on the GPU. 49 | 50 | ### Performance tips: 51 | 52 | - keep the number of shells as low as possible 53 | - minimse use of texture lookups (see above) 54 | - turn on dynamic LOD 55 | - keep your geometry simple - each shell has to render each triangle in your original geometry. Use simpler representations of your geometry for fur - the inherent noisiness of the fur can often mask the loss of geometric detail 56 | 57 | ## Noise and other artefacts 58 | 59 | Because strands are rendered as a series of (infinitely) thin shells, viewing fur side-on causes a lot of visual noise. Two common approaches to solve this issues are to render textured "fins" perpendicular to the camera, or to perform some sort of post-process blurring in screen space. 60 | 61 | Fin textures generally need to be hand-crafted to match the look of the fur being rendered; this addon does not intend to provide a fin rendering implementation. 62 | 63 | SO FLUFFY also does not currently provide any post-processing to reduce noise, this may change in the future. 64 | 65 | # LODs 66 | 67 | SO FLUFFY comes with a simple dynamic LOD system. Since performance is mainly dependent on the number of shells being rendered, reducing the number of shells for far-away objets can help control the performance load. 68 | 69 | LODs are generated dynamically by dropping shells based on distance. This is done per-object, so if you're rendering terrain, it is advisable to break up the terrain into a number of separate, sufficiently small tiles to take advantage of this feature. 70 | 71 | ![LOD demo](screenshots/LOD.png) 72 | 73 | # Usage 74 | 75 | ## Setup 76 | 77 | SO FLUFFY can be applied to any geometry in Godot that inherits from GeometryNode3D. Simply add a "Fur" node as a child of the geometry you want to grow fur on. 78 | 79 | ## Targeting 80 | 81 | By default, SO FLUFFY uses the Geometry Node's Material Overlay to render fur. 82 | 83 | If attached to a MeshInstance3D, you can optionally configure the fur system to render on one or more surfaces instead. To do so, configure the surface indices in the "Targeting" section. 84 | 85 | # Demos 86 | 87 | SO FLUFFY comes with a number of demo scenes that illustrate some of the system's features. 88 | 89 | - basic_hair: basic usage - simple hair, slight turbulence and basic physics setup 90 | - lod_test: demonstrates the effect of the dynamic LOD by showing LOD and FPS stats for an object moving away from the camera 91 | - hedgehog: uses the following features to render something approximating the spikes of a hedgehog: 92 | - heightmap texture (to control where the spikes are grown) 93 | - rotational physics scaling to mostly disable rotational physics 94 | - height gradient for stripy spikes 95 | - thickness curve for the spike shape 96 | - enoki: uses the thickness curve to render a large field of mushroom-shaped strands 97 | - bee: a fuzzy animated bee, demonstrating use of a skinned mesh, surface targeting, and Albedo texture. Bee model courtesy of https://github.com/gdquest-demos/godot-4-3D-Characters 98 | - curls: curly hair 99 | 100 | ![LOD demo](screenshots/enoki.png) 101 | 102 | # Fur Parameters 103 | 104 | ## General 105 | 106 | ### Preview in Editor 107 | 108 | For configuring fur parameters, it is very handy to be able to preview the fur in the editor. However, since fur rendering comes at a performance cost, it is often advisable to turn off the editor preview. Enabling this feature will clear and re-generate all shells from scratch. 109 | 110 | This setting does not affect runtime behaviour - fur rendering is always enabled at runtime. 111 | 112 | ## Targeting 113 | 114 | ### Target Surfaces 115 | 116 | Indices of surfaces to apply fur to. If empty, fur is applied to the entire mesh as a single overlay Material. Otherwise, fur is applied only to the specified surfaces. 117 | 118 | ## Performance and LODs 119 | 120 | ### Number of shells 121 | 122 | The maximum number of shells used to render the fur. More shells are more expensive to render. For fur that is intended to be seen close up, 128 or even 256 shells may be desirable - but balance this with performance concerns. For far-away fur, or things like distant vegetation, values as low as 16 shells may be entirely sufficient. 123 | 124 | Note that if LOD is enabled, the LOD settings affect how many shells are actually rendered; this is the upper limit. 125 | 126 | ### LOD enabled 127 | 128 | Turn dynamic LOD on or off. If LOD is off, the fur is always rendered with the maximum number of shells. 129 | 130 | LOD switching is based on bounding-box distance to the camera. You may want to set the distances based on screen resolution and camera FOV for your specific use case. 131 | 132 | Since dropping shells for lower LODs ultimately results in fewer fur pixels being rendered, fur appears "thinner" at less detailed LOD levels. The LOD system compensates for this by adjusting the thickness of each strand to maintain a consistent visual weight over the entire LOD range. 133 | 134 | ### LOD Min Distance 135 | 136 | Minimum distance from the camera at which lower-detail LODs are used. Any objects close than this distance will be rendered with highest quality. 137 | 138 | ### LOD Max Distance 139 | 140 | Distance from the camera at which the lowest level LOD is used. Any objects further from the camera will be rendered at the lowest quality. 141 | 142 | ### LOD Minimum Shells 143 | 144 | The number of shells to use for the lowest-quality LOD. Default and lower bound is 8, which should be a good value in most cases. 145 | 146 | ## Shape and Growth 147 | 148 | ### Length 149 | 150 | Strand length. This determines how much the shells are scaled up. Longer strands require more shells to render - balance this with performance concerns. 151 | 152 | ### Density 153 | 154 | Scaling of the fur density - strands per area. Higher numbers make the fur more dense. So FLUFFY uses UV0 coordinates for seeding noise, so you need to make sure your model has reasonable evenly-spaced UVs. 155 | 156 | ### Scruffiness 157 | 158 | Variation of the height distribution of strands. Higher values for a more scruffy look. 159 | 160 | ### Heightmap Texture 161 | 162 | Fur heightmap texture. Values scale hair length by [1..0[. Black pixels are not rendered, so the underlying skin material will be visible. 163 | 164 | ### Strand Thickness 165 | 166 | #### Thickness Curve 167 | 168 | Thickness profile of a single strand. Note that the values are inverted (1 it thin, 0 is thick) so that the curve presets can be used. 169 | 170 | This curve can be used to achieve some interesting effects - see the included Enoki demo scene. 171 | 172 | #### Thickness Scale 173 | 174 | Uniformly scales up th thickness of all strands. Thicker strands give the visual impression of denser fur. 175 | 176 | ### Curls 177 | 178 | #### Curls Enabled 179 | 180 | Turn curls rendering on or off. Curls are quite expensive to render. 181 | 182 | #### Curls Twist 183 | 184 | How twisty the curls should be. 185 | 186 | ### Curls Fill 187 | 188 | Controls the "thickness" of curls - higher twists and lower numbers of shells generally need more fill. 189 | 190 | ### Turbulence and Jitter 191 | 192 | #### Turbulence Texture 193 | 194 | Noise texture to overlay turbulence on the fur. Uses r and g channels to calculate a displacement vector, so is best provided as a normal map. Turbulence scales with density. 195 | 196 | #### Turbulence Strength 197 | 198 | Strength of the turbulence effect. Higher numbers apply more turbulence. 199 | 200 | #### Jitter Texture 201 | 202 | Noise texture to overlay UV-space turbulence on the fur. Uses r and g channels to calculate a displacement vector, so is best provided as a normal map. Jitter does not scale with density. 203 | 204 | #### Jitter Strength 205 | 206 | Strength of the Jitter noise effect. 207 | 208 | ### Growth Direction 209 | 210 | #### Normal Strength 211 | 212 | Blends the fur growth direction between the surface normal and the static directions below. A value of 1 means fur grows only in the direction of normals, a value of 0 means it grows only in a static direction. 213 | 214 | #### Static Direction Local 215 | 216 | Static direction of fur growth in object space. This is useful for fur that grows in a specific direction but moves with the object, such as a stiff mane or a mohawk. 217 | 218 | #### Static Direction World 219 | 220 | Static direction of fur growth in world space. This is useful for fur that grows in a specific direction in world coordinates, such as grass, which always grows upwards. 221 | 222 | 223 | ## Appearance 224 | 225 | ### Height Gradient 226 | 227 | Albedo color is multiplied by this gradient, sampled by relative height. The default gradient simulates ambient occlusion. 228 | 229 | If no gradient is provided, a simple (cheaper) power function is used to achieve the effect. 230 | 231 | ### Scale Height Gradient 232 | 233 | Should the height gradient be scaled with the length of individual strands? If true, each strand will use the full gradient, otherwise shorter strands only use a partial gradient. 234 | 235 | ### Render Skin 236 | 237 | If enabled, all pixels on shell 0 are rendered. Otherwise, non-strand pixels are transparent. This is useful if you do not want to incur the overhead of a dedicated skin material. Defaults to false. 238 | 239 | ### Albedo 240 | 241 | #### Albedo Color 242 | 243 | Plain hair color. This color is multiplied by the height gradient, and the albedo texture, if provided. Think of it as the base "tint" of the fur. 244 | 245 | #### Albedo Texture 246 | 247 | Texture defining hair color. Albedo color is [i]multiplied[\i] by the texture color. 248 | 249 | ### Emission 250 | 251 | #### Use Emission 252 | 253 | Enable/disable rendering of emission component. 254 | 255 | #### Emission Color 256 | 257 | Uniform emission color. 258 | 259 | #### Emission Energy Multiplier 260 | 261 | Emission energy multiplier. Higher numbers make the emission brighter. 262 | 263 | #### Emission Texture 264 | 265 | Texture defining emission color. Emission color is [i]added[i] to the texture color. 266 | 267 | ## Physics 268 | 269 | ### Physics Enabled 270 | 271 | Disable physics processing altogether. Physics simulation is very cheap, but should be disabled if the fur will not be subject to any movement. 272 | 273 | ### Physics Preview 274 | 275 | Simulate physics in the editor. Physics simulation is very cheap, but can be distracting while editing. 276 | 277 | ### Rotational Physics Scale 278 | 279 | Adjust the magnitude of rotational physics effects, relative to those of the linear physics. This is useful to model more rigid fur - see the Hedgehog demo for an example where rotational physics are scaled down. 280 | 281 | ### Gravity 282 | 283 | Constant gravity affecting the fur. 284 | 285 | ### Spring Constant 286 | 287 | Defines the spring constant. Higher values mean a stronger spring. 288 | 289 | ### Mass 290 | 291 | Strand mass - higher numbers make the hair more resistant to movement. 292 | 293 | ### Damping 294 | 295 | Spring damping - higher values make the fur move more slowly and suppress oscillations. 296 | 297 | ### Stretch 298 | 299 | Values greater than 1 allow strands to stretch beyond Length. This gives the visual impression of more elastic, flowy fur. 300 | 301 | ### Stiffness 302 | 303 | Controls how stiff the strands are over their length - higher numbers make the strands more bendy, lower numbers give a more bristly look. -------------------------------------------------------------------------------- /addons/so_fluffy/so_fluffy.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | ## Show fur in editor. Fur rendering is relatively expensive, so it is recommened to disable this when not needed. 5 | @export 6 | var preview_in_editor: bool = true: 7 | set(v): 8 | var previous_value = preview_in_editor 9 | preview_in_editor = v 10 | if Engine.is_editor_hint(): 11 | clear_materials() 12 | if preview_in_editor: 13 | create_materials() 14 | setup_materials() 15 | init_physics() 16 | 17 | @export_group("Targeting") 18 | 19 | ## Indices of surfaces to apply fur to. If empty, fur is applied to the entire mesh as a single overlay Material. Otherwise, fur is applied only to the specified surfaces. 20 | @export var target_surfaces : Array[int] = []: 21 | set(v): 22 | clear_materials() 23 | target_surfaces = v 24 | create_materials() 25 | setup_materials() 26 | 27 | @export_group("Shells and LOD") 28 | 29 | ## Number of shells to generate. Higher numbers look better, but incur larger performance penalties. 30 | @export 31 | var number_of_shells: int = 64: 32 | set(v): 33 | number_of_shells = v 34 | clear_materials() 35 | create_materials() 36 | setup_materials() 37 | 38 | ## Enable or disable dynamic LOD. If disabled, fur will always be rendered with the maximum number of shells. 39 | @export var lod_enabled: bool = false: 40 | set(v): 41 | lod_enabled = v 42 | if !lod_enabled: 43 | lod = 0 44 | apply_lod() 45 | setup_materials() 46 | else: 47 | apply_lod() 48 | setup_materials() 49 | notify_property_list_changed() 50 | 51 | var lod: int = 0: 52 | set(v): 53 | var old_lod = lod 54 | lod = v 55 | if lod != old_lod: 56 | apply_lod() 57 | setup_materials() 58 | 59 | 60 | ## Minimum distance from the camera at which lower-detail LODs are used. 61 | @export_range(0, 10, 0.01, "or_greater") 62 | var lod_min_distance = 3.0 63 | 64 | ## Distance from the camera at which the lowest level LOD is used. 65 | @export_range(0, 50, 0.01, "or_greater") 66 | var lod_max_distance = 25.0 67 | 68 | ## Number of shells to use for the lowest-quality LOD. Default and lower bound is 8, which should be a good value in most cases. 69 | @export_range(8, 256, 1, "or greater") 70 | var lod_minimum_shells: int = 8 71 | 72 | 73 | ## Parameters that affect the growth of the fur - density, length, turbulence, etc. 74 | @export_group("Shape and Growth") 75 | 76 | ## Strand length 77 | @export_range(0, 2, 0.01, "or_greater") 78 | var length: float = 0.1: 79 | set(v): 80 | length = v 81 | setup_materials() 82 | 83 | ## Scaling of the fur density - strands per area. Higher numbers make the fur more dense. 84 | @export_range(0.010, 3, 0.001, "or_greater") 85 | var density: float = 0.5: 86 | set(v): 87 | density = v 88 | setup_materials() 89 | 90 | ## seed for fur noise random generator 91 | @export 92 | var seed: int = RandomNumberGenerator.new().randi_range(0, 65535): 93 | set(v): 94 | seed = v 95 | setup_materials() 96 | 97 | ## Variation of the height distribution of strands. Higher values for a more scruffy look. 98 | @export_range(0.0, 4, 0.001, "or_greater") 99 | var scruffiness: float = 0.5: 100 | set(v): 101 | scruffiness = v 102 | setup_materials() 103 | 104 | ## Fur heightmap texture. Values scale hair length by [1..0[. Black pixels are not rendered. 105 | @export 106 | var heightmap_texture: Texture2D: # = preload("res://addons/so_fluffy/density_default.tres").duplicate(): 107 | set(v): 108 | heightmap_texture = v 109 | setup_materials() 110 | 111 | 112 | @export_subgroup("Strand Thickness") 113 | 114 | ## Thickness profile of a single strand. The values are inverted (1 it thin, 0 is thick) so that the curve presets can be used. 115 | @export 116 | var thickness_curve: CurveTexture: 117 | set(v): 118 | thickness_curve = v 119 | setup_materials() 120 | 121 | ## Uniformly scales the thickness of all strands. 122 | @export_range(0.01, 4.0, 0.01, "or_greater") 123 | var thickness_scale: float = 1.5: 124 | set(v): 125 | thickness_scale = v 126 | setup_materials() 127 | 128 | 129 | @export_subgroup("Curls") 130 | 131 | ## Turn curls rendering on or off. Curls are quite expensive to render. 132 | @export var curls_enabled: bool = false: 133 | set(v): 134 | curls_enabled = v 135 | setup_materials() 136 | notify_property_list_changed() 137 | 138 | @export_range(0, 128, 0.01, "or_greater") 139 | var curls_twist: float = 48.0: 140 | set(v): 141 | curls_twist = v 142 | setup_materials() 143 | 144 | @export_range(0, 2 * PI, 0.01, "or_greater") 145 | var curls_fill: float = PI / 4.0: 146 | set(v): 147 | curls_fill = v 148 | setup_materials() 149 | 150 | 151 | 152 | @export_subgroup("Turbulence and Jitter") 153 | 154 | ## Noise texture to overlay displacement turbulence on the fur. Best provided as a normal map. 155 | @export 156 | var turbulence_texture: Texture2D = preload("res://addons/so_fluffy/turbulence_default.tres").duplicate(): 157 | set(v): 158 | turbulence_texture = v 159 | setup_materials() 160 | 161 | ## Strength of the turbulence effect. Higher numbers apply more turbulence. 162 | @export_range(0, 1, 0.001, "or_greater") 163 | var turbulence_strength: float = 0.3: 164 | set(v): 165 | turbulence_strength = v 166 | setup_materials() 167 | 168 | ## Noise texture to add high-frequency jitter on the fur. 169 | @export 170 | var jitter_texture: Texture2D = preload("res://addons/so_fluffy/turbulence_default.tres").duplicate(): 171 | set(v): 172 | jitter_texture = v 173 | setup_materials() 174 | 175 | ## Strength of the jitter effect. 176 | @export_range(0, 1, 0.001, "or_greater") 177 | var jitter_strength: float = 0.0: 178 | set(v): 179 | jitter_strength = v 180 | setup_materials() 181 | 182 | @export_subgroup("Growth Direction") 183 | 184 | ## Blends the fur growth direction between the surface normal and the static directions below. A value of 1 means fur grows only in the direction of normals, a value of 0 means it grows only in a static direction. 185 | @export_range(0, 1, 0.005) 186 | var normal_strength: float = 1.0: 187 | set(v): 188 | normal_strength = v 189 | setup_materials() 190 | 191 | ## Static direction of fur growth in object space. This is useful for fur that grows in a specific direction but moves with the object, such as a mane or a mohawk. 192 | @export 193 | var static_direction_local: Vector3 = Vector3.ZERO: 194 | set(v): 195 | static_direction_local = v 196 | setup_materials() 197 | 198 | ## Static direction of fur growth in world space. This is useful for fur that grows in a specific direction in world coordinates, such as grass, which always grows upwards. 199 | @export 200 | var static_direction_world: Vector3 = Vector3.ZERO: 201 | set(v): 202 | static_direction_world = v 203 | setup_materials() 204 | 205 | 206 | 207 | # Material parameters 208 | @export_group("Appearance") 209 | 210 | var shell_material: Material = preload("res://addons/so_fluffy/shell_material.tres").duplicate(); 211 | 212 | ## Albedo color is multiplied by this gradient, sampled by relative height. The default gradient simulates ambient occlusion. 213 | @export 214 | var height_gradient: GradientTexture2D: 215 | set(v): 216 | height_gradient = v 217 | setup_materials() 218 | 219 | ## Should the height gradient be scaled with the length of individual strands? If true, each strand will use the full gradient. 220 | @export 221 | var scale_height_gradient: bool = false: 222 | set(v): 223 | scale_height_gradient = v 224 | setup_materials() 225 | 226 | ## If enabled, all pixels on shell 0 are rendered. Otherwise, non-strand pixels are transparent. This is useful if you do not want to incur the overhead of a dedicated skin material. 227 | @export 228 | var render_skin: bool = false: 229 | set(v): 230 | render_skin = v 231 | setup_materials() 232 | 233 | ## Albedo 234 | @export_subgroup("Albedo") 235 | ## Plain hair color 236 | @export_color_no_alpha 237 | var albedo_color: Color = Color.LIGHT_BLUE: 238 | set(v): 239 | albedo_color = v 240 | setup_materials() 241 | 242 | ## Texture defining hair color. Albedo color is [i]multiplied[\i] by the texture color. 243 | @export 244 | var albedo_texture: Texture2D: 245 | set(v): 246 | albedo_texture = v 247 | setup_materials() 248 | 249 | @export_subgroup("Emission") 250 | 251 | 252 | ## Emission 253 | @export 254 | var use_emission: bool = false: 255 | set(v): 256 | use_emission = v 257 | setup_materials() 258 | notify_property_list_changed() # necessary to trigger _validate_property, apparently 259 | 260 | ## Uniform emission color 261 | @export_color_no_alpha 262 | var emission_color: Color: 263 | set(v): 264 | emission_color = v 265 | setup_materials() 266 | 267 | ## Emission energy multiplier. Higher numbers make the emission brighter. 268 | @export_range(0, 16, 0.01) 269 | var emission_energy_multiplier: float = 1.0: 270 | set(v): 271 | emission_energy_multiplier = v 272 | setup_materials() 273 | 274 | ## Texture defining emission color. Emission color is [i]added[i] to the texture color. 275 | @export 276 | var emission_texture: Texture2D: 277 | set(v): 278 | emission_texture = v 279 | setup_materials() 280 | 281 | 282 | @export_group("Physics") 283 | 284 | # physics parameters are set in _process, no need to call setup_materials() 285 | ## Disable physics processing altogether. Physics simulation is very cheap, but should be disabled if the fur will not be subject to any movement. 286 | @export var physics_enabled: bool = true: 287 | set(v): 288 | physics_enabled = v 289 | init_physics() 290 | notify_property_list_changed() 291 | 292 | ## Simulate physics in the editor. Physics simulation is very cheap, but can be distracting while editing. 293 | @export var physics_preview: bool = true: 294 | set(v): 295 | physics_preview = v 296 | if(!physics_preview): 297 | setup_materials() 298 | init_physics() 299 | ## adjust the magnitude of rotational physics effects 300 | @export var rotational_physics_scale: float = 1.0 301 | ## gravity constant 302 | @export var gravity: Vector3 = Vector3(0,0,0) 303 | ## strand spring spring_constant 304 | @export var spring_constant: float = 80 305 | ## strand mass - higher numbers make the hair more resistant to movement 306 | @export var mass: float = 0.15 307 | ## spring damping 308 | @export var damping: float = 3 309 | ## allow strand to stretch beyond its length for a more elastic appearance. A value of 1 means no stretch is allowed. 310 | @export_range(1, 2, 0.01, "or_greater") var stretch: float = 1.0 311 | 312 | ## controls how stiff the strands are over their length - higher numbers make the strands more bendy 313 | @export_range(0, 4, 0.01, "or_greater") 314 | var stiffness: float = 1.0 315 | 316 | 317 | 318 | var lod_shell_count: int = 0 319 | 320 | # the geometry we're growing fur on 321 | var mesh: GeometryInstance3D 322 | 323 | # the generated shell materials 324 | var shells: Array[Material] = [] 325 | 326 | # shells for current LOD 327 | var lod_shells: Array[Material] = [] 328 | 329 | # linear spring physics state 330 | var previous_position: Vector3 331 | var spring_offset: Vector3 = Vector3.ZERO 332 | var spring_velocity: Vector3 = Vector3.ZERO 333 | 334 | # rotational spring physics state 335 | var previous_rotation: Vector3 = Vector3.ZERO 336 | var spring_rotation: Vector3 = Vector3.ZERO 337 | var spring_angular_velocity: Vector3 = Vector3.ZERO 338 | 339 | func _validate_property(property: Dictionary): 340 | # hide/show emission section details 341 | if property.name in ["emission_color", "emission_energy_multiplier", "emission_texture"] and !use_emission: 342 | property.usage = PROPERTY_USAGE_NO_EDITOR 343 | # hide/show curls section details 344 | if property.name in ["curls_twist", "curls_fill"] and !curls_enabled: 345 | property.usage = PROPERTY_USAGE_NO_EDITOR 346 | # hide/show physics section details 347 | if property.name in ["physics_preview", "gravity", "spring_constant", "mass", "damping", "stretch"] and !physics_enabled: 348 | property.usage = PROPERTY_USAGE_NO_EDITOR 349 | # hide/show LOD section details 350 | if property.name in ["lod_min_distance", "lod_max_distance"] and !lod_enabled: 351 | property.usage = PROPERTY_USAGE_NO_EDITOR 352 | 353 | func _ready(): 354 | mesh = get_parent() 355 | clear_materials() 356 | create_materials() 357 | lod = 0 358 | lod_shell_count = number_of_shells 359 | apply_lod() 360 | setup_materials() 361 | init_physics() 362 | notify_property_list_changed() 363 | 364 | 365 | func _enter_tree(): 366 | pass 367 | 368 | # remove fur material 369 | func clear_materials(): 370 | if(mesh == null): 371 | return 372 | if target_surfaces.size() == 0: 373 | if !remove_fur_material(mesh.material_overlay): 374 | mesh.material_overlay = null 375 | else: 376 | for i in target_surfaces: 377 | if !remove_fur_material(mesh.get_surface_override_material(i)): 378 | mesh.set_surface_override_material(i, null) 379 | 380 | for shell in shells: 381 | shell.next_pass = null 382 | shells = [] 383 | 384 | 385 | # remove fur materials when Fur node is deleted 386 | func _exit_tree() -> void: 387 | clear_materials() 388 | 389 | 390 | # remove fur material from the material chain. Returns false if the passed material is itself a fur material. 391 | func remove_fur_material(mat: Material) -> bool: 392 | if mat == null: 393 | return false 394 | if mat.has_meta("is_fur"): 395 | return false 396 | 397 | while mat.next_pass != null: 398 | if mat.next_pass.has_meta("is_fur"): 399 | mat.next_pass = null 400 | return true 401 | mat = mat.next_pass 402 | 403 | return true 404 | 405 | # assigns a fur material to the last material in the next_pass chain. If the start of the chain is null, return false. 406 | func assign_fur_material(mat: Material, fur: Material) -> bool: 407 | if mat == null: 408 | return false 409 | 410 | while mat.next_pass != null: 411 | mat = mat.next_pass 412 | 413 | mat.next_pass = fur 414 | 415 | return true 416 | 417 | 418 | # create cascade of shell materials and assign to subsequent next_pass slots 419 | func create_materials(): 420 | if(mesh == null): 421 | return 422 | 423 | var mat = shell_material.duplicate() 424 | mat.set_meta("is_fur", true) 425 | 426 | if target_surfaces.size() == 0: 427 | if !assign_fur_material(mesh.material_overlay, mat): 428 | mesh.material_overlay = mat 429 | else: 430 | for i in target_surfaces: 431 | if !assign_fur_material(mesh.get_surface_override_material(i), mat): 432 | mesh.set_surface_override_material(i, mat) 433 | 434 | shells.append(mat) 435 | 436 | for i in range(1, number_of_shells): 437 | var new_mat = shell_material.duplicate() 438 | new_mat.set_meta("is_fur", true) 439 | mat.next_pass = new_mat 440 | mat = new_mat 441 | shells.append(mat) 442 | 443 | previous_position = mesh.transform.origin 444 | previous_rotation = mesh.transform.basis.get_euler() 445 | 446 | 447 | func apply_lod(): 448 | if mesh == null: 449 | return 450 | 451 | lod_shells = [] 452 | 453 | lod_shell_count = lod_minimum_shells + (1 - float(lod) / (number_of_shells-1)) * (number_of_shells - lod_minimum_shells) 454 | 455 | var step = float(number_of_shells-1) / (lod_shell_count-1) 456 | 457 | lod_shells.append(shells[0]) 458 | 459 | for i in range(lod_shell_count-1): 460 | var base = int(step * i) 461 | var next = int(step * (i+1)) 462 | shells[base].next_pass = shells[next] 463 | lod_shells.append(shells[next]) 464 | 465 | # setup parameters for all shell materials 466 | func setup_materials(): 467 | if mesh == null: 468 | return 469 | if shells.size() == 0: 470 | create_materials() 471 | 472 | for i in number_of_shells: 473 | configure_material_for_level(shells[i], i) 474 | 475 | 476 | # set shader parameters for a single shell at the given level 477 | func configure_material_for_level(mat: Material, level: int): 478 | var h = float(level) / (number_of_shells-1) 479 | 480 | # lower number of shells means visually less dense fur. We adjust the thickness based on an empirical formula 481 | # to compensate for the loss of strand pixels 482 | var lod_thickness = 4.5987 * pow(lod_shell_count, -0.2807) if lod_enabled else 1.0 483 | 484 | # growth 485 | mat.set_shader_parameter("height", length) 486 | mat.set_shader_parameter("normal_strength", normal_strength) 487 | mat.set_shader_parameter("static_direction_local", static_direction_local) 488 | mat.set_shader_parameter("static_direction_world", static_direction_world) 489 | mat.set_shader_parameter("h", h) 490 | mat.set_shader_parameter("heightmap_texture", heightmap_texture) 491 | mat.set_shader_parameter("use_heightmap_texture", heightmap_texture != null) 492 | #curls 493 | mat.set_shader_parameter("curls_enabled", curls_enabled) 494 | mat.set_shader_parameter("curls_twist", curls_twist) 495 | mat.set_shader_parameter("curls_fill", curls_fill) 496 | # turbulence & jitter 497 | mat.set_shader_parameter("turbulence_texture", turbulence_texture) 498 | mat.set_shader_parameter("turbulence_strength", turbulence_strength) 499 | mat.set_shader_parameter("jitter_texture", jitter_texture) 500 | mat.set_shader_parameter("jitter_strength", jitter_strength) 501 | mat.set_shader_parameter("density", density) 502 | mat.set_shader_parameter("seed", seed) 503 | mat.set_shader_parameter("scruffiness", scruffiness) 504 | mat.set_shader_parameter("thickness_curve", thickness_curve) 505 | mat.set_shader_parameter("use_thickness_curve", thickness_curve != null) 506 | mat.set_shader_parameter("thickness_scale", thickness_scale * lod_thickness) 507 | mat.set_shader_parameter("render_skin", render_skin) 508 | # Albedo 509 | mat.set_shader_parameter("color", albedo_color) 510 | mat.set_shader_parameter("height_gradient", height_gradient) 511 | mat.set_shader_parameter("use_height_gradient", height_gradient != null) 512 | mat.set_shader_parameter("scale_height_gradient", scale_height_gradient) 513 | mat.set_shader_parameter("use_albedo_texture", albedo_texture != null) 514 | mat.set_shader_parameter("albedo_texture", albedo_texture) 515 | # Emission 516 | mat.set_shader_parameter("use_emission", use_emission) 517 | mat.set_shader_parameter("emission_color", emission_color) 518 | mat.set_shader_parameter("emission_energy_multiplier", emission_energy_multiplier) 519 | mat.set_shader_parameter("use_emission_texture", emission_texture != null) 520 | mat.set_shader_parameter("emission_texture", emission_texture) 521 | 522 | func init_physics(): 523 | spring_offset = Vector3.ZERO 524 | spring_velocity = Vector3.ZERO 525 | spring_rotation = Vector3.ZERO 526 | spring_angular_velocity = Vector3.ZERO 527 | 528 | if mesh == null: 529 | return 530 | 531 | previous_position = mesh.transform.origin 532 | previous_rotation = mesh.transform.basis.get_euler() 533 | 534 | for mat in shells: 535 | # initial Physics parameters 536 | mat.set_shader_parameter("physics_pos_offset", Vector3.ZERO) 537 | mat.set_shader_parameter("physics_rot_offset", Basis.IDENTITY) 538 | 539 | func _process(_delta): 540 | # LOD 541 | if lod_enabled: 542 | if mesh == null: 543 | return 544 | # calculate distance from transform origin to camera 545 | var camera: Camera3D = get_viewport().get_camera_3d() 546 | if camera == null: 547 | return 548 | 549 | # use closest point on AABB for distance calculation. 550 | var aabb: AABB = mesh.get_aabb() 551 | var closest = closest_point_on_aabb(aabb, camera.transform.origin) 552 | 553 | # lod distance in the range [0, 1] 554 | var rel_dist: float = clamp((camera.transform.origin.distance_to(closest) - lod_min_distance) / (lod_max_distance - lod_min_distance), 0, 1) 555 | 556 | # linearly scale number of shells 557 | lod = clamp(floor(rel_dist * number_of_shells), 0, number_of_shells-1) 558 | 559 | 560 | func closest_point_on_aabb(aabb: AABB, p: Vector3): 561 | var closest = Vector3.ZERO 562 | var pos = mesh.to_global(aabb.position) 563 | var end = mesh.to_global(aabb.end) 564 | closest.x = clamp(p.x, pos.x, end.x) 565 | closest.y = clamp(p.y, pos.y, end.y) 566 | closest.z = clamp(p.z, pos.z, end.z) 567 | return closest 568 | 569 | 570 | func _physics_process(delta): 571 | linear_spring_physics(delta) 572 | rotational_spring_physics(delta) 573 | 574 | 575 | # calculate spring physics for linear movement 576 | func linear_spring_physics(delta: float): 577 | if !physics_enabled: 578 | return 579 | if shells.size() == 0: 580 | return 581 | if Engine.is_editor_hint() and (!physics_preview || !preview_in_editor): return 582 | # calculate compound linear forces acting on the shells 583 | var f = gravity 584 | 585 | # calculate movement from previous position 586 | var dx = mesh.transform.origin - previous_position # movement from previous position 587 | var v = dx / delta # velocity 588 | spring_offset += dx # new offset, after base has moved 589 | 590 | var st = 8.0 # "exaggeration" factor for more fun spring movement 591 | 592 | f += -spring_constant * spring_offset - damping * (v+spring_velocity) 593 | 594 | var a = f / mass 595 | spring_velocity += a * delta 596 | var s = spring_velocity * delta / 2 597 | 598 | spring_offset += s 599 | 600 | spring_velocity = spring_velocity.limit_length( 200.0 * length ) 601 | 602 | # iterate through materials from 0 length to 1 and set physics params 603 | var dh = 1.0 / (number_of_shells-1) 604 | var h = dh 605 | 606 | spring_offset = spring_offset.limit_length(length / st * stretch) 607 | 608 | for i in range(number_of_shells): 609 | var mat = shells[i] 610 | var offset_at_height = st * spring_offset * pow(h * i, stiffness) 611 | mat.set_shader_parameter("physics_pos_offset", -offset_at_height) 612 | i+=1 613 | 614 | previous_position = mesh.transform.origin 615 | 616 | 617 | func short_angle(a): 618 | return fmod(2 * a, 2 * PI) - a 619 | 620 | # calculate spring physics for rotational movement 621 | func rotational_spring_physics(delta: float): 622 | if !physics_enabled: 623 | return 624 | if shells.size() == 0: 625 | return 626 | if Engine.is_editor_hint() and (!physics_preview || !preview_in_editor): 627 | return 628 | # calculate compound rotational forces acting on the shells, as a Vector3 of Euler angles 629 | var f = Vector3.ZERO 630 | 631 | # calculate rotation from previous position 632 | var dp: Vector3 = mesh.transform.basis.get_euler() - previous_rotation # rotation from previous rotation 633 | 634 | dp = Vector3(short_angle(dp.x), short_angle(dp.y), short_angle(dp.z)) 635 | 636 | var w: Vector3 = dp / delta # velocity 637 | spring_rotation += dp # new offset, after base has rotated 638 | 639 | f += -spring_rotation * spring_constant - damping * (w+spring_angular_velocity) 640 | 641 | var a = f / mass 642 | spring_angular_velocity += a * delta 643 | var p = spring_angular_velocity * delta / 2 644 | 645 | spring_rotation += p 646 | 647 | # iterate through materials from 0 length to 1 and set physics params 648 | var dh = 1.0 / (number_of_shells-1) 649 | var h = dh 650 | 651 | spring_rotation = spring_rotation.limit_length(PI * length / 2.0) 652 | 653 | for i in range(number_of_shells): 654 | var mat = shells[i] 655 | var rotation_at_height = rotational_physics_scale * spring_rotation * pow(h * i, stiffness) 656 | mat.set_shader_parameter("physics_rot_offset", Basis.from_euler(rotation_at_height)) 657 | i+=1 658 | 659 | previous_rotation = mesh.transform.basis.get_euler() 660 | --------------------------------------------------------------------------------