creator实现调整图片的色调

1,297 阅读6分钟

转灰度图原理

先转灰度图,灰度图是指每个像素的颜色都相等,灰度值范围一般为0-255,白色为255,黑色为0

灰度值是通过加权平均计算得出的。这种方法的一个常见公式是:

[Y=0.299×R+0.587×G+0.114×B][ Y = 0.299 \times R + 0.587 \times G + 0.114 \times B ]

因为人眼对于不同颜色的敏感度是不同的,人眼对于绿色的敏感度最高,对红色的敏感度次之,而对蓝色的敏感度最低。

而这个公式中的系数是经过实验和调查得出的对人眼感知最为准确的比例,可以更好地反映人眼对图像的感知。

对应的shader大致逻辑为:

CCTexture(texture, v_uv0, texColor);
float y = 0.299 * texColor.r + 0.587 * texColor.g + 0.114 * texColor.b;
y = max(0.0, y);
y = min(1.0, y);
texColor.r = texColor.g = texColor.b = y;

最终效果如下: image.png

creator在2.4.10内置了置灰的shader,本质原理也是这个

image.png

混合目标颜色

vec3 c = vec3(0.0, 0.4, 0.9);// 要混合的颜色
texColor.b = c.b * y;
texColor.r = c.r * y;
texColor.g = c.g * y;

其实就是将目标颜色 x 灰度值

image.png

至此,效果就好了,下边就是如何将颜色传递到shader中 定义一个uniform,然后给spine设置material,最后设置property

sprite是怎么加载material的

  • CCSpirte.js

    _getDefaultMaterial () {
        return Material.getBuiltinMaterial('2d-sprite');
    },
  _activateMaterial () {
        let materials = this._materials;
        if (!materials[0]) {
            let material = this._getDefaultMaterial();
            materials[0] = material;
        }
 
    },
  • CCMaterial.js
 getBuiltinMaterial (name) {
    if (cc.game.renderType === cc.game.RENDER_TYPE_CANVAS) {
        return new cc.Material();
    }
    return cc.assetManager.builtins.getBuiltin('material', 'builtin-' + name);
},
  • builtins.js
    getBuiltin (type, name) {
        if (arguments.length === 0) return this._assets;
        else if (arguments.length === 1) return this._assets.get(type);// 看_assets是怎么加载的
        else return this._assets.get(type).get(name);
    },
    
        _loadBuiltins (name, cb) {
        let dirname = name  + 's';
        let builtin = this._assets.get(name);
        return cc.assetManager.internal.loadDir(dirname, null, null, (err, assets) => {
            if (err) {
                cc.error(err.message, err.stack);
            }
            else {
                for (let i = 0; i < assets.length; i++) {
                    var asset = assets[i];
                    builtin.add(asset.name, asset.addRef());
                }
            }

            cb();
        });
    },

image.png

creator也是在resources下边,这个还真的没法搞

native没有效果

native是如何设置material.define

  • EffectBase.cpp
void EffectBase::define(const std::string& name, const Value& value, int passIdx)
{
    auto& passes = getPasses();
    size_t start = 0, end = passes.size();
    if (passIdx != -1) {
        if (passIdx >= passes.size()) {
            RENDERER_LOGD("EffectBase::define error passIdx [%d]", passIdx);
            return;
        }
        start = passIdx; end = passIdx + 1;
    }
    for (size_t i = start; i < end; i++) {
        const auto& pass = passes.at(i);
        pass->define(name, value);
    }
    _dirty = true;
}
  • pass.cpp
void Pass::define(const std::string& name, const Value& value)
{
    if (_defines[name] == value) {
        return;
    };
    _defines[name] = value;
    generateDefinesKey(); // 做了一次hash
}
void Pass::generateDefinesKey() 
{
    std::string key = "";
    for (auto& def : _defines) {
        key += def.first + std::to_string(def.second.asUnsignedInt());
    }
    _definesHash = 0;
    MathUtil::combineHash(_definesHash, std::hash<std::string>{}(key));
}

所以define本质还是shader的拼接,重新linkcompile

web平台的逻辑

// base-render.js
_draw(item){ // 渲染逻辑的入口之一
    for (let i = 0; i < passes.length; ++i) {
        // 获取defines对应的program
        let program = programLib.getProgram(pass, defines, effect.name); 
        device.setProgram(program);
    }
}
// program-lib.js
getProgram(pass, defines, errPrefix) {
    // 16:2 的意思是engine的第16个shader,这个shader的第2个宏值为真
    let key = pass._programKey = pass._programKey || this.getKey(pass._programName, defines);
    let program = this._cache[key];
    if (program) {
      // 使用缓存的program
      return program;
    }
    // 缓存中没有对应key的program,就重新生成defines对应的shader的源代码
    let tmpl = this._templates[pass._programName];
    // 生成对应的头,例如:
    //   #define marco1 1
    //   #define marco2 0
    let customDef = _generateDefines(tmpl.defines, defines);
    // 替换shader中数字类型的marco
    let vert = _replaceMacroNums(tmpl.vert, tmpl.defines, defines);
    // 拼接在一起
    vert = customDef + _unrollLoops(vert);
    if (!this._highpSupported) {
      vert = _replaceHighp(vert);
    }
    
    // ...
    // frag同理
    // ...
    
    // 没有重新创建shader,然后link,compile
    program = new gfx.Program(this._device, {vert, frag});
    let errors = program.link();
    // ....
    this._cache[key] = program;
}
// 关于key的生成逻辑,name: SpineMixColor|vs|fs, defines为宏定义
getKey(name, defines) {
    let tmpl = this._templates[name];
    let key = 0;
    for (let i = 0; i < tmpl.defines.length; ++i) {
      let tmplDefs = tmpl.defines[i]; // 里面有很重要的_offset变量

      let value = defines[tmplDefs.name]; // 宏定义的值
      if (value === undefined) {
        continue;
      }
      // 内部的实现逻辑,值为真,offset左移1,否则返回0
      key |= tmplDefs._map(value);
    }
    
    // return key << 8 | tmpl.id;
    // key number maybe bigger than 32 bit, need use string to store value.
    return tmpl.id + ':' + key;
}

解释下program的key

program中的每个宏都对应字节中的每一位,值为真则该位置1,否则置0

define0define1define2define3define4define5define6define7
01010000

假如program有8个宏,第1个、3个宏为真(计数从0开始),则对应的二进制为1010(注意顺序是从右往左),对应的十进制10,因为int占用4字节,所以有32位,也就是shader可以有32个宏,一般有这么多已经足够用了。

tmpl.id

let _shdID = 0;
define(prog) {
    let id = ++_shdID; // 每次定义一个program,就自增1
    // store it
    this._templates[name] = {
        id,
        name,
        vert,
        frag,
        defines,
        attributes: prog.attributes,
        uniforms,
        extensions: prog.extensions
    };
}

以上代码就能看出来,id意思是第几个shader,从0自增的

native逻辑

// material.js
define: function define(name, val, passIdx, force) {
    if (cc.game.renderType === cc.game.RENDER_TYPE_CANVAS) return;
    "string" === typeof passIdx && (passIdx = parseInt(passIdx));
    this._effect.define(name, val, passIdx, force); // 1. 调用了effect
},
// EffectBase(adapter层)
define: function define(name, value, passIdx, force) {
    _define.call(this, name, value, passIdx, force); // 2.调用到了jsb层
    // 5.this._nativeObj的类型为EffectVariant,调用到了c++层
    this._nativeObj.define(name, value, passIdx === undefined ? -1 : passIdx);
},

//
_proto.define = function define(name, value, passIdx, force) {
    var success = false;
    var passes = this.passes;
    var start = 0, end = passes.length;
    void 0 !== passIdx && (start = passIdx, end = passIdx + 1);
    for (var i = start; i < end; i++) {
        // 3.调用到了pass层
        passes[i].define(name, value, force) && (success = true);
    }
    success || cc.warnID(9104, this.name, name);
};
// pass
_proto.define = function define(name, value, force) {
    var oldValue = this._defines[name];
    if (!force && void 0 === oldValue) return false;
    if (oldValue !== value) {
        this._defines[name] = value; // 4. 更新了define
        this._programKey = null;
    }
    return true;
};
auto cls = se::Class::create("EffectBase", obj, nullptr, nullptr);
cls->defineFunction("define", _SE(js_renderer_EffectBase_define));
static bool js_renderer_EffectBase_define(se::State& s){
    cobj->define(arg0, arg1, arg2); // 1. jsb调用到这里
}

void EffectBase::define(const std::string& name, const Value& value, int passIdx)
{
    auto& passes = getPasses();
    size_t start = 0, end = passes.size();
    if (passIdx != -1) {
        if (passIdx >= passes.size()) {
            RENDERER_LOGD("EffectBase::define error passIdx [%d]", passIdx);
            return;
        }
        start = passIdx; end = passIdx + 1;
    }
    for (size_t i = start; i < end; i++)
    {
        const auto& pass = passes.at(i);
        pass->define(name, value); // 2. 到pass层更新
    }
    _dirty = true;
}

void Pass::define(const std::string& name, const Value& value)
{
    if (_defines[name] == value)
    {
        return;
    };
    _defines[name] = value;
    generateDefinesKey();// 3. 更新了definesHash
}
void Pass::generateDefinesKey()
{
    std::string key = "";
    for (auto& def : _defines) {
        key += def.first + std::to_string(def.second.asUnsignedInt());
    }
    _definesHash = 0;
    //                     ↓ 这个参数类型是引用,所以本质是在修改 _definesHash
    MathUtil::combineHash(_definesHash, std::hash<std::string>{}(key));
}

reder源于

BaseRenderer.cpp

void BaseRenderer::draw(const StageItem& item)
{
    for (const auto& pass : item.passes)
    {
        // 
        pass->extractDefines(definesHash, __tmp_defines__);
        size_t definesHash = _definesHash; // 来自哪里,发现始终为0,也就是初始值,虽然switchProgram中有切换的逻辑,但是因为这个definesHash的问题,导致shader无法切换
        // 1. 依据name进行切换的, switchProgram会复用之前的Program,如果没有就会自动重新编译新的Program
        _program = _programLib->switchProgram(pass->getHashName(), definesHash, __tmp_defines__);
    }
}
Program* ProgramLib::switchProgram(const size_t programNameHash, const size_t definesKeyHash, const std::vector<const OrderedValueMap*>& definesList)
{
    size_t programHash = 0;
    MathUtil::combineHash(programHash, programNameHash);
    MathUtil::combineHash(programHash, definesKeyHash); // 可以看到defines也参与了hash
    // 知识点:为啥采用string hash?因为int比较非常快,string比较慢
    
    if (_current && _current->getHash() == programHash) {
        return _current;
    }
    
    auto iter = _cache.find(programHash);
    if (iter != _cache.end()) {
        return iter->second;
    }
}
Pass::Pass(const std::string& programName, Pass* parent)
: _programName(programName),
  _parent(parent)
{
    _hashName = std::hash<std::string>{}(programName);
    reset();
}
Pass::Pass(
    const std::string& programName,
    std::unordered_map<size_t, Technique::Parameter>& properties,
    ValueMap& defines
): _programName(programName)
{
    _hashName = std::hash<std::string>{}(programName); // 2. 构造函数赋值hashName
    // 2个类型的的构造函数中都没有看到hashName于define的关联
    _properties = properties;
    _defines.insert(defines.begin(), defines.end());
    generateDefinesKey();
    reset();
}

new Pass的逻辑,来自_programName

image.png

调用的源头

    // Effect
    __jsb_cocos2d_renderer_Effect_proto->defineFunction("init", _SE(js_renderer_Effect_init));

_definesHash

class ForwardRenderer final : public BaseRenderer
void ForwardRenderer::updateDefines()
{
    _definesHash = std::hash<std::string>{}(_definesKey);
}

void ForwardRenderer::updateLights(Scene* scene)
{
    const Vector<Light*> lights = scene->getLights();
    if (lights.size() > 0)
    {
        updateDefines();
    }
    _numLights = lights.size();
}
void ForwardRenderer::render(Scene* scene, float deltaTime)
{
    updateLights(scene);
}
void ForwardRenderer::renderCamera(Camera* camera, Scene* scene)
{
    updateLights(scene);
}

其实看到这里,你会发现这个_definesHash的更新和material没有关系,所以,creator native没有相关define重新编译shader的逻辑

但是为啥color没有生效呢

uniform没有生效

const Technique::Parameter* Pass::getProperty(const size_t hashName) const
{
    const auto& iter = _properties.find(hashName); // 所有的defines都在_properties
    if (_properties.end() == iter) {
        if (_parent) {
            return _parent->getProperty(hashName);
        }
        return nullptr;
    }
    else
        return &iter->second;
}

native setProperty

发现 Material.setProperty和render时的pass不是同一个

render时,pass来自哪里

原因是native的实现差异导致,需要重新设置setMaterial(0,material)即可

  skeleton._updateMaterial = function () {
    _updateMaterial.call(this);

    this._assembler && this._assembler.clearEffect();
    var baseMaterial = this.getMaterial(0);

    if (this._nativeSkeleton && baseMaterial) {
      var originHash = baseMaterial.effect.getHash();
      var id = _materialHashMap[originHash] || _materialId++;
      _materialHashMap[originHash] = id;
      baseMaterial.effect.updateHash(id); // 重要,jsb有导出
      var nativeEffect = baseMaterial.effect._nativeObj;

      this._nativeSkeleton.setEffect(nativeEffect); // 重要
    }
  };