leios / Fable.jl

General purpose animations via iterated function systems
MIT License
28 stars 4 forks source link

More Hutchinson layers / restricted chaos games #65

Open leios opened 1 year ago

leios commented 1 year ago

As a note, the Hutchinson redesign (#64) currently allows for users to specify hutchinson operators (and technically shaders) like so:

H = Hutchinson(f_1, (f_2, f_3, f_4, f_5), f_6)

Which means: execute f_1, then choose between f_2 -> f_5, then execute f_6, but we could imagine:

H = Hutchinson(f_1, ((f_2, f_3), f_4, f_5), f_6)

Which would mean: execute f_1, then choose between (f_2, f_3), f_4, or f_5, then execute f_6. This Cannot be done right now because I don't have a way to reason about a choice between f_2 and f_3 as one of the choices in another IFS. A simple strategy would be to set FractalOperators as a super type and create a null struct:

struct NullOperator <: FractalOperator
    prob::Number
    fos::Tuple{FractalOperator}
end

Then we would store [f_1, f_null, f_4, f_5, f_2, f_3, f_null, f_6] with fnums of [1,3,2,1] and potentially another set of indices like fstarts = [1, 2, 5, 7] so if we have 2 or more recursive IFS definitions, we can keep track of where we are on the list. We could probably get rid of the list of lists entirely in this case and just do everything in the kernel instead...

We also need to think about how users will write down both the probability of the null operator and the other functions in a reasonable way, maybe...

    H = Hutchinson(f_1, (fo(f_2, f_3; prob = 0.33), f_4, f_5), f_6)

With fo(fos::Tuple{FractalOperator}; prob = 0) = NullOperator(prob, fos)

Or something.

leios commented 1 year ago

I think I'm overthinking this. It should be possible for the user to implement everything they want for the "game" part of the chaos game. So, Fable handles the iteration, but the user can write custom compute kernels by abusing the fum syntax?

leios commented 1 year ago

Otherwise, the core problem here is that we use the fid construct to iterate over functions. This means that we generate a single random number that is used for all functions. This is nice in a way for performance, but if the user is implementing tuples of tuples where we know the probabilities, we can probably just use those instead and make a function choice on-the-fly.

I am not sure how this would play with the @generated functions used to iterate over different fx tuples, ie:

# These functions essentially unroll the loops in the kernel because of a
# known julia bug preventing us from using for i = 1:10...
@generated function pt_loop(fxs, fid, pt, frame, fnums, kwargs)
    exs = Expr[]
    push!(exs, :(bit_offset = 0))
    push!(exs, :(fx_offset = 0))
    for i = 1:length(fnums.parameters)
        ex = quote
            idx = decode_fid(fid, bit_offset, fnums[$i]) + fx_offset
            pt = call_pt_fx(fxs, pt, frame, kwargs, idx)
            bit_offset += ceil(UInt,log2(fnums[$i]))
            fx_offset += fnums[$i]
        end
        push!(exs, ex)
    end

    push!(exs, :(return pt))

    # to return 3 separate colors to mix separately
    # return :(Expr(:tuple, $exs...))

    return Expr(:block, exs...)
end

We could either change this to do some form of DFS over the fx tuple provided (maybe some sort of stack-like implementation while unrolling the while loop?), or we allow the fids to accept conditional probabilities and such.