-------- -- Clouds: spawning dynamically in chunks, moving with the wind and what not. It’s a bit of a mess as I was trying to get it -- working as fast as possible and produce as little garbage as possible. TODO: move algorithm to C++ side allowing to use -- several layers of clouds at once cheaper? -------- -- Local state (will be updated with values from `conditions_converter.lua`) local ccClouds = 0 local windDir = vec2(1, 0) local windSpeed = 0 local windAngle = 0 -- Calculates base Y coordinate of a cloud from a circle of clouds near horizon require 'src/weather_clouds_pertrack' -- Different types of clouds require 'src/weather_clouds_types' -- Creates a new cloud and sets it using `fn`, which would be one of `CloudTypes` functions ---@return ac.SkyCloudV2 local function createCloud(fn, arg1, arg2) local cloud = ac.SkyCloudV2() cloud.color = rgb(1, 1, 1) cloud.procMap = vec2(0.6, 0.65 + math.random() * 0.05) + math.random() * 0.1 cloud.procNormalScale = vec2(0.9, 0.3) cloud.procShapeShifting = math.random() cloud.opacity = 0.9 cloud.shadowOpacity = 1.0 cloud.cutoff = 0 cloud.occludeGodrays = false cloud.useNoise = true cloud.material = CloudMaterials.Main cloud.up = vec3(0, -1, 0) cloud.side = math.cross(-cloud.position, cloud.up):normalize() cloud.up = math.cross(cloud.side, -cloud.position):normalize() cloud.noiseOffset:set(math.random(), math.random()) fn(cloud, arg1, arg2) return cloud end local function transitionHeadingAngle(current, target, dt) if current == -1 then return target end local delta = target - current if delta > 180 then delta = 360 - delta elseif delta < -180 then delta = 360 + delta end return current + delta * math.min(dt, 0.1) * math.lerpInvSat(windSpeed, 0.002, 0.009) * 0.02 end local nightEarlyK = 0 local nightEarlySmoothK = -1 local CloudsCell = {} function CloudsCell:new(o) o = o or {} setmetatable(o, self) self.__index = self o.initialized = false o.clouds = {} o.cloudsCount = 0 o.hoveringClouds = {} o.hoveringCloudsCount = 0 o.lastActive = 0 o.updateDelay = 10 o:reuse(o.index) return o end function CloudsCell:reuse(index) self.index = index self.pointA = CloudsCell.getCellCenter(self.index) self.pointB = self.pointA + vec3(CloudCellSize, 0, CloudCellSize) self.center = (self.pointA + self.pointB) / 2 if self.initialized then for i = 1, self.cloudsCount do self.clouds[i].pos = self:getPos() end end self.updateDelay = 10 end function CloudsCell:addCloud(cloudInfo) if cloudInfo.hovering then self.hoveringCloudsCount = self.hoveringCloudsCount + 1 self.hoveringClouds[self.hoveringCloudsCount] = cloudInfo else self.cloudsCount = self.cloudsCount + 1 self.clouds[self.cloudsCount] = cloudInfo end end function CloudsCell:getPos(hovering) return vec3( math.lerp(self.pointA.x, self.pointB.x, math.random()), hovering and math.lerp(HoveringMinHeight, HoveringMaxHeight, math.random()) or math.lerp(DynCloudsMinHeight, DynCloudsMaxHeight, math.random()), math.lerp(self.pointA.z, self.pointB.z, math.random())) end local cloudNoise = LowFrequency2DNoise:new{ frequency = 0.001 } function CloudsCell:initialize() self.initialized = true local DynamicClouds = 8 local HoveringClouds = UseGammaFix and 0 or 2 for i = 1, DynamicClouds + HoveringClouds do local hovering = i > DynamicClouds local pos = self:getPos(hovering) local cloud = createCloud(hovering and (math.random() > 0.3 and CloudTypes.Spread or CloudTypes.Hovering) or CloudTypes.Dynamic, pos) local weatherThreshold = cloudNoise:get(pos) * (hovering and 0.4 or 0.95) self:addCloud({ cloud = cloud, pos = pos, size = cloud.size:clone(), procMap = cloud.procMap:clone(), procScale = cloud.procScale:clone(), opacity = cloud.opacity, flatCloud = nil, visibilityOffset = 1 + (hovering and 0.5 or math.random()), weatherThreshold0 = weatherThreshold, weatherThreshold1 = 0.05 + weatherThreshold, hovering = hovering, cloudAdded = false, flatCloudAdded = false }) end end function CloudsCell:updateHovering(cameraPos, cellDistance, dt) for i = 1, self.hoveringCloudsCount do local e = self.hoveringClouds[i] local c = e.cloud c.position:set(e.pos):sub(cameraPos) local d = math.horizontalLength(c.position) c.opacity = e.opacity * (1 - math.saturateN(e.visibilityOffset * d * 5 / (cellDistance * CloudCellSize) - 4)) * (1 - 0.2 * ccClouds) * CloudsMult if c.opacity > 0.001 then local weatherCutoff = 1 - math.lerpInvSat(ccClouds, e.weatherThreshold0, e.weatherThreshold1) if not c.passedFrustumTest then c.orderBy = math.dot(c.position, c.position) + 1e9 end c.opacity = c.opacity * (0.5 + 0.5 * ccClouds) * (1 - weatherCutoff) c.extraFidelity = math.lerp(c.extras.extraFidelity, -2, nightEarlyK) c.horizontalHeading = transitionHeadingAngle(c.horizontalHeading, windAngle, dt) local up = windDir.x * c.up.x + windDir.y * c.up.z local side = windDir.x * c.side.x + windDir.y * c.side.z local windDeltaC = (0.15 * windSpeed * dt * CloudShapeMovingSpeed) / c.size.x c.noiseOffset.x = c.noiseOffset.x - windDeltaC * side c.noiseOffset.y = c.noiseOffset.y - windDeltaC * up if not e.cloudAdded then e.cloudAdded = true ac.weatherClouds[#ac.weatherClouds + 1] = c end elseif e.cloudAdded then e.cloudAdded = false ac.weatherClouds:erase(c) end end end function CloudsCell:updateDynamic(cameraPos, cellDistance, dt) local distance = math.horizontalDistance(self.center, cameraPos) local distanceK = math.smoothstep(math.lerpInvSat(distance, CloudDistanceShiftEnd, CloudDistanceShiftStart) ^ 0.8) local windDelta = windSpeed * dt * CloudShapeMovingSpeed local shapeShiftingDelta = dt * CloudShapeShiftingSpeed -- local maxDistanceInv = 5 / (cellDistance * CloudCellSize) local fadeNearbyInv = 500 / CloudFadeNearby -- local opacityMult = 1 - 0.2 * ccClouds local opacityMult = 1 local extraThick = math.lerpInvSat(CurrentConditions.clear, 1, 0.5) local cloudDark = math.max((1 - CurrentConditions.clear) * 0.2, CurrentConditions.cloudsDensity) local cloudsHeight = math.lerp(1, 0.5, CurrentConditions.cloudsDensity) local mapYMult = math.lerp(1, 1.2, nightEarlyK) local procScaleMult = math.lerp(1, 0.8, nightEarlySmoothK) local distMult = 1 / (math.lerp(4, 0.5, math.max(math.pow(ccClouds * (1 - CurrentConditions.clear) - CurrentConditions.cloudsDensity, 8), NightK * 0.8)) * cellDistance * CloudCellSize) for i = 1, self.cloudsCount do local e = self.clouds[i] local c = e.cloud c.position.x = e.pos.x - cameraPos.x c.position.y = e.pos.y * cloudsHeight - cameraPos.y c.position.z = e.pos.z - cameraPos.z -- c.position:set(e.pos):sub(cameraPos) local horDist = math.horizontalLength(c.position) local fullDist = math.sqrt(horDist ^ 2 + c.position.y ^ 2) local weatherCutoff = 0 local nearbyCutoff = 0 local windDeltaC = 0 c.opacity = math.lerpInvSat(horDist, (CloudCellDistance + 0.5) * CloudCellSize, (CloudCellDistance - 0.5) * CloudCellSize) * e.opacity * opacityMult * math.saturateN(1 - fullDist * distMult) * math.saturateN(fullDist / 200 - 1) c.shadowOpacity = c.opacity local lookingFromBelow = math.saturateN(c.position.y / fullDist) c.normalYExponent = 1 + 2 * lookingFromBelow c.topFogBoost = 0.2 * lookingFromBelow if c.opacity > 0.001 then if not c.passedFrustumTest then c.orderBy = fullDist end windDeltaC = windDelta / c.size.x nearbyCutoff = 2 + c.extras.nearbyCutoffOffset - horDist / math.max(1, c.position.y) * fadeNearbyInv weatherCutoff = 1 - math.lerpInvSat(ccClouds, e.weatherThreshold0, e.weatherThreshold1) c.cutoff = math.max(math.saturateN(nearbyCutoff), weatherCutoff) c.position.y = math.lerp(DynCloudsDistantHeight + c.size.y * 0.4, c.position.y + c.size.y * 0.8, distanceK) -- c.position.y = 1000 - ac.getCameraPosition().y end if c.cutoff < 0.999 and c.opacity > 0.001 then SetLightPollution(c) c.extraFidelity = math.lerp(c.extras.extraFidelity, -2, nightEarlyK) c.procMap.x = math.lerp(c.extras.procMap.x, c.extras.procMap.x * 0.5, extraThick) / mapYMult c.procScale:set(c.extras.procScale):scale(procScaleMult) local fwd = windDir.x * c.position.x / horDist + windDir.y * c.position.z / horDist local side = windDir.x * c.position.z / horDist + windDir.y * -c.position.x / horDist c.noiseOffset.x = c.noiseOffset.x + windDeltaC * side * c.procScale.x c.procShapeShifting = c.procShapeShifting + (shapeShiftingDelta + windDeltaC * fwd * 0.5) * c.procScale.x if not e.cloudAdded then e.cloudAdded = true ac.weatherClouds[#ac.weatherClouds + 1] = c end c.color:set(math.lerp(1, 0.5, cloudDark * c.extras.lowerK)) elseif e.cloudAdded then e.cloudAdded = false ac.weatherClouds:erase(c) end -- c.color = table.random(rgb.colors) local flatCutoff = math.max(1 - math.saturateN(0.5 + nearbyCutoff), weatherCutoff) if flatCutoff < 0.999 and c.opacity > 0.001 then local f = e.flatCloud if f == nil then f = createCloud(CloudTypes.Bottom, c) e.flatCloud = f end f.cutoff = flatCutoff f.opacity = c.opacity if f.cutoff < 0.999 then f.position:set(c.position) f.extraDownlit:set(c.extraDownlit) f.procMap:set(c.procMap) local up = windDir.x * f.up.x + windDir.y * f.up.z local side = windDir.x * f.side.x + windDir.y * f.side.z f.noiseOffset.x = f.noiseOffset.x - windDeltaC * side f.noiseOffset.y = f.noiseOffset.y - windDeltaC * up f.procShapeShifting = f.procShapeShifting + (shapeShiftingDelta + windDeltaC * 0.5) * 0.5 if not f.passedFrustumTest then f.orderBy = fullDist + 10 end if not e.flatCloudAdded then e.flatCloudAdded = true ac.weatherClouds[#ac.weatherClouds + 1] = f end end elseif e.flatCloudAdded then e.flatCloudAdded = false ac.weatherClouds:erase(e.flatCloud) end end end local cloudCellRadius = #vec2.new(CloudCellSize) function CloudsCell:update(cameraPos, cellDistance, dt) local isVisible = ac.testFrustumIntersection(vec3.tmp():set(self.center):sub(cameraPos), cloudCellRadius) local updateRate = not isVisible and 10 or 0 if self.updateDelay >= updateRate then if not self.initialized then self:initialize() end if self.updateDelay > 5 then self.updateDelay = self.updateDelay - 1 else self.updateDelay = 0 end self:updateHovering(cameraPos, cellDistance, dt) self:updateDynamic(cameraPos, cellDistance, dt) else self.updateDelay = self.updateDelay + 1 end end function CloudsCell:deactivate() for i = 1, self.cloudsCount do local e = self.clouds[i] if e.cloudAdded then ac.weatherClouds:erase(e.cloud) e.cloudAdded = false end if e.flatCloudAdded then ac.weatherClouds:erase(e.flatCloud) e.flatCloudAdded = false end end for i = 1, self.hoveringCloudsCount do local e = self.hoveringClouds[i] if e.cloudAdded then e.cloudAdded = false ac.weatherClouds:erase(e.cloud) end end end function CloudsCell:destroy() for i = 1, self.cloudsCount do local e = self.clouds[i] ac.weatherClouds:erase(e.cloud) ac.weatherClouds:erase(e.flatCloud) end end function CloudsCell.getCellOrigin(pos) return vec3(math.floor(pos.x / CloudCellSize) * CloudCellSize, 0, math.floor(pos.z / CloudCellSize) * CloudCellSize) end function CloudsCell.getCellCenter(cellIndex) local x = math.floor(cellIndex / 1e5 - 100) * CloudCellSize local y = (math.fmod(cellIndex, 1e5) - 100) * CloudCellSize return vec3(x, 0, y) end function CloudsCell.getCellIndex(pos) return math.floor(100 + pos.x / CloudCellSize) * 1e5 + math.floor(100 + pos.z / CloudCellSize) end function CloudsCell.getCellNeighbour(cell, x, y) return cell + x + y * 1e5 end local cloudCells = {} local cloudCellsList = {} local cellsTotal = 0 local activeIndex = 0 local windOffset = vec2() local cellsPool = {} local cellsPoolTotal = 0 local function createCloudCell(cellIndex) local c = nil if cellsPoolTotal > 0 then c = cellsPool[cellsPoolTotal] table.remove(cellsPool, cellsPoolTotal) cellsPoolTotal = cellsPoolTotal - 1 c:reuse(cellIndex) else c = CloudsCell:new{ index = cellIndex } end cloudCells[cellIndex] = c cloudCellsList[cellsTotal + 1] = c cellsTotal = cellsTotal + 1 return c end local cameraPos = vec3() local cleanUp = 0 local function updateCloudCells(dt) if ccClouds <= 0.0001 and (cellsPoolTotal > 1 or cellsTotal > 1) then if activeIndex >= 0 then activeIndex = -1 for i = cellsTotal, 1, -1 do local cell = cloudCellsList[i] cellsPoolTotal = cellsPoolTotal + 1 cellsPool[cellsPoolTotal] = cell cell:deactivate() end cloudCellsList = {} cloudCells = {} cellsTotal = 0 end return end activeIndex = activeIndex + 1 if activeIndex > 1e6 then activeIndex = 0 end ac.getCameraPositionTo(cameraPos) ac.fixHeadingInvSelf(cameraPos) cameraPos.x = cameraPos.x + windOffset.x cameraPos.z = cameraPos.z + windOffset.y windOffset:add(windDir * (windSpeed * dt)) local cellIndex = CloudsCell.getCellIndex(cameraPos) local cellDistance = math.ceil(CloudCellDistance * (1 - FinalFog * 0.3)) for x = -cellDistance, cellDistance do for y = -cellDistance, cellDistance do local n = CloudsCell.getCellNeighbour(cellIndex, x, y) local c = cloudCells[n] if c == nil then c = createCloudCell(n) end if c then c:update(cameraPos, cellDistance, dt) c.lastActive = activeIndex end end end if cleanUp > 0 then cleanUp = cleanUp - 1 else for i = cellsTotal, 1, -1 do local cell = cloudCellsList[i] if cell.lastActive ~= activeIndex then table.remove(cloudCellsList, i) cloudCells[cell.index] = nil cellsTotal = cellsTotal - 1 cellsPoolTotal = cellsPoolTotal + 1 cellsPool[cellsPoolTotal] = cell cell:deactivate() end end cleanUp = 20 end end -- Static clouds local staticClouds = {} local staticCloudsCount = 0 local function addStaticCloud(cloud) staticCloudsCount = staticCloudsCount + 1 staticClouds[staticCloudsCount] = cloud ac.weatherClouds[#ac.weatherClouds + 1] = cloud end local function updateStaticClouds(dt) local cutoff = math.max(math.saturateN(1 - ccClouds * 1.5), math.lerpInvSat(ccClouds, 0.6, 0.9)) ^ 2 cutoff = math.lerp(cutoff, 1, NightK) -- cutoff = 1 local lightPollution = GetRemoteLightPollution() local dtLocal = math.min(dt, 0.05) local procMapLerp = math.max(0, ccClouds - 0.5) for i = 1, staticCloudsCount do local c = staticClouds[i] local withWind = math.dot(vec2(c.side.x, c.side.z), windDir) c.noiseOffset.x = c.noiseOffset.x + (0.2 + math.saturateN(windSpeed / 100)) * 0.002 * dtLocal * withWind c.procShapeShifting = c.procShapeShifting + (1 + math.saturateN(windSpeed / 100) * (1 - withWind)) * 0.002 * dtLocal c.extraDownlit:set(lightPollution) c.extras.randomOffset = c.extras.randomOffset + dt * 0.1 c.cutoff = cutoff + math.sin(c.extras.randomOffset) * 0.2 + math.sin(-c.extras.randomOffset * 0.271) * 0.1 c.opacity = c.extras.opacity * CloudsMult c.procMap.y = math.lerp(c.extras.procMap.y, c.extras.procMap.x, procMapLerp) end end for j = 1, 35 do local angle = math.pi * 2 * (j + math.random()) / 35 local lowRow = vec2(math.sin(angle), math.cos(angle)) local count = math.floor(math.random() * 2.5 + 1) for i = 1, count do addStaticCloud(createCloud(CloudTypes.Low, lowRow, 1 - i / count)) lowRow = (lowRow + math.randomVec2():normalize() * 0.2):normalize() end end function UpdateClouds(dt) windDir = CurrentConditions.windDir windSpeed = CurrentConditions.windSpeed * 4 -- clouds move faster up there + math.perlin(Sim.timestamp / 1.03e5, 3) * 20 -- and a bit of randomization to keep the clouds moving windAngle = math.atan2(windDir.y, -windDir.x) * 180 / math.pi ccClouds = math.lerp(Sim.weatherConditions.humidity * 0.02, 1, CurrentConditions.clouds * CloudsMult) nightEarlyK = math.smoothstep(math.lerpInvSat(SunDir.y, 0.3, -0.05)) nightEarlySmoothK = nightEarlySmoothK == -1 and nightEarlyK or math.applyLag(nightEarlySmoothK, nightEarlyK, 0.99, ac.getSim().dt) updateCloudCells(dt) updateStaticClouds(dt) ac.sortClouds() ac.invalidateCloudMaps() end