summaryrefslogtreecommitdiffstats
path: root/water/BuoyantBody.gd
blob: 4a9dc78ee448ad056013aea1b43e5de46b697623 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
extends RigidBody
class_name BuoyantBody

export(NodePath) var water = NodePath("/root/Main/Water")
onready var _water = get_node(water)

var volume_half = 0.0
var volume = 0.0
var bounding_box = []
var center_of_mass = Vector3.ZERO
var volume_up = Vector3.UP

onready var inv_gravity = -ProjectSettings.get_setting("physics/3d/default_gravity_vector") * ProjectSettings.get_setting("physics/3d/default_gravity")
const POINT_VOLUME_WEIGHT = 0.125

func _ready():
    for c in get_children():
        if c is CollisionShape:
            if c.shape is SphereShape:
                _init_buoyancy(c.shape.radius,c.shape.radius,c.shape.radius,c.transform)
            elif c.shape is CapsuleShape:
                _init_buoyancy(c.shape.radius,c.shape.radius,c.shape.height/2.0,c.transform)
            elif c.shape is BoxShape:
                _init_buoyancy(c.shape.extents.x,c.shape.extents.y,c.shape.extents.z,c.transform)
            else:
                assert(false,"this collision shape isn't supported for buoyancy")
            break

func _init_buoyancy(x_rad,y_rad,z_rad,xform):
    self.volume_half = x_rad * y_rad * z_rad * 4.0
    self.volume = volume_half * 2.0
    var aabb = [
        Vector3(-x_rad,-y_rad,-z_rad),
        Vector3(-x_rad,-y_rad, z_rad),
        Vector3(-x_rad, y_rad,-z_rad),
        Vector3(-x_rad, y_rad, z_rad),
        Vector3( x_rad,-y_rad,-z_rad),
        Vector3( x_rad,-y_rad, z_rad),
        Vector3( x_rad, y_rad,-z_rad),
        Vector3( x_rad, y_rad, z_rad)
    ]
    for p in aabb:
        self.bounding_box.push_back(xform * p)
    self.center_of_mass = xform * center_of_mass
    self.volume_up = (xform.basis * Vector3.UP).normalized()

func _physics_process(_delta):
    #check if points are submerged
    #also weight them based on how deep they are to help get a weighted average center point later
    var submerged = []
    var sub_weight = []
    var sub_total = 0.0
    for p in bounding_box:
        var g_p = self.global_transform * p
        var wave_height = _water.height(g_p)
        var diff = g_p.y - wave_height
        if diff < 0.0:
            submerged.push_back(p)
            sub_weight.push_back(diff)
            sub_total += diff
    #if no points are submerged, the whole thing is likely above water
    #therefore, no buoyant force is applied
    if submerged.size() == 0:
        return
    #if all points are submerged, the whole thing is likely below water
    #so the buoyant force would be the weight of the water displaced by the entire volume
    elif submerged.size() == 8:
        add_central_force(inv_gravity * volume)
    #if only some points are submerged, we need to estimate the amount of the volume displacing water
    #the weight of that water is the buoyant force
    else:
        #we want to apply the buoyant force to the center of mass of the submerged part of the volume
        #we can estimate it with a weighted average of the submerged points
        var force_pnt = Vector3.ZERO
        for p in range(submerged.size()):
            force_pnt += submerged[p] * sub_weight[p]
        force_pnt /= sub_total
        #apply_force uses global rotation but local origin...
        force_pnt = self.global_transform.basis * force_pnt
        #to estimate the submerged part of the volume,
        #we can see how deep into the water the bottom of an axis-aligned bounding box is
        #and do an easing and lerp over the volume
        var lowest = (self.global_transform * bounding_box[0]).y
        for p in bounding_box:
            var p_h = (self.global_transform * p).y
            if p_h < lowest:
                lowest = p_h
        var center = self.global_transform * center_of_mass
        var depth = _water.height(center) - lowest
        #the water isn't a flat plane
        #it's possible that some points are submerged,
        #but the lowest point of the axis-aligned bounding box isn't actually under water
        #in that case, for simplicity,
        #just fall back to the old method of applying a small, constant force
        #proportional to the number of submerged points
        if depth <= 0.0:
            add_force(inv_gravity * volume * POINT_VOLUME_WEIGHT * submerged.size(), force_pnt)
        #lerp the volume against the depth of the axis-aligned bounding box
        #but that lerp is only linear when the actual bounding box is axis-aligned
        #for simplicity, we can lerp an easing curve based on how aligned the bounding box is
        #on one end, it's linear and on the other it uses a basic exponential easing
        #we want to do an inverse exponential ease for the upper half of the volume if it is also submerged
        else:
            var up = self.global_transform.basis * volume_up
            var aligned = up.dot(Vector3.UP)
            aligned = abs(aligned)
            aligned -= 0.5
            aligned = abs(aligned)
            aligned *= 2.0
            var easing_curve = lerp(4.8,1.0,aligned)
            var breadth = (center.y - lowest)
            depth = clamp(depth,0.0,breadth*2.0)
            depth /= breadth
            var v = clamp(depth,0.0,1.0)
            v = ease(v,easing_curve)
            var v2 = clamp(depth-1.0,0.0,1.0)
            v2 = 1.0 - ease(1.0 - v2,easing_curve)
            v += v2
            v = lerp(0.0,volume_half,v)
            add_force(inv_gravity * v, force_pnt)