--[[ Library for generating new license plates on-fly. Meant to look similar to Kunos license plates. Uses CM license generator assets, so might not work on some PCs (I might redo it later). To use, include with `local plates = require('shared/sim/plates')`. ]] ---@diagnostic disable local plates = {} local function isStandardLicensePlateTexture(tex) return tex and tex.size.x >= 1024 and tex.size.x == tex.size.y * 4 end -- Trim string to N glyphs, not bytes. local function trimStringGlyphs(str, len) local i, ret = 1, '' while i <= #str and len > 0 do local p, e = string.codePointAt(str, i) ret = ret..string.sub(str, i, i + e - 1) i, len = i + math.max(e, 1), len - 1 end return ret end local curProcessing = setmetatable({}, { __mode = 'kv' }) local function formRequest(meshes) local requestID = (curProcessing[meshes] or 0) + 1 curProcessing[meshes] = requestID return function () return curProcessing[meshes] == requestID end end ---Reset license plates to default ones, if they were altered by this module before. ---@param meshes ac.SceneReference ---@param carIndex integer @0-based car index. function plates.reset(meshes, carIndex) if not carIndex then return end formRequest(meshes) for i = 1, #meshes do local curDiffuse = meshes:getTextureSlotDetails(i, 'txDiffuse') local curNormal = meshes:getTextureSlotDetails(i, 'txNormal') if isStandardLicensePlateTexture(curDiffuse) and isStandardLicensePlateTexture(curNormal) then meshes:at(i) :setMaterialTexture('txDiffuse', 'car%d::car::%s' % {carIndex, curDiffuse.filename:regmatch('::(.+)') or 'plate_d.dds'}) :setMaterialTexture('txNormal', 'car%d::car::%s' % {carIndex, curNormal.filename:regmatch('::(.+)') or 'plate_nm.dds'}) end end end ---Generate a new license plate in a style similar to Kunos. Doesn’t apply to meshes that don’t use the ---same style already (guessed based on texture size). ---@param meshes ac.SceneReference @Meshes to apply texture to. ---@param text string @Text to display. If non-latin symbols are present, uses a different font. ---@param countryID ac.NationCode? @If provided, used to add a flag and a country code to the plate. ---@return boolean @Returns `false` if license plate couldn’t be generated. function plates.kunos(meshes, text, countryID) -- I’m adding it for a small tweak, for license plates online to show user names. So, for now let’s just grab -- assets from CM. Maybe later I’ll add some fallback loading them. local res = ac.getFolder(ac.FolderID.AppDataLocal)..'/AcTools Content Manager/Data/License Plates/Europe (Kunos)' local bgFilename = res..'/Countries/Italy.png' local bgNmFilename = res..'/../Europe/nm.png' if not io.fileExists(bgFilename) then return false end local req = formRequest(meshes) -- Gotta make sure it wouldn’t alter other license plates, just the standard ones. local filtered = ac.emptySceneReference() for i = 1, #meshes do local curDiffuse = meshes:getTextureSlotDetails(i, 'txDiffuse') local curNormal = meshes:getTextureSlotDetails(i, 'txNormal') if isStandardLicensePlateTexture(curDiffuse) and isStandardLicensePlateTexture(curNormal) then filtered:append(meshes:at(i)) end end if #filtered == 0 then return false end filtered:ensureUniqueMaterials() -- This texture could be a single channel, but with colors it’ll support emojis. local baseTex = ui.ExtraCanvas(vec2(1024, 256)):clear(rgbm.colors.white):update(function (dt) text = text:reggsub('[ \t\r\n]+', ' ') text = trimStringGlyphs(text, text:find(' ', 1, true) and 9 or 8) local weirdText = string.regfind(text, '[^ a-zA-Z0-9]') ui.pushDWriteFont(weirdText and ui.DWriteFont('Arial', '@System'):spacing(-10, 0, 0) or ui.DWriteFont('License Plate', res):spacing(-3.95, 0, 0)) ui.setCursor(vec2(162, weirdText and -20 or 3)) ui.beginGroup(698) ui.pushAlignment(false, 0.5) for i, v in ipairs(string.split(text, ' ', nil, true, true, true)) do if i > 1 then ui.sameLine(0, 40) end ui.dwriteTextAligned(v, weirdText and 180 or 210, ui.Alignment.Start, ui.Alignment.End, vec2(0, 256), false, rgbm(0.275, 0.247, 0.16, 1)) end ui.popAlignment() ui.endGroup() ui.popDWriteFont() end) -- Mask with MIPs to easily generate shadows and normals. local maskTex = ui.ExtraCanvas(vec2(1024, 256), math.huge, render.TextureFormat.R8.UNorm) maskTex:updateWithShader({ textures = { txBase = baseTex }, shader = 'float4 main(PS_IN pin) { return saturate(1.5 - dot(txBase.SampleLevel(samLinear, pin.Tex, 0).rgb, 0.5));}' }) local function applyResults(attempt) if not req() then return end local anyLoading = ui.imageState(bgFilename) == 'loading' anyLoading = ui.imageState(bgNmFilename) == 'loading' or anyLoading if anyLoading then ac.warn('License plate generator: base textures are still loading, attempt:', attempt) if attempt > 4 then bgFilename = 'color::#ffffff' bgNmFilename = 'color::#7f7fff' else setTimeout(function () applyResults(attempt + 1) end, 0.2) return end end -- Diffuse texture, pretty straightforward stuff. And, of course, it’ll need MIPs to look good in distance. local litTex = ui.ExtraCanvas(vec2(1024, 256), math.huge) litTex:updateWithShader({ textures = { txBg = bgFilename, txBase = baseTex, ['txMask.1'] = maskTex }, shader = [[ float4 cubic(float v){ float4 n = float4(1, 2, 3, 4) - v; float4 s = n * n * n; float x = s.x; float y = s.y - 4 * s.x; float z = s.z - 4 * s.y + 6 * s.x; float w = 6 - x - y - z; return float4(x, y, z, w); } float sampleBicubic(float2 texCoords, float level, float2 dim){ texCoords = texCoords * dim - 0.5; float2 fxy = frac(texCoords); float4 xcubic = cubic(fxy.x); float4 ycubic = cubic(fxy.y); float4 c = (texCoords - fxy).xxyy + float2(-0.5, 1.5).xyxy; float4 s = float4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw); float4 offset = (c + float4(xcubic.yw, ycubic.yw) / s) / dim.xxyy; float sample0 = txMask.SampleLevel(samLinearBorder0, offset.xz, level).r; float sample1 = txMask.SampleLevel(samLinearBorder0, offset.yz, level).r; float sample2 = txMask.SampleLevel(samLinearBorder0, offset.xw, level).r; float sample3 = txMask.SampleLevel(samLinearBorder0, offset.yw, level).r; float sx = s.x / (s.x + s.y); float sy = s.z / (s.z + s.w); return lerp(lerp(sample3, sample2, sx), lerp(sample1, sample0, sx), sy); } float sampleBicubic(float2 uv, float level) { uint2 dim; uint sampleCount; txBase.GetDimensions(level, dim.x, dim.y, sampleCount); return sampleBicubic(uv, level, (float2)dim); } float4 main(PS_IN pin) { float4 ret = txBg.SampleLevel(samLinear, pin.Tex, 0); float mask = saturate(dot(ret.rgb, 1) - 1.5); ret.rgb += (sampleBicubic(pin.Tex + float2(0, 0.006), 3) * 0.4 - sampleBicubic(pin.Tex - float2(0, 0.006), 3) * 0.5) * mask; float3 cnt = txBase.SampleLevel(samLinearBorder0, pin.Tex, 0).rgb; ret.rgb = min(ret.rgb, lerp(1, cnt, mask)); return ret; }]] }) if countryID then -- Replacing UI logo with a flag. litTex:update(function (dt) ui.drawRectFilled(vec2(30, 40), vec2(115, 220), rgbm(0.169, 0.384, 0.722, 1)) ui.drawIcon(countryID, vec2(32, 49), vec2(110, 127)) local countryFont = ui.DWriteFont('Verdana', '@System'):spacing(-1, 0, 0) ui.pushDWriteFont(countryFont) local measure = ui.measureDWriteText(countryID, 54).x ui.dwriteDrawTextClipped(countryID, 54 * math.min(80 / measure, 1), vec2(22, 120), vec2(114, 230), ui.Alignment.Center, ui.Alignment.Center) ui.popDWriteFont() end) end -- Similar story with normal map. We’re using different normal map as a base though, for compatibility -- with Kunos plates CM’s basic normal map is a bit sad. local nmTex = ui.ExtraCanvas(vec2(1024, 256), math.huge) nmTex:updateWithShader({ textures = { txBg = bgFilename, txNmBg = bgNmFilename, txBase = baseTex }, shader = [[ float4 main(PS_IN pin) { float4 bg = txBg.SampleLevel(samLinear, pin.Tex, 0); float mask = saturate(dot(bg.rgb, 1) - 1.5); float4 ret = txNmBg.SampleLevel(samLinear, pin.Tex, 0); float v0 = saturate(2 - dot(txBase.SampleLevel(samLinearBorder0, pin.Tex - float2(0.001, 0), 0).rgb, 1)); float v1 = saturate(2 - dot(txBase.SampleLevel(samLinearBorder0, pin.Tex + float2(0.001, 0), 0).rgb, 1)); float v2 = saturate(2 - dot(txBase.SampleLevel(samLinearBorder0, pin.Tex - float2(0, 0.004), 0).rgb, 1)); float v3 = saturate(2 - dot(txBase.SampleLevel(samLinearBorder0, pin.Tex + float2(0, 0.004), 0).rgb, 1)); float3 nm = normalize(float3(v1 - v0, v3 - v2, 1)); ret.rgb += nm * mask; return ret; }]] }) filtered :setMaterialTexture('txDiffuse', litTex) :setMaterialTexture('txNormal', nmTex) end applyResults(1) end return plates