Simple 2D hair simulation/manipulation

I would like to simulate very simple hair in a 2D environment, and will need to be able to "brush" the hair.

What I want to do: A 2D character is facing squarely towards the player. The player can then brush the character's hair. Think of a little girl brushing her doll's hair into different styles.

In my preliminary Google searches, I found many 3D, super-optimized hair simulations for tens of thousands of hairs, which are far more complicated than I need. I only need 100-500 hairs, and they only need to react to "brushing" by the player.

How might I do this?

-
I am tempted in giving an answer, but I'd like to request you to post either a small pic of that action (not animation, a paint.exe sketch should do), or a short description. The details required are: what is the view (front - i.e. back of the head pointing out of the screen or side - ears pointing out of the screen). One very simple thing to do even in 2D is implementing a follow the leader strategy. Here is the paper matthiasmueller.info/publications/FTLHairFur.pdf . – teodron Feb 20 '13 at 16:01
Thanks for the paper you linked to, I'll give it a read! – loneboat Feb 20 '13 at 16:13

I tried it.

Brown is generated hair. The red circle is the hairbrush. The hairstyles were achieved by brushing the hair with the hairbrush.

Here's how that works:

The Hair

Hairs are like this:

• Bendy. Like a bezier curve. Quadratic ones are pretty simple.
• Finite. They start and end somewhere. They also have some fixed length.
• Coloured. They've got some RGB to them, usually pretty heavy in the R and G.
• (Weighty.) Let's ignore gravity to keep things simple -- and to allow awesome hairstyles.

This is roughly how you might translate that into a program:

``````oneHair = {
start = vector(100,100),
controlPoint = vector(120,70), -- For the bezier
end = vector(100,200),
length = 100,
update = function(self)
-- Make sure the END is within LENGTH of START
-- (This isn't actually quite right, since beziers aren't lines,
--  but it's a pretty good approximation...)
end,
draw = function(self)
-- Make a bezier curve using START, CONTROL and END.
-- Sample it at intervals. Draw lines between samples.
end
}
``````

And then you just create really many. (There are 1200 in each of the pictures.)

It's useful to create them on or within some circle describing the shape of the head or the line of the fringe. (I used a filled half-circle for the bulk of the hair an edge of a different half-circle for the fringe.)

You can also vary the lengths of hairs. (I find bangs running down both sides of the face attractive, so I approximated hair length with this quadratic function (picture just below), using the horizontal distance from the centre of the aforementioned guide circle as input.)

I recommend a similar approach, obviously keeping the desired style in mind.

The Brush

The brushing mechanic is based on these (idealised) observations:

• Hair that touches the brush moves in the direction the brush is moved in, except slower.
• Brushing along hair straightens the hairs.
• Brushing across hair bends the hairs.

So my brushing code does essentially this:

1. Find hairs that touch brush (using their bezier curves)
2. For each hair:
• Move the endpoint of the hair by some fraction of the pointer movement vector.
• Project the movement vector of the pointer onto the perpendicular of the hair's direction. This gives a vector describing the brush motion in relation to the hair.
• If the brush motion in relation to the hair is more along than across the hair, move the control point some fraction of the way toward the midpoint of the hair. This smooths the hair.
• Else (if the brush motion is more across than along, move the control point some fraction of the pointer's movement vector.

The Code

I made it with LÖVE in Lua. Here's my `main.lua`, if you just want to read:

``````local v = require "vector"
local graphics = love.graphics
local mouse = love.mouse
graphics.setBackgroundColor(255,255,255) --> white background

-- A function that makes bezier functions
-- Beziers have start point     p0
--              control point   p1
--              end point       p2
local function makeBezierFunction(p0,p1,p2)
return function (t)
local pow = math.pow
return pow( (1-t),2 ) * p0
+ 2 * (1-t) * t * p1
+ pow(t,2) * p2
end
end

-- Hair class
-- Encapsulates all that needs to be known to create, draw or update a hair.
local hair = {}
function hair.draw(self)
-- Make a bezier function describing the shape of the hair
local shape = makeBezierFunction(
self.from,
self.mid,
self.to
)
-- This is the increment for one segment of a hair's bezier function
local segIncr = 1/self.resolution
for n=0,1-segIncr,segIncr do
-- This and next sample point
local x1,y1 = shape(n):unpack()
local x2,y2 = shape(n + segIncr):unpack()

graphics.setColor(unpack(self.col))
graphics.line(x1,y1, x2,y2)
end
end
function hair.update(self)

-- Beziers can be longer than lines. This is an approximation to save us
-- from some complicated maths.
local vectorAlongLength = self.to - self.from
-- Move endpoint to within the length of the hair of the start point.
if vectorAlongLength:len() > self.length then
self.to = self.from + vectorAlongLength:normalized() * self.length
end

end
function hair.new(from, to, verticalBend, col) -- Create a new hair object
return setmetatable({
length = (from - to):len(),
from = from,
to   = to,
mid  = from + v(0,verticalBend),
verticalBend = verticalBend,
resolution = 5,
col = col,
}, {
__index = hair
})
end
setmetatable(hair, {
__call = function(_,...) return hair.new(...) end
})

local hairs = {}                -- all hairs stored here
local selectorSize = 30         -- radius of brush
local fringeOrigin = v(325,260) -- origin of the fringe guideline circle

-- Terrible drawing of a bald person, happy to soon receive lots of hair.
local portrait = graphics.newImage "portrait.png"

---[[ Bulk of hair, at the back
local numHairs = 1000
for _=1,numHairs do
-- Get a random angle within 180 degrees
local randomAngle = math.random() * math.pi + math.pi/2
-- Find random point within circle (though offset somewhat)
local point = fringeOrigin + v(0,-30) + v(math.sin(randomAngle), math.cos(randomAngle)) * (fringeRadius - 20 + math.random(50))
-- Fairly dark brown
local randomValue = math.random(50)
local randomCol = {randomValue+50, randomValue+30, randomValue}
local length = 30+math.random(50 * (math.pow(2*(point.x - fringeOrigin.x)/fringeRadius, 2)))
local horizontalBend = -15 + math.random(30)
local verticalBend = math.random(length)
local newHair = hair(v(point.x, point.y), v(point.x+horizontalBend, point.y+length), verticalBend, randomCol)
table.insert(hairs, newHair)
end
--]]
---[[ Fringe
local numHairs = 200
-- get random points on fringe circle arc
for _=1,numHairs do
-- Get a random angle within 180 degrees
local randomAngle = math.random() * math.pi + math.pi/2
-- Find random point somewhere ON (NOT WITHIN) the fringe arc
local point = fringeOrigin + v(math.sin(randomAngle), math.cos(randomAngle)) * fringeRadius
local randomValue = math.random(100)
local randomCol = {randomValue+50, randomValue+30, randomValue}
local length = 50+math.random(50 * (math.pow(2*(point.x - fringeOrigin.x)/fringeRadius, 2)))
local horizontalBend = -15 + math.random(30)
local verticalBend = math.random(length)
local newHair = hair(v(point.x, point.y), v(point.x+horizontalBend, point.y+length), verticalBend, randomCol)
table.insert(hairs, newHair)
end
--]]
end

lastMousePos = v()
mousePos = v()
mouseMovement = v() -- Movement vector of the mouse this step
function love.update()

mousePos = v(mouse.getPosition())
mouseMovement = mousePos - lastMousePos

if mouse.isDown "l" then -- Left mouse button pressed

-- Find all hairs with ends nearby the cursor
local nearbyHairs = {}
for _,hair in ipairs(hairs) do
-- Create a function describing the shape of the bezier
local shape = makeBezierFunction(
hair.from,
hair.from + v(0,hair.verticalBend),
hair.to
)
-- This is the increment for one segment of a hair's bezier function
local segIncr = 1/hair.resolution
for n=0,1,segIncr do
local pointOnShape = v(shape(n):unpack())
-- If it's close enough to the mouse
if (pointOnShape - mousePos):len() < selectorSize then
-- Select it
table.insert(nearbyHairs, hair)
end
end

end

-- Apply transforms to each hair
for _,hair in ipairs(nearbyHairs) do

-- Move endpoint along with movement vector
hair.to = hair.to + 0.5 * mouseMovement

-- Find movement in relation to hair
local movementAcross = mouseMovement:projectOn((hair.to - hair.from):perpendicular())

-- If the movement was ALONG the hair
if math.abs(movementAcross.x) < 10 then
-- Smooth the hair by moving control point closer to midpoint
local midPoint = (hair.to + hair.from) /2
hair.mid = 0.85 * hair.mid + midPoint * 0.15
else -- Movement was ACROSS the hair
-- Move the control point along with the movement vector
hair.mid.x = hair.mid.x + 0.25 * mouseMovement.x
end
end

end

-- Update hairs to make sure their lengths stay the same
for _,hair in ipairs(hairs) do hair:update() end

lastMousePos = mousePos
end

function love.draw(dt)
-- Draw the bald portrait image
graphics.draw(portrait, 200, 100)
-- This is a nice cheat for making less hair look like it has more volume
graphics.setLineWidth(2)

for _,hair in ipairs(hairs) do
hair:draw(dt)
end

-- Draw brush pointer
graphics.setColor(255,0,0)
graphics.circle("line", mousePos.x, mousePos.y, selectorSize)

-- The fringe line
--graphics.arc("line", fringeOrigin.x, fringeOrigin.y, fringeRadius, -math.pi, 0)
end
``````

If you want to have a play with it, get LÖVE (any OS is OK) and open my love file with it.

If you want to see the source code in its entirety, just change the `.love` extension to `.zip` and unpack.

-
Well done, impressive! – Vaughan Hilts Feb 26 '13 at 2:22
Wow! This is very in depth. Thanks so much, I hope you had as much fun writing it as I'm expecting to have messing with it! You rock! – loneboat Feb 26 '13 at 15:05
@lon Heh, messing with it. :> Tell me if you find unclear parts or if you make awesome things with it! – Anko Feb 26 '13 at 23:53