Best way to allow update exceptions on a cell level
This sounds like the classic use-case for a SQL Server-based cursor.  Yes, I just said the "c" word.

Take this code, which uses your sample data:

SET NOCOUNT ON;

IF OBJECT_ID(N'dbo.tExclude', N'U') IS NOT NULL
DROP TABLE dbo.tExclude;
CREATE TABLE dbo.tExclude
(
    ID int NOT NULL
    , ColName sysname NOT NULL
);

IF OBJECT_ID(N'dbo.tBase', N'U') IS NOT NULL
DROP TABLE dbo.tBase;
CREATE TABLE dbo.tBase
(
    ID int NOT NULL
    , item_name varchar(30)
    , item_type varchar(30)
    , color varchar(30)
    , size varchar(30)
);

IF OBJECT_ID(N'dbo.tUpdate', N'U') IS NOT NULL
DROP TABLE dbo.tUpdate;
CREATE TABLE dbo.tUpdate
(
    ID int NOT NULL
    , item_name varchar(30)
    , item_type varchar(30)
    , color varchar(30)
    , size varchar(30)
);


Sample data:

INSERT INTO dbo.tExclude (ID, ColName)
VALUES (1, 'item_name')
    , (1, 'size');

INSERT INTO dbo.tBase (ID, item_name, item_type, color, size)
VALUES (1, 'A', 'Z', 'red', 'big')
    , (2, 'B', 'Y', 'blue', 'tiny')
    , (3, 'C', 'X', 'red', 'tiny')
    , (4, 'D', NULL, NULL, NULL);

INSERT INTO dbo.tUpdate(ID, item_name, item_type, color, size)
VALUES (1, 'A', 'Z', 'red', 'big')
    , (2, 'B', 'Y', 'blue', 'tiny')
    , (3, 'C', 'X', 'red', 'tiny')
    , (4, 'D', 'W', 'gold', 'big')
    , (5, 'E', 'Y', 'grey', 'cat');


At this point, the dbo.tBase table looks like:

╔════╦═══════════╦═══════════╦═══════╦══════╗
║ ID ║ item_name ║ item_type ║ color ║ size ║
╠════╬═══════════╬═══════════╬═══════╬══════╣
║  1 ║ A         ║ Z         ║ red   ║ big  ║
║  2 ║ B         ║ Y         ║ blue  ║ tiny ║
║  3 ║ C         ║ X         ║ red   ║ tiny ║
║  4 ║ D         ║ NULL      ║ NULL  ║ NULL ║
╚════╩═══════════╩═══════════╩═══════╩══════╝

Code to do the desired updates and inserts:

IF OBJECT_ID(N'tempdb..#t', N'U') IS NOT NULL
DROP TABLE #t;
CREATE TABLE #t 
(
    cmd nvarchar(max) NOT NULL
);

DECLARE @cmd nvarchar(max);
DECLARE @ID int;
DECLARE @Type varchar(6);
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC
FOR
SELECT tu.ID
    , CASE WHEN tb.ID IS NULL THEN 'INSERT' ELSE 'UPDATE' END
FROM dbo.tUpdate tu
    LEFT JOIN dbo.tBase tb ON tu.ID = tb.ID;
OPEN cur;
FETCH NEXT FROM cur INTO @ID, @Type;
WHILE @@FETCH_STATUS = 0
BEGIN
    IF @Type = 'UPDATE'
    BEGIN
        SET @cmd = N'
WITH src AS 
(
    SELECT c = QUOTENAME(c.name) + '' = '' + ''tu.'' + QUOTENAME(c.name)
    FROM sys.columns c
        INNER JOIN sys.tables t ON c.object_id = t.object_id
    WHERE t.name = ''tBase''
        AND c.name &lt;&gt; ''ID''
    EXCEPT 
    SELECT QUOTENAME(e.ColName) + '' = '' + ''tu.'' + QUOTENAME(e.ColName)
    FROM dbo.tExclude e
    WHERE ID = ' + CONVERT(nvarchar(max), @ID) + N'
)
SELECT ''UPDATE dbo.tBase
SET '' + (STUFF((SELECT '', '' + src.c FROM src FOR XML PATH ('''')), 1, 2, '''')) + ''
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = ' + CONVERT(nvarchar(max), @ID) + N';''';
        INSERT INTO #t
        EXEC sys.sp_executesql @cmd;
    END
    ELSE
    BEGIN
        SET @cmd = N'INSERT INTO dbo.tBase (ID, item_name, item_type, color, size)
SELECT ID, item_name, item_type, color, size
FROM dbo.tUpdate tu
WHERE tu.ID = ' + CONVERT(nvarchar(max), @ID) + N';';
        INSERT INTO #t (cmd)
        SELECT @cmd;
    END
    FETCH NEXT FROM cur INTO @ID, @Type;
END
CLOSE cur;
DEALLOCATE cur;

DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC
FOR
SELECT t.cmd
FROM #t t;
OPEN cur;
FETCH NEXT FROM cur INTO @cmd;
WHILE @@FETCH_STATUS = 0
BEGIN
    PRINT @cmd;
    EXEC sys.sp_executesql @cmd;
    PRINT N'';
    FETCH NEXT FROM cur INTO @cmd;
END
CLOSE cur;
DEALLOCATE cur;


The code above uses a couple of cursors to create and execute INSERT or UPDATE statements as required.  The UPDATE statements never update columns for rows included in the dbo.tExclude table.

The generated INSERT/UPDATE statements look like:

UPDATE dbo.tBase
SET [color] = tu.[color], [item_type] = tu.[item_type]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 1;

UPDATE dbo.tBase
SET [color] = tu.[color], [item_name] = tu.[item_name], [item_type] = tu.[item_type], [size] = tu.[size]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 2;

UPDATE dbo.tBase
SET [color] = tu.[color], [item_name] = tu.[item_name], [item_type] = tu.[item_type], [size] = tu.[size]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 3;

UPDATE dbo.tBase
SET [color] = tu.[color], [item_name] = tu.[item_name], [item_type] = tu.[item_type], [size] = tu.[size]
FROM dbo.tBase tb
    INNER JOIN dbo.tUpdate tu ON tb.ID = tu.ID
WHERE tb.ID = 4;

INSERT INTO dbo.tBase (ID, item_name, item_type, color, size)
SELECT ID, item_name, item_type, color, size
FROM dbo.tUpdate tu
WHERE tu.ID = 5;


And the dbo.tBase table now looks like:

SELECT *
FROM dbo.tBase;


╔════╦═══════════╦═══════════╦═══════╦══════╗
║ ID ║ item_name ║ item_type ║ color ║ size ║
╠════╬═══════════╬═══════════╬═══════╬══════╣
║  1 ║ A         ║ Z         ║ red   ║ big  ║
║  2 ║ B         ║ Y         ║ blue  ║ tiny ║
║  3 ║ C         ║ X         ║ red   ║ tiny ║
║  4 ║ D         ║ W         ║ gold  ║ big  ║
║  5 ║ E         ║ Y         ║ grey  ║ cat  ║
╚════╩═══════════╩═══════════╩═══════╩══════╝