Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

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?

share|improve this question
    
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
add comment

1 Answer 1

up vote 13 down vote accepted

I tried it.

Some hairstyles.

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.)

graph of (x minus one) squared

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
local fringeRadius = 90         -- radius of the fringe guideline circle

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

function love.load()

    ---[[ 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.

share|improve this answer
    
Well done, impressive! –  Vaughan Hilts Feb 26 '13 at 2:22
1  
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
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.